Skip to main content

Fine Grained Access

Overview

RestAPI Service provides an additional set of endpoints that can be used to implement an additional layer of authorization.

Let's assume we have a client-server multi-user application. There is an authorization routine in place which ensures that the current user has access to requested records and permissions to execute requested operations. Now we're adding integration with InCountry to localize data. A simple OAuth token provides full access to data stored in an environment. To implement fine-grained access control depending on the current user's permissions, the InCountry service allows specifying an endpoint to be called upon data access operations.

Authorization flow for frontend calls

How the flow works:

  1. The application frontend obtains an OAuth token (from InCountry) and a client authorization token.

  2. The application frontend performs a find request with the obtained tokens to the RestAPI. Use authorization header for InCountry auth token and x-client-auth header for client authorization token.

  3. The RestAPI retrieves records matching the provided filter and extracts their identifiers.

  4. The RestAPI performs a request to the application backend and provides record identifiers, client authorization token, and the field list.

  5. The application backend returns the list of filtered record identifiers and the field list which the current user can view.

  6. The RestAPI returns the list of filtered records (applying the row-level and filed-level access-control lists) to the application frontend for rendering.

Request/Response Payload Examples

JSON schema of a request body to the filterEndpoint endpoint

{
"$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
"properties": {
"operation": {
"enum": [
"write-record",
"read-record",
"find-records",
"delete-record",
"aggregate-records",
"add-attachment",
"get-attachment",
"delete-attachment"
]
},
"params": {
"type": "object",
"properties": {
"ids": {
"type": "array",
"items": [ { "type": "string" } ]
},
"keys": {
"type": "array",
"items": [ { "type": "string" } ]
}
},
"required": [ ]
}
},
"required": [ "operation", "params" ]
}

JSON schema of a response body from the filterEndpoint endpoint

note

The endpoint you create on the application side should return the response body in the specified format outlined below. Returning data in any other format will result in an error.

{
"$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
"properties": {
"ids": {
"type": "array",
"items": [ { "type": "string" } ]
},
"keys": {
"type": "array",
"items": [ { "type": "string" } ]
}
},
"required": [ "ids", "keys" ]
}

How the operation parameter and number of filterEndpoint calls correlate with operations

Some operations performed by the RestAPI require two requests to the filterEndpoint endpoint, while others require only one. The following table illustrates the correlation between the type of operation and the corresponding requests to be made against the filterEndpoint.

OperationfilterEndpoint request data before accessing InCountry vaultfilterEndpoint request data after accessing InCountry vault
Create / update a single record
{
"operation": "write-record",
"params": {...}
}
{ 
"operation": "read-record",
"params": {...}
}
Delete a single record
{ 
"operation": "delete-record",
"params": {...}
}
Find records
{ 
"operation": "find-records",
"params": {...}
}
{ 
"operation": "read-record",
"params": {...}
}
Create multiple records
{ 
"operation": "write-record",
"params": {...}
}
{ 
"operation": "read-record",
"params": {...}
}
Delete multiple records
{ 
"operation": "delete-record",
"params": {...}
}
Aggregate records
{ 
"operation": "aggregate-records",
"params": {...}
}
Add a single attachment
{ 
"operation": "add-attachment",
"params": {...}
}
Get a single attachment
{ 
"operation": "get-attachment",
"params": {...}
}
Delete a single attachment
{ 
"operation": "delete-attachment",
"params": {...}
}

Getting Started Example

Let's examine what you need to implement on your side to properly handle access control levels within your application with the filterEndpoint endpoint

For example, let's explore the request to find records.

Assume we have the employees schema defined as follows:

   id: Primary Key,
first_name: String,
last_name: String,
deals_ratio: Decimal,
yearly_spending: Integer

The request URL you need to use (according to the documentation below) is:

POST /webapi/employees/find

Consider the following implementation of filterEndpoint written in Javascript with ExpressJS framework:

app.post('/filter-endpoint', async (req, res) => {
const currentUser = await getCurrentUser(req.headers.authorization);
let allowedRecords, allowedFields;
switch (req.body.operation) {
case 'find-records':
allowedRecords = await currentUser.getAllowedRecords('find-records'); // Assume we have a method getAllowedRecords(operation, [ids]) which returns an array of record ids allowed for a current user for the requested operation. If ids is specified, it filters the list
allowedFields = await currentUser.getAllowedFields('find-records', req.body.params.keys); // assume we have a method getAllowedFields(operation, keys) which returns an array of fields allowed for a current user for the requested operation
res.json({
"ids": allowedRecords,
"keys": allowedFields
});
break;
case 'read-record':
allowedRecords = await currentUser.getAllowedRecords('read-record', req.body.params.ids);
allowedFields = await currentUser.getAllowedFields('read-record', req.body.params.keys);
res.json({
"ids": allowedRecords,
"keys": allowedFields
});
break;
}
})

