Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Permissions in Arminius and Nuss

Author: timorl

Some user actions, chiefly account suspensions and locks, on the homeserver require more privileges or additional authentication. Nuss also requires handling permissions for user management. Furthermore, the user might want to consent to only specific action scopes for a given login, e.g. to facilitate read only access from devices, create bots with limited permissions, or just ensure that they are not authorizing a significant action they did not intend. This document describes how we deal with all these authorization issues.

Decision

Nuss will have a configurable mapping from group membership, as indicated by appropriate metadata in the OIDC process, to allowed permission scopes for a given account. Our initial configuration will consist of three privileged groups that users can belong to, two for Matrix administrative and moderation tasks, and another for Nuss. For details, including the definitions of permission scopes, see the Solution section.

Context

Current Setup

Arminius, our Matrix homeserver, delegates user and session management to Nuss. Currently, Nuss only proxies authentication to an upstream IdP, using OIDC. In particular, it does not really proxy authorization, as it completely replaces any request with one that requests only authentication, and later just grants all requested scopes if the authentication is successful. (Yes, this is as insecure as it sounds, at the time of writing this is not remotely production grade software.) We are also synchronizing used data over SCIM, which does contain some group membership information, which might be relevant to permissions, but we are not handling this information in any way.

According to the IdP ADR Nuss will remain just an identity broker for connecting Arminius to upstream IdPs.

Constraints

  1. We can create and assign users to groups in upstream IdPs.
  2. Upstream IdPs can return all information about group membership in login tokens.
  3. Our customers must be able to manage who has access to these groups.
  4. We must support the standard Matrix scopes.
  5. We must restrict access to the Matrix endpoints spelled out in MSC4484.
  6. We must restrict access to Arminius administration endpoints in a manner consistent with Matrix administration endpoints.
  7. We must restrict access to the Nuss user management functionality. This includes:
    1. mapping upstream users to Matrix accounts,
    2. mapping user group membership to allowed permission scopes.

Desiderata

  1. It would be nice to be able to give permissions only to access specific API endpoint.
  2. In particular permissions only for specific rooms.
  3. Even more particularly, read-only permissions.

Solution

We introduce a Nuss API for defining a mapping from group membership, as represented in claims returned in ID tokens, to allowed permission scopes.

Scopes

The specific scopes we support are:

  1. urn:matrix:client:api:* – the standard Matrix scope granting access to the entire non-admin client API
  2. urn:matrix:client:server_administration – the proposed scope for accessing the Matrix admin client API, it will grant access to all endpoints mentioned in the MSC and all Arminius administration endpoints
  3. urn:matrix:client:server_moderation – a new scope only allowing for server moderation, it will grant access to all user moderation endpoints, like locks, suspensions, listing users and rooms; it will not grant access to administration endpoints (currently only Arminius key rotation); the server_administration scope expands and overrides this one
  4. urn:matrix:client:device:<device_id> – the standard Matrix scope used for associating a login with a device, as defined in the spec
  5. urn:nuss:administration – access to Nuss’ administrative endpoints, allowing mapping users to Matrix accounts and changing the mapping of groups to permission scopes
  6. urn:matrix:client:api:room:<room_id> – only grant access to operations on the specific room
  7. scopes that add read right after urn:matrix:client:api:, and create the same scope, but with read-only access

Redundant requested scopes will be removed by Nuss, only leaving the minimal strongest set – right now this only applies to overriding read-only scopes, removing the room scope if the full API scope is also present, and the Matrix administration scope overriding the moderation scope. However, Arminius will have logic valid for handling any set of scopes anyway.

We might add more scopes in the future, the above list only contains what we already know that we want.

Login Flow

On login, the user’s client will request a set of scopes it needs, like usual. Nuss will first redirect the user to the upstream IdP, where the user will login. Then it will present a consent screen, containing only the scopes that were both requested and are possible to grant based on the user’s group membership. These scopes should be presented as human-readable descriptions of the permissions being granted, but with a possibility to see the scope itself (i.e. through hovering over an UI element). Nuss will return a token granting the scopes if the user granted consent.

Mapping Definitions

A mapping consists of:

  1. the ID of the upstream IdP this mapping applies to
  2. a group membership description, represented as a JSON containing
    1. the type of mapping, currently either “All” or “IdToken”; the former represents scopes anyone can request
    2. in the case of idToken additionally a specification consisting of
      1. the claim JSON path
      2. a regexp that should be matched by the string under that path
  3. a list of allowed scopes; a scope definition ending with a * means all subscopes of that scope can be granted (including the one that literally ends with a *)

This will usually be represented as a JSON, containing additionally an ID to refer to it in some operations. Example:

{
  "id": 13,
  "idpId": 12,
  "group": {
    "IdToken": {
      "path": "roles",
      "match": "\bmatrixAdmin\b"
    }
  },
  "scopes": [
    "urn:matrix:client:server_administration",
    "urn:matrix:client:server_moderation"
  ]
}

Mapping API

The configuration API will be a standard REST API, allowing for listing, adding, modifying, and removing mappings, using the JSON format described above. The detailed definitions of endpoints will be created in the course of implementation, as there is little that isn’t obvious.

Attempting to remove the last mapping granting access to the urn:nuss:administration scope will return an error, otherwise there should be no restrictions.

Initial Configuration

On Nuss setup we will provide a configuration file defining the initial mapping as a JSON array. Minimally the mapping has to include at least one group granting the urn:nuss:administration scope, otherwise the setup should return an error. The initial mapping will be loaded into the database, and any further modifications should happen through the API. Any provided configuration file should be ignored with a warning if the database mapping is already initialized.

Our Mapping

