Introduction

Version 0.91 of Qvarn (JSONB) added the fine-grained access control feature. It solves a long-standing limitation with Qvarn access control. This document explains with that is all about, and how to enable the new feature, and how applications built on top of Qvarn should use the new feature.

Why fine-grained access control is needed

Qvarn has, from its very early days, used JWT tokens and their scopes for access control. This means that the API client gets an access token from an identity provider (IDP; Gluu has usually been used). The token is a digitally signed JWT token, which contains certain "claims". One of the claims is a list of "scopes". For any API request Qvarn allows the request if the token has a corresponding scope. The scopes are named systematically based on the HTTP method (GET, PUT, etc) and the path.

For example, consider the following request:

GET /persons/123456

This would map into a scope named uapi_persons_id_get. If the token included in the HTTP request has that scope, Qvarn allows the request. Otherwise, Qvarn returns an error.

This works, and allows a coarse access control. This API client can read these types of resources, but not write them, whereas this other API client can create new resources of this type, but can't read them at all. For many applications this has been sufficient.

The problem is that this approach does not differentiate between resources of the same type. If a user can read any person resource, they can read all of them. If they can view their own data (as required by the GDPR, for example), they can view anyone's data. That's obviously not acceptable. This has been worked around by having another layer of access control in the form of the "facade" application in front of Qvarn, which adds rules to what Qvarn does. The facade will know things like "users who are in an HR role can see and update the company's employee's information".

The fine-grained access control feature brings that into Qvarn itself.

Overview

The fine-grained access control feature needs to work with every type of application built on top of Qvarn. Qvarn itself, however, cannot embed application specific rules. That would make Qvarn susceptible to changes from the application requirements changing. Such changes must affect the application only.

For example, an application might need to allow every end user to view and update their own information, and any HR person to allow view and update any employee's information. The employment relation is visible via a contract resource describing it. It is not acceptable for Qvarn to understand this kind of relationship.

Instead, Qvarn adds a generic system for rules allowing access, and lets privileged API clients update those rules based on application specific requirements.

Access control rules

Fine-grained access control is enabled by setting the following configuration variable in the Qvarn configuration file (usually /etc/qvarn/qvarn.conf):

enable-fine-grained-access-control: yes

Qvarn needs to be restarted after the configuration change. The setting cannot be turned off without changing the configuration file and restarting Qvarn.

Access control rules are of the following form:

{
    "method": "GET",
    "client_id": "*",
    "user_id": "*",
    "resource_id": "*",
    "subpath": "",
    "resource_type": "person",
    "resource_field": null,
    "resource_value": null
}

The fields have the following meanings:

  • method – the HTTP method being used: GET, PUT, DELETE. Note that POST is not govenrned by fine-grained access control, since there is no resource to which a rule could provide finer access control. POST access is governed only by access token scopes.

  • client_id – the identifier of the API client, from the aud claim in the access token. May have the value "*" for any client.

  • user_id – the end-user using the API client, from the sub claim in the access token. May have the value "*" for any end-user.

  • resource_id – which resource is being affected by the rule, or "*" for all resources.

  • subpath – the name of the sub-resource, such as private for a person resource. Note that each sub-resource needs its own rule. No wildcards are allowed.

  • resource_type – the type of the resource, or null for any type.

  • resource_field – type name of a top-level resource field, or null.

  • resource_value – the value of the field named by resource_field, or null or "*" for any value.

Sometimes it is not possible to arrange for the end-user's identifier to be in the access token. In this case, if the access token in the Authorization header has the uapi_trusted_client scope, Qvarn will look for the a JWT token in a header named Qvarn-Access-By. These tokens do not need to be signed (and if they are, the signature is not checked): the API client is trusted to verify the information on their own. The first such token with a sub field will be used for the user_id matching against the access rule.

Rules are managed using the /allow endpoint:

  • POST /allow – create a new rule
  • GET /allow – check if a rule exists
  • DELETE /allow– delete a rule

Access to /allow is governed by scopes.

For each request (including GET and DELETE), the content type must be application/json and the body must have the whole access control rule expressed as JSON. Note that the /allow endpoint does not treat access control rules as resources.

The /allow endpoint works whether fine-grained access control is enabled or not. Rules can be managed even if the feature is disabled, but they do not affect API access unless the feature is enabled. This allows a Qvarn deployment to start using fine-grained access control gradually: first, create all the /allow rules that are needed, then enable the feature.

If fine-grained access control is enabled, Qvarn will allow an API request if all of the following are true:

  • The HTTP method and path match a scope in the JWT token (as before)
  • There exists an "allow" rule that satisfies all the following:
    • the sub-resource matches subpath (empty string for the main resource)
    • the client id (token aud claim) matches the rule client_id field, or the rule has value "*" for the client_id field
    • the end-user id (token sub claim) matches the rule user_id field, or the rule has value "*" for the user_id field
    • the rule resource_id field is "*" or matches the resource's identifier
    • the rule resource_type field is "*" or matches the resource's type
    • the rule resource_field field is null or the resource has a top-level field named in the rule field
    • the rule resource_value field is null or "*" or the resource has a top-level field named in the resource_field with the value given in the resource_value field

For listing resources, either via GET /foos or GET /foos/search/..., a resource is included the result if it can be retrieved by id. If the user/client cannot retrieve the field directly, it does not show up in listings, either. This avoids a data leak using searches. For example, if /persons/contains/contains/full_name/Bond would return anything, it would reveal that there exists a person named Bond. The mere existence of such a resource can be a leak.

Example: The following rule will allow an user, using any client, to read any person resource. It is pretty useless, you might as well just disable the fine-grained access control feature instead.

{
    "method": "GET",
    "client_id": "*",
    "user_id": "*",
    "resource_id": "*",
    "subpath": "",
    "resource_type": "person",
    "resource_field": null,
    "resource_value": null
}

Example: The following rule will allow user A to read their own person resource.

{
    "method": "GET",
    "client_id": "*",
    "user_id": "A",
    "resource_id": "CAFEAAAA",
    "subpath": "",
    "resource_type": "person",
    "resource_field": null,
    "resource_value": null
}

For the above rule to allow access, the IDP, which issues the JWT tokens, must have a sub field containing A to match the user_id field in the rule, and the resource corresponding to A must be CAFEAAAA.

Design of a rules engine

The "rules engine" which manages access control rules via the /allow endpoint is not part of Qvarn, and is not provided by QvarnLabs. It is something the application (facade) developer needs to write, either as part of the application or as a separate service.

The rules engine operational principle is roughly as follows:

  • use Qvarn listeners and notifications to learn of changes to relevant resources in Qvarn
  • when notification happens, add or delete access control rules via /allow

For example, a simple application might have a rules engine working like this:

  • add a listener for new contract resources
  • when there is a notification for a new employment contract, add a new /allow rule that allows every known HR person to access the person named in the contract
  • if the new contract is for an HR person, add a rule for each other employee for the company to allow the new HR person to access the employee's information
  • if a contract is deleted, delete the HR related access rules

Note that a single Qvarn instance may be used by multiple applications. There may be several rules engines for the same Qvarn instance. The application developers sharing the same Qvarn need to co-operate so that the access rules work for all applications. One approach for this is to use rules tied to specifid client identifiers.