The whole flow would be as follows:

  1. The user makes a request to the RestAPI endpoint with the following payload: For example, we want to find records of employees who are older than 40 and have an income less than or equal to $100,000.

       curl --location 'https://{restApiURLAddress}/webapi/employees/find' \
    --header 'Authorization: Bearer {InCountry auth token}' \
    --header 'x-client-auth: Bearer {Client auth token}' \
    --header 'Content-Type: application/json' \
    --data '{
    "filter": {
    "deals_ratio": {
    "$gt": 4.0
    },
    "yearly_spending": {
    "$lte": 100000
    }
    }
    }'
  2. The RestAPI makes a request to the filterEndpoint endpoint to find out the applicability of the filter to records. Here, the RestAPI passes the keys against which the user wants to look up records.

    info

    The RestAPI will set the auth header Authorization: Bearer {Client auth token} with the token you provided in the x-client-auth header upon the initial request

    // POST https://your-backend.com/filter-endpoint
    {
    "operation": "find-records",
    "params": {
    "keys": ["deals_ratio", "yearly_spending"]
    }
    }

    On this step, you can check if the current user should be able to access deals_ratio and yearly_spending fields.

  3. The filterEndpoint endpoint responds with a list of records' ids and a list of keys (schema fields) which are available for the user: Here, the RestAPI gets the response after the initial ACL check performed at the customer’s application backend. The response contains identifiers of records the user can view and the record’s keys against which the user can look up records (only by deals_ratio, as the yearly_spending field was removed by the filterEndpoint endpoint). The current user has no access to the yearly_spending field.

    {
    "ids": ["record_1", "record_2", "record_3", "record_4", "record_5"],
    "keys": ["deals_ratio"]
    }

    Alternatively, if there are too many records or for any other reason, the ids field may be set to *. In this case, the RestAPI will assume that all records are allowed for the current user. After the search is complete, there will be another step allowing the filtering out of IDs that should not be available for the current user.

  4. The RestAPI obtains records from the InCountry Vault that match the provided filter and constraints received from the filterEndpoint in Step 3. Considering that the ACL check has forbidden the search against the yearly_spending field, the RestAPI can look up records by the deals_ratio field only.

    {
    "records": [
    {
    "id": "record_1",
    "first_name": "John",
    "last_name": "Smith",
    "deals_ratio": 6.0,
    "created_at": "2023-09-02T13:07:01Z",
    "updated_at": "2023-09-02T13:07:01Z"
    },
    {
    "id": "record_2",
    "first_name": "John",
    "last_name": "Doe",
    "deals_ratio": 4.5,
    "created_at": "2023-09-01T13:07:01Z",
    "updated_at": "2023-09-01T13:07:01Z"
    },
    {
    "id": "record_3",
    "first_name": "Jane",
    "last_name": "Doe",
    "deals_ratio": 5.2,
    "created_at": "2023-09-02T13:07:01Z",
    "updated_at": "2023-09-02T13:07:01Z"
    }
    ]
    }
  5. The RestAPI performs another request to the filterEndpoint endpoint.

    // POST https://your-backend.com/filter-endpoint
    {
    "operation": "read-record",
    "params": {
    "ids": ["record_1", "record_2", "record_3"],
    "keys": [
    "first_name",
    "last_name",
    "deals_ratio"
    ]
    }
    }
  6. The filterEndpoint endpoint responds with ids and keys that are available for the current user. Assume that the user has no access to the record_3 entry. Additionally, suppose the last_name key is disallowed for the user. The initial subset of records is further constrained to two entries instead of three. Furthermore, the last_name key is filtered out from all returned records.

    {
    "ids": ["record_1", "record_2"],
    "keys": ["first_name", "deals_ratio"]
    }
  7. The RestAPI returns the following response to the user.

    {
    "data": [
    {
    "id": "record_1",
    "first_name": "John",
    "deals_ratio": 6.0,
    "created_at": "2023-09-02T13:07:01Z",
    "updated_at": "2023-09-02T13:07:01Z"
    },
    {
    "id": "record_2",
    "first_name": "John",
    "deals_ratio": 4.5,
    "created_at": "2023-09-01T13:07:01Z",
    "updated_at": "2023-09-01T13:07:01Z"
    }
    ],
    "meta": { "total": 2 }
    }