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.
How the flow works:
The application frontend obtains an OAuth token (from InCountry) and a client authorization token.
The application frontend performs a find request with the obtained tokens to the RestAPI. Use
authorization
header for InCountry auth token andx-client-auth
header for client authorization token.The RestAPI retrieves records matching the provided filter and extracts their identifiers.
The RestAPI performs a request to the application backend and provides record identifiers, client authorization token, and the field list.
The application backend returns the list of filtered record identifiers and the field list which the current user can view.
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
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
.
Operation | filterEndpoint request data before accessing InCountry vault | filterEndpoint request data after accessing InCountry vault |
---|---|---|
Create / update a single record |
|
|
Delete a single record |
| |
Find records |
|
|
Create multiple records |
|
|
Delete multiple records |
| |
Aggregate records |
| |
Add a single attachment |
| |
Get a single attachment |
| |
Delete a single attachment |
|
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:
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
}
}
}'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.infoThe RestAPI will set the auth header
Authorization: Bearer {Client auth token}
with the token you provided in thex-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
andyearly_spending
fields.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 bydeals_ratio
, as theyearly_spending
field was removed by thefilterEndpoint
endpoint). The current user has no access to theyearly_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.
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 thedeals_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"
}
]
}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"
]
}
}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 thelast_name
key is disallowed for the user. The initial subset of records is further constrained to two entries instead of three. Furthermore, thelast_name
key is filtered out from all returned records.{
"ids": ["record_1", "record_2"],
"keys": ["first_name", "deals_ratio"]
}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 }
}