We already mostly know the basic outline of how we would like to map groups to permissions so, while we don’t need to define that at the ADR level due to configurability, the following is both a recommendation, as well as an example.

We would like to define three groups for upstream IdPs: matrixModerator, matrixAdmin and nussAdmin. The first one grants access to the Matrix moderator endpoints (scope 3), the second one to Matrix administrator endpoints (scope 2), and the third one to Nuss administration (scope 5). All other scopes are available for any account that is mapped to a Matrix account. These scopes formatted as a JSON table should look like follows:

[
  {
    "idpId": 0,
    "group": {
      "IdToken": {
        "path": "roles",
        "match": "\bmatrixModerator\b"
      }
    },
    "scopes": [
      "urn:matrix:client:server_moderation"
    ]
  },
  {
    "idpId": 0,
    "group": {
      "IdToken": {
        "path": "roles",
        "match": "\bmatrixAdmin\b"
      }
    },
    "scopes": [
      "urn:matrix:client:server_administration",
      "urn:matrix:client:server_moderation"
    ]
  },
  {
    "idpId": 0,
    "group": {
      "IdToken": {
        "path": "roles",
        "match": "\bnussAdmin\b"
      }
    },
    "scopes": [
      "urn:nuss:administration"
    ]
  },
  {
    "idpId": 0,
    "group": "All",
    "scopes": [
      "urn:matrix:client:api:*",
      "urn:matrix:client:device:*"
    ]
  }
]

These cannot be a complete initial configuration – they refer to an IdP of ID 0, so that has to be configured as well – but this is everything related to permissions. The complete configuration is out of the scope of this document, and will most likely be finalized only during implementation.

Revocations

Revoking administration or moderation permissions, whether by changing user roles in the upstream or changing the mappings, should propagate to all sessions with the appropriate scopes, which in turn will propagate through the SCIM sync to the homeserver. Since login tokens are supposed to represent scopes, they will be marked as revoked, but users will be able to attempt a token refresh.

Other Details

We will ensure authorized access to restricted endpoints, by defining axum extractors for types representing various forms of scoped access.

A note of implementation priorities – we don’t have to implement the last two scopes listed above for the October release. Everything else described here should be there, at least in a MVP version.

We probably will want to propose some of our extended scopes as MSCs, although all of the ones proposed here are entirely optional and shouldn’t break any client compatibility, so this is not high priority.

The mapping of upstream users to Matrix accounts can also be seen as a permission issue, however this topic is left for another ADR since it comes with more problems unrelated to permissions. For the purposes of this document, we assume that before checking for allowed scopes for a specific Matrix account we first checked whether the user is even allowed to log into that account.

Rationale

With this solution we get all the access controls we want in a very flexible and configurable manner.

The solution adds relatively few assumptions about the upstream IdP on top of what we already require. Chiefly the fact that group membership can be represented as substrings in a field in the provided ID token – and even that should be easily extensible in the future if need be, since we already treat “IdToken” and “All” as an enumeration.

Group membership being determined through regexp matches might seem excessive, but unfortunately ID token fields that are lists are usually represented using just spaces as delimiters, so we cannot assume there is any JSON structure there.

We only specify a small number of additional scopes to implement, as their main point is to exemplify the schema how we could specify more. Implementing these should pave the road for any future ones we might want.

Consequences

We will have to implement:

  1. Access controls through scope handling on the Arminius and Nuss sides.
  2. API for configuring the mapping of group memberships to allowed scopes in Nuss.
  3. UI for the above, initially at least a basic version.
  4. Executing the above mapping on the Nuss side, including scope deduplication.
  5. Initial configuration logic.

This architecture should make it relatively easy to add more granular permissions and mapping to them from arbitrary sources in the future.

Nuss should be able to work with a wide range of IdPs that support just SCIM+OIDC, although initially it would require passing the group membership information in ID tokens, like Zitadel does. Getting this information out of Zitadel requires a custom scope though (urn:zitadel:iam:org:projects:roles), so we will probably have to implement some parts of the support for specific other upstreams manually.

The homeserver will have to keep information about all OIDC sessions, not just devices, to facilitate revoking tokens when the scopes associated to a session change. This will make getting devices from the database somewhat more complex, as it will require parsing some scopes.

Alternatives

Static Mapping of Groups to Allowed Scopes

We could hardcode the mapping of groups to allowed scopes, saving quite a bit of complexity and development time.

Why Not

Having this be configurable is a strict requirement.

This would limit us only to IdPs with configurable group names, and ones where we can require a very specific group assignment.

Group Membership Information in SCIM

Nuss already uses SCIM to get additional information about users, so we could read group membership information there, and use it for the mapping into allowed scope requests. This would let us work with IdPs that don’t support showing group memberships in JWTs.

We might want to implement this at some point anyway, if this turns out to be the best way to support a particular IdP.

Why Not

We currently only use SCIM data for auxiliary info, and this approach would turn it into a mandatory part of authentication.

We only periodically update SCIM data, so we would either have to manually trigger an update on every login, or risk granting outdated permissions. Having all this happen in a single flow leaves less room for authorization problems, and puts less complexity in the code.

Define All Scopes Directly on the Upstream IdP

We could in principle define all scopes in the upstream IdP, and have Nuss only proxy the request, with all the permission logic happening through configuration in the upstream. This would significantly cut down on the complexity of Nuss.

Why Not

Zitadel doesn’t support custom scopes at all, and probably neither do most other OIDC IdPs.

Even if an IdP supported such custom scopes, we would still have to deal with the urn:matrix:client:device:<device_id> scope manually, since its behaviour is very Matrix specific, and related to device and session management.