Skip to main content

Fine Grained Access

Overview

InCountry access control system has another layer of authorization called "external ACL". It is designed to allow integration with 3rd-party services, so they can apply additional rules on top of what is set in our Portal.

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 knows only those rules, the administrator set during creation of an access policy. 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 and client authorization token.

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

  6. The RestAPI returns the list of filtered records to the application frontend for rendering.

Request/Response Payload Examples

JSON schema of a request body to the filter endpoint

{
"$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
"properties": {
"action": {
"enum": ["v1.restapi.read", "v1.restapi.write", "v1.restapi.delete"]
},
"type": {
"type": "string"
}
},
"additionalProperties": {
"type": "array",
"items": {
"type": "string"
}
},
"required": ["type", "action"],
"maxProperties": 3,
"minProperties": 3
}

JSON schema of a response body from the filter 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",
"additionalProperties": {
"oneOf": [
{
"type": "array",
"items": {
"type": "string"
}
},
{
"const": "*"
}
]
},
"maxProperties": 1,
"minProperties": 1
}

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 filter endpoint

As a beginning, you need to create an access policy in an appropriate section of InCountry portal. Please pay attention, that there is a separate field called External ACL support where you can write the URL of your filter endpoint. Then you need to assign an access policy to a set of credentials. All the following requests in this example will use those credentials.

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

Assume we have the employees schema defined as follows:

   employee_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 /api/models/employees/find

Consider the following implementation of your filter endpoint written in Javascript with ExpressJS framework:

app.post("/filter-endpoint", async (req, res) => {
const currentUser = await getCurrentUser(req.headers.authorization);
switch (req.body.action) {
case "v1.restapi.read":
// Assume we have a method getAllowedRecords(action, [ids])
// which returns an array of record ids allowed for a current user for the requested action.
// If ids is specified, it filters the list
const ids = await currentUser.getAllowedRecords("read");

res.json({
// This name should match the name we defined in the schema
employee_id: ids,
});
break;
case "v1.restapi.write":
// This one should return the given list of ids or "*" meaning all the records could be written
res.json({
// For this example, we allow this user to write any record
employee_id: "*",
});
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}/api/models/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 filter endpoint to find out the applicability of the filter to records. Here, the RestAPI passes the fields 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
    {
    "action": "v1.restapi.read",
    "type": "employees",
    "employee_id": [
    "first_employee_id",
    "second_employee_id",
    "third_employee_id"
    ]
    }

    On this step, you can check if the current user should be able to access employees data model and any of the found records.

  3. The filter endpoint responds with a list of records' IDs 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.

    {
    "employee_id": ["first_employee_id", "second_employee_id"]
    }

    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 filter endpoint in Step 3.

    {
    "records": [
    {
    "employee_id": "first_employee_id",
    "first_name": "John",
    "last_name": "Smith",
    "deals_ratio": 6.0,
    "created_at": "2023-09-02T13:07:01Z",
    "updated_at": "2023-09-02T13:07:01Z"
    },
    {
    "employee_id": "second_employee_id",
    "first_name": "John",
    "last_name": "Doe",
    "deals_ratio": 4.5,
    "created_at": "2023-09-01T13:07:01Z",
    "updated_at": "2023-09-01T13:07:01Z"
    },
    {
    "employee_id": "third_employee_id",
    "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 returns the following response to the user.

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