Authentication & Authorization

Authentication

Authentication happens by checking a JSON Web Token (JWT) against the public key of a trusted authentication service. The token carries scopes, strings that denote permissions, which are tested by the datapunt-authorization-django package.

Authorization Rulesets

There are two mechanisms for authorization on schema level:

  • The auth fields in the Amsterdam Schema restrict access to resources.

  • The profiles grant permissions, which were restricted by the schema.

Schema Files

The schema definitions can add an auth field on various levels:

  • The whole dataset

  • A single table

  • A single field

The absence of an auth field makes a resource publicly available.

At every level, the auth field contains a list of scopes. The JWT token of the request must contain one of these scopes to access the resource.

When there is a scope at both the dataset, table and field level these should all be satisfied to have access to the field.

The scopes of a dataset or table act as mandatory access control. When those scopes can’t be satisfied, the API returns an HTTP 403 Forbidden error. The fields act a bit different: when the scope of a field is not satisfied, the field is omitted from the response.

Sometimes it’s not possible to remove a field (for example, a geometry field for Mapbox Vector Tiles). In that case, the endpoints produces a HTTP 403 error to completely deny access.

Profiles

While the auth fields define the basic rules for authentication, Profiles provide a more fine-grained approach to authorization.

This addresses the “all or nothing” approach of auth fields that isn’t sufficient in complex cases. Note however, that profiles are only examined when authorization is already restricted. So in practice, the auth scope needs to be defined (e.g. superuser-only), and then profiles will be analysed to grant permissions for specific use-cases.

Profiles have a name, a set of scopes, and rules that grant additional permissions. When a request comes in, all profiles are checked against the request’s scopes and only matching profiles are applied.

Here’s an example profile in JSON:

{
    "name": "medewerker",
    "scopes": ["BRP/R"],
    "datasets": {
        "brp": {
            "tables": {
                "ingeschrevenpersonen": {
                    "permissions": "read",
                    "fields": {
                        "bsn": "read"
                    },
                    "mandatoryFilterSets": [
                        ["bsn", "lastname"],
                        ["postcode", "lastname"]
                    ]
                }
            }
        }
    }
}

This profile is only applied when requests have the BRP/R scope. If more than one scope is listed, all scopes must be carried for the profile to apply. By implication, an empty scopes denotes a profile that always applies.

The datasets part of the profile lists permissions granted beyond those that are granted to the scope(s) by the schema. Permissions already granted by the schema are never taken away. The permissions granted may be restricted to requests that query particular fields. With the example profile, requests with scope BRP/R gain permission to read the field bsn on the table ingeschrevenpersonen, provided that the request queries for either bsn and lastname, or postcode and lastname (or all three fields).

The mandatoryFilterSets ensures that listings are restricted on a need-to-know basis. Only when some information can be provided, the API grants access to see the remaining data. For example, a frontend office employee may only see data of someone when they can already provide their last name and postal code.

Profiles can also be used to avoid cluttering the main schema with many auth rules. Instead, deny full access to the table, and open specific fields via profiles. For example, a statistician might be allowed to read age and neighbourhood fields to aggregate data, without ever having access to identifiable data.

Application in DSO-API

The dataset and profile files stored in the repository for Amsterdam Schema. Both are imported into the DSO-API database, and loaded once on startup.

Schematools

The authorization engine is implemented within schematools as low-level Python objects. The UserScopes class provides the main logic, which is accessed within the DSO-API as request.user_scopes.has_..._access(). Each access function returns a Permission object with the granted access level. When no permission is given, the object evaluates to False in boolean comparisons (e.g. if permission).

The Permission object provides a level, sub_value and transform_function() for fine-grained access levels, such as only viewing a field as encoded or only its first three letters.

WFS Logic

Authorization is also applied to the WFS server; it’s one of the reasons for writing a custom WFS server in the first place. See the WFS Server documentation for more details.

Tip

When changing the authorization logic, make sure to test the WFS server endpoint too. While most logic is shared, it’s important to double-check no additional data is exposed.

Testing

When testing datasets with authorization from the command line you can use the maketoken management command, which generates a test token for the provided scope(s).

This requires DSO-API to be installed in the current virtualenv (cd src && pip install -e .) and the test JWKS to be in the environment. After setting the latter and getting a token with

export PUB_JWKS="$(cat jwks_test.json)"  # in src/
token=$(python manage.py maketoken BRK/RSN)

you can issue a curl command such as

curl http://localhost:8000/v1/haalcentraal/brk/kadastraalonroerendezaken/${id}/ \
    --header "Authorization: Bearer ${token}"