From 4745379d6ed557baad7a9f6874615bf1e53851d6 Mon Sep 17 00:00:00 2001 From: Sam Mesterton-Gibbons Date: Thu, 13 Feb 2025 09:42:19 +0000 Subject: [PATCH 01/33] appnote: Add appnote covering Authorisation --- README.md | 40 +++--- docs/README.md | 1 + .../0016-authorisation-in-tams-workflows.md | 121 ++++++++++++++++++ 3 files changed, 137 insertions(+), 25 deletions(-) create mode 100644 docs/appnotes/0016-authorisation-in-tams-workflows.md diff --git a/README.md b/README.md index 0d97439d..90c11f80 100644 --- a/README.md +++ b/README.md @@ -175,30 +175,7 @@ This is intended to reduce the amount of polling required by clients to keep up However the specification is deliberately left open-ended; only the message bodies are specified, but not the protocol by which they are carried nor the method by which clients subscribe. It is assumed that implementations will provide a suitable mechanism, such as a call to allow clients to subscribe to webhooks, or details of an event bus to connect to and receive the messages. -### Security - -The TAMS specification stipulates authentication methods that a client should support in order to identify themselves and provide credentials to the server, using standard HTTP approaches. -The authorisation model (the rules by which authenticated requests are allowed or denied) is not part of the TAMS specification, and is up to individual implementers and organisations depending on their exact rules, needs and threat model. - -It is assumed that implementations will apply other IT and cloud infrastructure security best practices, notably including the use of TLS (e.g. HTTPS connections) within and between their systems. - -## Mock TAMS Service - -This repo contains some automation to run a mock version of the API using [Stoplight Prism](https://stoplight.io/open-source/prism). -To run the mock server using Docker, try something like the command below (or run `make mock-server-up`): - -```shell -docker run --rm --init --name mock-tams -v "$(pwd)":/data:ro -p 4010:4010 stoplight/prism mock /data/TimeAddressableMediaStore.yaml -h 0.0.0.0 -``` - -A mock API server will start at - -## Proposals, Decisions and Architecture Changes - -This repository uses [(M)ADR documents](https://adr.github.io/madr/) to propose significant changes, facilitate discussions and decision making, and to store a record of options that were considered. -These documents may be found in the [docs/adr](./docs/adr/) directory, and are managed as described by the [ADR Readme](./docs/adr/README.md). - -## API Versioning +### API Versioning The API is versioned using a major and minor version number. A breaking change - such as removal of a feature, or renaming of properties in such a way that would break compatibility (including fixing a typo) - results in a major version increment and the minor version is reset to 0. @@ -218,7 +195,20 @@ Otherwise, the version will not change. It is possible to see what the version would be if a release was made at the current commit by running `make next-version` in the top directory of this repository. -### Making a release +### Security + +The TAMS specification stipulates authentication methods that a client should support in order to identify themselves and provide credentials to the server, using standard HTTP approaches. +The authorisation model (the rules by which authenticated requests are allowed or denied) is not part of the TAMS specification, and is up to individual implementers and organisations depending on their exact rules, needs and threat model. +However some principles and suggestions are discussed in [AppNote0016: Authorisation in TAMS workflows](./docs/appnotes/0016-authorisation-in-tams-workflows.md). + +It is assumed that implementations will apply other IT and cloud infrastructure security best practices, notably including the use of TLS (e.g. HTTPS connections) within and between their systems. + +## Proposals, Decisions and Architecture Changes + +This repository uses [(M)ADR documents](https://adr.github.io/madr/) to propose significant changes, facilitate discussions and decision making, and to store a record of options that were considered. +These documents may be found in the [docs/adr](./docs/adr/) directory, and are managed as described by the [ADR Readme](./docs/adr/README.md). + +## Making a release Run the `release` workflow under the `Actions` tab on this repository on GitHub against the `main` branch. This workflow requires approval. diff --git a/docs/README.md b/docs/README.md index b18e27c1..e63b507a 100644 --- a/docs/README.md +++ b/docs/README.md @@ -22,6 +22,7 @@ For more information on how we use application notes, see [here](./appnotes/READ | [0013](./appnotes/0013-setting-flow-bit-rate-properties.md) | Setting Flow bit rate properties | | [0014](./appnotes/0014-referencing-tams-content-in-other-systems.md) | Referencing TAMS content in other systems | | [0015](./appnotes/0015-using-tams-in-opentimelineio.md) | Using TAMS in OpenTimelineIO | +| [0016](./appnotes/0016-authorisation-in-tams-workflows.md) | Authorisation in TAMS workflows | | [0017](./appnotes/0017-reuse-of-ids.md) | When to re-use IDs in TAMS and compatible systems | | [0018](./appnotes/0018-managing-multiple-object-instances.md) | Managing Multiple Object Instances | diff --git a/docs/appnotes/0016-authorisation-in-tams-workflows.md b/docs/appnotes/0016-authorisation-in-tams-workflows.md new file mode 100644 index 00000000..95fd28b2 --- /dev/null +++ b/docs/appnotes/0016-authorisation-in-tams-workflows.md @@ -0,0 +1,121 @@ +# 0016: Authorisation in TAMS Workflows + +## Abstract + +Media workflows often contain sensitive or high-value content, and media organisations need to effectively manage access to that content across their estate. +That requires suitable approaches across both authentication (identifying the user) as discussed in [ADR0028](../adr/0028-authentication-methods.md), and also authorisation (deciding what the user can do), which is discussed here. +In general, store implementations, and the organisations that deploy them, are free to define how the authorisation model works based on their needs, however this Application Note provides some guidelines and a starting point. + +## Overall Principles + +Implementers should consider the material they need to protect, the nature of their business and their threat model when deciding how to build authorisation into TAMS-based media workflows. + +For some organisations a coarse-grained approach is sufficient: for example allowing groups of users to have read- or write-access to a store or large blocks of content. +This might be appropriate for example in a newsroom, where staff are deliberately enabled to work together and access each other's material. + +Conversely a finer-grained approach may be required, where specific rules and policies are applied to each piece of content, and groups of users are carefully managed. +This may be appropriate for example when working with a large number of third parties and freelancers in drama production, or when managing a large media archive of high-value content. + +> [!NOTE] +> Throughout this document the term "user" is used as a shorthand for all security principals, including human users, machine accounts, third-party SaaS integrations, etc. + +## Coarse Grained Authorisation + +A simple approach is to define permissions that apply to an entire TAMS instance at a very coarse level, and use Role-based Access Control (RBAC) to grant access through those permissions. +In RBAC, each action is restricted to users holding a certain role, and users are assigned the relevant roles they need. + +The permissions (or "scopes" in OAuth 2.0), could be: + +- `tams-api/read`: Allow GET and HEAD methods +- `tams-api/write`: Allow PUT and POST methods +- `tams-api/delete`: Allow DELETE methods + +_Note that these example permissions are drawn from the [AWS TAMS implementation](https://github.com/awslabs/time-addressable-media-store/blob/v3.0/README.md#usage)._ + +Then roles could be created in the authorisation system which allow some combinations of those scopes, for example: + +- `administrator`: Has all three scopes +- `viewer`: Has `tams-api/read` +- `editor`: Has `tams-api/read` and `tams-api/write` +- `store-writer`: Has `tams-api/write` +- `store-cleanup-system`: Has `tams-api/delete` + +Users, or groups of users can then be assigned into those roles: for example a "News Journalists" group might be assigned `editor`, while other staff have `viewer`, but only the automated processes of a MAM have `store-cleanup-system`. +Alternatively, the permissions can be used directly, assigning them to user groups without grouping them into roles. + +To implement the authorisation, the authorisation server checks the requested scopes against the user's access when issuing a token, and rejects the request if a suitable access isn't assigned. + +## Finer Grained Authorisation + +A further build on the very coarse role-based approach above is to expand the set of permissions to apply to specific Sources and Flows. +However the implementation of this can become complex and unwieldy, especially if each Source and Flow in the system has a separate set of permissions to manage and it becomes necessary to edit them all to implement a policy change. + +Attribute-based Access Control (ABAC) is one approach to manage this complexity, by describing permissions policies based on the attributes of resources (Sources and Flows), and if necessary, users as well. +However full ABAC can be challenging to implement and requires a degree of organisational maturity to construct and manage stable attributes. +This topic is well outside the scope of this document, however there exists plenty of literature and tools implementing ABAC in general terms. + +In practical TAMS solutions, this could look like defining a "class" attribute which can be applied to a Source or Flow, where "class" could also be thought of as "owner" or "project". +A permissions system then defines policies that apply to those classes. + +For example, consider a store shared by multiple teams from the News and Sport production teams of an organisation. +Each team have the ability to read and write their own content, and no access to the other team's content. +However in some cases it is necessary to share a particular Source (e.g. to work on a shared story) to the other team. + +| Resource | Classes | Comments | +| -------------- | ------- | -------- | +| Source Sport A | sport | | +| Source Sport B | sport | | +| Source News X | news, sport_share | This item is shared across to Sport | +| Source News Y | news | | + +The permissions policies are then implemented as: + +- Users in the "sport" user group have read and write access to all content in the "sport" class +- Users in the "news" user group have read and write access to all content in the "news" class +- Users in the "sport" user group have read access to all content in the "sport_share" class + +### Implementation + +To implement the model above, way to hold the classes in the store is needed, along with a system to store the permissions and how they map on to classes. + +For the latter, [Amazon Verified Permissions](https://aws.amazon.com/verified-permissions/) and [Permify](https://github.com/Permify/permify) both serve as permissions management tools. +They allow authorisation decisions to be made by taking a set of policies defined in some domain-specific language, along with the attributes of the user (group membership) and resource (Source/Flow class), and computing whether to allow the request. +This decision process is intended to be run inline for each request, for example at an authenticating proxy placed in front of the API server. + +For storing classes, an initial proof-of-concept could be built using Source and Flow tags: for example defining "special" tags such as `authz_class.news = 1` (using one tag per class to enable querying for presence of that tag, which is not possible with e.g. a comma-separated list). +The authenticating proxy would need to take steps to prevent unauthorised modification of this special tag. + +In addition, it should be possible to set a class on a multi-essence Source or Flow, and apply that permissions downwards to all the Sources or Flows it collects. +Similarly, classes should be set by default on a Source (and apply to all Flows), but be settable on individual Flows as well for additional flexibility. +To avoid a complex traversal of potentially a large hierarchy (and to simplify the listing endpoints), it may be useful to denormalise the tag on write, writing it to all the Sources and Flows it would affect as well. + +As a result, the process of authorising a request is: + +1. Read the list of classes assigned to the resource +2. Read the user's groups from their provided token +3. Request a decision from the permissions system based on those data +4. (Write requests only): Check whether the request would modify a special `authz_*` tag, and confirm the user has permission to make that modification +5. (Flow segment write requests only): Check if the object already exists in the store using the `/objects` endpoint, and if it does, confirm the user would have access to read it +6. (Write requests only): Propagate any changes to `authz_class` tags to Flows and Sources collected by this one + +## Where to Enforce Authorisation + +Some consideration should be given for where to apply the authorisation step, depending on how TAMS is deployed and integrated. +For example a TAMS instance could be deployed with fine-grained authorisation support, and used directly by systems across an organisation. +In this case it would make sense to treat all clients of that TAMS instance as identical from an authentication/authorisation perspective: for example a user operating an NLE would be expected to provide suitable authorised credentials, but so too would the organisations MAM when it wants access to the store. + +Another deployment approach might see a MAM or other tool expose a TAMS API interface itself, which is proxied through to some simpler backing store. +In this case the MAM might manage and enforce the other policies and rules around access to content, so it would make more sense to do the same in the TAMS API interface, and then use the MAM's own credentials to access the backing TAMS instance. + +## Future Work + +The model described above allows for more use cases than coarse-grained RBAC: especially use cases where multiple tenants share a single store. +However it would be useful to allow more attributes to be used in rules: for example making the tags of the Sources/Flows available to write policies upon as well. + +Furthermore the finer-grained model makes listing Sources and Flows difficult: items in the list from the backing database that the user does not have access to should be removed. +For a small number of policies, this may be achievable by appending `?tag=` queries in the authenticating proxy (and then merging the results if multiple classes are involved). +However for a larger and more complex system, it may be necessary to integrate policies into the TAMS implementation itself, using queries to the underlying database to limit results based on policies. + +One of the areas noted in [ADR0028: Authentication Methods](../adr/0028-authentication-methods.md) is being able to issue credentials restricted to a limited subset of Sources or Flows, which must also be supported by the authorisation system. +This could be achieved by issuing JWT bearer tokens using the [RFC9396 authorization details](https://www.rfc-editor.org/rfc/rfc9396.html#name-authorization-request) field to embed permissions granted directly into the token. +It would also allow for a suitable authenticating proxy to validate access without making a query to the authorisation server or permissions system, instead relying on the access claimed in the token, along with the cryptographic properties of the token itself. From 03f6d25127b494f15c70d1ed8753ec7b4afaa072 Mon Sep 17 00:00:00 2001 From: Sam Mesterton-Gibbons Date: Fri, 14 Feb 2025 10:37:25 +0000 Subject: [PATCH 02/33] appnote: Add AuthZ tags to tag list --- docs/appnotes/0003-tag-names.md | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/docs/appnotes/0003-tag-names.md b/docs/appnotes/0003-tag-names.md index 69918f1e..f5b3c87a 100644 --- a/docs/appnotes/0003-tag-names.md +++ b/docs/appnotes/0003-tag-names.md @@ -195,6 +195,17 @@ The type is a `boolean`. It is used to indictate the Flow should be excluded from HLS manifest generation. Defaults to `false` if the tag is not set. +### authz_class.${classname} + +Status: **Experimental** + +Suggested as a way to build lightweight Attribute-based Access Control in [AppNote0016: Authorisation in TAMS workflows](./0016-authorisation-in-tams-workflows.md). +The tag would actually be created once for each "class" associated with a Flow: for example there could be an `authz_class.news = 1` and `authz_class.sport = 1`. +As a result, queries can be constructed on the presence of specific tags, which would not be possible if it were a comma-seperated list for example `authz_class = news, sport`. +The value has no particular meaning, but tags must have a value. + +No known implementations yet. + ## Known Source Tags ### hls_exclude From dd9c8d6f0a3bfd795ba36a9b9f31761bf7262b1e Mon Sep 17 00:00:00 2001 From: James Sandford Date: Mon, 30 Jun 2025 15:38:24 +0100 Subject: [PATCH 03/33] Enumerate endpoints, methods, and recommended roles for coarse-grain auth --- .../0016-authorisation-in-tams-workflows.md | 82 +++++++++++++++---- 1 file changed, 67 insertions(+), 15 deletions(-) diff --git a/docs/appnotes/0016-authorisation-in-tams-workflows.md b/docs/appnotes/0016-authorisation-in-tams-workflows.md index 95fd28b2..036f2697 100644 --- a/docs/appnotes/0016-authorisation-in-tams-workflows.md +++ b/docs/appnotes/0016-authorisation-in-tams-workflows.md @@ -24,26 +24,78 @@ This may be appropriate for example when working with a large number of third pa A simple approach is to define permissions that apply to an entire TAMS instance at a very coarse level, and use Role-based Access Control (RBAC) to grant access through those permissions. In RBAC, each action is restricted to users holding a certain role, and users are assigned the relevant roles they need. -The permissions (or "scopes" in OAuth 2.0), could be: - -- `tams-api/read`: Allow GET and HEAD methods -- `tams-api/write`: Allow PUT and POST methods -- `tams-api/delete`: Allow DELETE methods - -_Note that these example permissions are drawn from the [AWS TAMS implementation](https://github.com/awslabs/time-addressable-media-store/blob/v3.0/README.md#usage)._ - -Then roles could be created in the authorisation system which allow some combinations of those scopes, for example: - -- `administrator`: Has all three scopes +These are the recommended permissions (or "scopes" in OAuth 2.0): + +| Endpoint | Method | `tams-api/admin` | `tams-api/read` | `tams-api/write` | `tams-api/delete` | +| ------------------------------------ | --------------- | ---------------- | --------------- | ---------------- | ----------------- | +| `/` | `HEAD`/`GET` ⚠️ | ✅ | ✅ | ✅ | ✅ | +| `/service` | `HEAD`/`GET` ⚠️ | ✅ | ✅ | ✅ | ✅ | +| | `POST` ⚠️ | ✅ | ❌ | ❌ | ❌ | +| `/service/storage-backends` | `HEAD`/`GET` ⚠️ | ❌ | ✅ | ✅ | ❌ | +| `/service/webhooks` | `HEAD`/`GET` ⚠️ | ✅ | ❌ | ❌ | ❌ | +| | `POST` ⚠️ | ✅ | ❌ | ❌ | ❌ | +| `/sources` | `HEAD`/`GET` | ❌ | ✅ | ❌ | ❌ | +| `/sources/{sourceId}` | `HEAD`/`GET` | ❌ | ✅ | ❌ | ❌ | +| `/sources/{sourceId}/tags` | `HEAD`/`GET` | ❌ | ✅ | ❌ | ❌ | +| `/sources/{sourceId}/tags/{name}` | `HEAD`/`GET` | ❌ | ✅ | ❌ | ❌ | +| | `PUT` | ❌ | ❌ | ✅ | ❌ | +| | `DELETE` ⚠️ | ❌ | ❌ | ✅ | ❌ | +| `/sources/{sourceId}/description` | `HEAD`/`GET` | ❌ | ✅ | ❌ | ❌ | +| | `PUT` | ❌ | ❌ | ✅ | ❌ | +| | `DELETE` ⚠️ | ❌ | ❌ | ✅ | ❌ | +| `/sources/{sourceId}/label` | `HEAD`/`GET` | ❌ | ✅ | ❌ | ❌ | +| | `PUT` | ❌ | ❌ | ✅ | ❌ | +| | `DELETE` ⚠️ | ❌ | ❌ | ✅ | ❌ | +| `/flows` | `HEAD`/`GET` | ❌ | ✅ | ❌ | ❌ | +| `/flows/{flowId}` | `HEAD`/`GET` | ❌ | ✅ | ❌ | ❌ | +| | `PUT` | ❌ | ❌ | ✅ | ❌ | +| | `DELETE` | ❌ | ❌ | ❌ | ✅ | +| `/flows/{flowId}/tags` | `HEAD`/`GET` | ❌ | ✅ | ❌ | ❌ | +| `/flows/{flowId}/tags/{name}` | `HEAD`/`GET` | ❌ | ✅ | ❌ | ❌ | +| | `PUT` | ❌ | ❌ | ✅ | ❌ | +| | `DELETE` ⚠️ | ❌ | ❌ | ✅ | ❌ | +| `/flows/{flowId}/description` | `HEAD`/`GET` | ❌ | ✅ | ❌ | ❌ | +| | `PUT` | ❌ | ❌ | ✅ | ❌ | +| | `DELETE` ⚠️ | ❌ | ❌ | ✅ | ❌ | +| `/flows/{flowId}/label` | `HEAD`/`GET` | ❌ | ✅ | ❌ | ❌ | +| | `PUT` | ❌ | ❌ | ✅ | ❌ | +| | `DELETE` ⚠️ | ❌ | ❌ | ✅ | ❌ | +| `/flows/{flowId}/read_only` | `HEAD`/`GET` | ❌ | ✅ | ❌ | ❌ | +| | `PUT` | ❌ | ❌ | ✅ | ❌ | +| `/flows/{flowId}/flow_collection` | `HEAD`/`GET` | ❌ | ✅ | ❌ | ❌ | +| | `PUT` | ❌ | ❌ | ✅ | ❌ | +| | `DELETE` ⚠️ | ❌ | ❌ | ✅ | ❌ | +| `/flows/{flowId}/max_bit_rate` | `HEAD`/`GET` | ❌ | ✅ | ❌ | ❌ | +| | `PUT` | ❌ | ❌ | ✅ | ❌ | +| | `DELETE` ⚠️ | ❌ | ❌ | ✅ | ❌ | +| `/flows/{flowId}/avg_bit_rate` | `HEAD`/`GET` | ❌ | ✅ | ❌ | ❌ | +| | `PUT` | ❌ | ❌ | ✅ | ❌ | +| | `DELETE` ⚠️ | ❌ | ❌ | ✅ | ❌ | +| `/flows/{flowId}/segments` | `HEAD`/`GET` | ❌ | ✅ | ❌ | ❌ | +| | `POST` | ❌ | ❌ | ✅ | ❌ | +| | `DELETE` | ❌ | ❌ | ❌ | ✅ | +| `/flows/{flowId}/storage` | `POST` | ❌ | ❌ | ✅ | ❌ | +| `/objects/{objectId}` | `HEAD`/`GET` | ❌ | ✅ | ❌ | ❌ | +| `/flow-delete-requests` | `HEAD`/`GET` ⚠️ | ✅ | ❌ | ❌ | ❌ | +| `/flow-delete-requests/{request-id}` | `HEAD`/`GET` ⚠️ | ❌ | ❌ | ❌ | ✅ | + +Key for the listing: + +- ✅: Allow method with this OAuth scope +- ❌: Do not allow method with this OAuth scope. +Other claimed scopes may still allow this method +- ⚠️: Method does not follow the basic mapping of `tams-api/read` to `HEAD`/`GET`, `tams-api/write` to `POST`/`PUT`, and `tams-api/delete` to `DELETE` + +Users may be assigned combinations of these roles for different purposes, for example: + +- `administrator`: Has all four scopes - `viewer`: Has `tams-api/read` - `editor`: Has `tams-api/read` and `tams-api/write` - `store-writer`: Has `tams-api/write` - `store-cleanup-system`: Has `tams-api/delete` -Users, or groups of users can then be assigned into those roles: for example a "News Journalists" group might be assigned `editor`, while other staff have `viewer`, but only the automated processes of a MAM have `store-cleanup-system`. -Alternatively, the permissions can be used directly, assigning them to user groups without grouping them into roles. - -To implement the authorisation, the authorisation server checks the requested scopes against the user's access when issuing a token, and rejects the request if a suitable access isn't assigned. +To implement the authorisation, the authorisation server checks the requested scopes against the user's access when issuing a token. +The TAMS server, or it's auth proxy, rejects requests without appropriate scopes. ## Finer Grained Authorisation From f52328aa0e4e240abb372e1b5674c8a6e82a9b38 Mon Sep 17 00:00:00 2001 From: James Sandford Date: Wed, 2 Jul 2025 12:34:16 +0100 Subject: [PATCH 04/33] Enumerate endpoints, methods, and recommended auth logic for fine-grained auth --- .../0016-authorisation-in-tams-workflows.md | 165 ++++++++++++++---- 1 file changed, 134 insertions(+), 31 deletions(-) diff --git a/docs/appnotes/0016-authorisation-in-tams-workflows.md b/docs/appnotes/0016-authorisation-in-tams-workflows.md index 036f2697..ca4d1d7c 100644 --- a/docs/appnotes/0016-authorisation-in-tams-workflows.md +++ b/docs/appnotes/0016-authorisation-in-tams-workflows.md @@ -31,7 +31,7 @@ These are the recommended permissions (or "scopes" in OAuth 2.0): | `/` | `HEAD`/`GET` ⚠️ | ✅ | ✅ | ✅ | ✅ | | `/service` | `HEAD`/`GET` ⚠️ | ✅ | ✅ | ✅ | ✅ | | | `POST` ⚠️ | ✅ | ❌ | ❌ | ❌ | -| `/service/storage-backends` | `HEAD`/`GET` ⚠️ | ❌ | ✅ | ✅ | ❌ | +| `/service/storage-backends` | `HEAD`/`GET` ⚠️ | ✅ | ✅ | ✅ | ✅ | | `/service/webhooks` | `HEAD`/`GET` ⚠️ | ✅ | ❌ | ❌ | ❌ | | | `POST` ⚠️ | ✅ | ❌ | ❌ | ❌ | | `/sources` | `HEAD`/`GET` | ❌ | ✅ | ❌ | ❌ | @@ -102,53 +102,149 @@ The TAMS server, or it's auth proxy, rejects requests without appropriate scopes A further build on the very coarse role-based approach above is to expand the set of permissions to apply to specific Sources and Flows. However the implementation of this can become complex and unwieldy, especially if each Source and Flow in the system has a separate set of permissions to manage and it becomes necessary to edit them all to implement a policy change. -Attribute-based Access Control (ABAC) is one approach to manage this complexity, by describing permissions policies based on the attributes of resources (Sources and Flows), and if necessary, users as well. +Attribute-Based Access Control (ABAC) is one approach to manage this complexity, by describing permissions policies based on the attributes of resources (Sources and Flows), and if necessary, users as well. However full ABAC can be challenging to implement and requires a degree of organisational maturity to construct and manage stable attributes. -This topic is well outside the scope of this document, however there exists plenty of literature and tools implementing ABAC in general terms. +This section describes a recommended approach to ABAC authorisation logic to aid interoperability between TAMS implementations. +This approach should be considered experimental at this point. +Due its experimental nature, this approach makes use of the tags feature in TAMS. +Future iterations of these proposals may elevate ABAC attributes to a specific field in the core specification. -In practical TAMS solutions, this could look like defining a "class" attribute which can be applied to a Source or Flow, where "class" could also be thought of as "owner" or "project". -A permissions system then defines policies that apply to those classes. +### Scopes + +In practical TAMS solutions, ABAC could look like defining a "scope" tag which maps to OAuth scopes. +A permissions system then defines policies that apply to those scopes. + +These scopes could use the following patterns: + +| Scope name regex | Meaning | Example | +| `^admin$` | Grants all permission to the user | `admin` | +| `^\w+_read$` | Grants read access to the user | `news_read` | +| `^\w+_write$` | Grants write access to the user | `news_write` | +| `^\w+_delete$` | Grants delete access to the user | `news_delete` | For example, consider a store shared by multiple teams from the News and Sport production teams of an organisation. Each team have the ability to read and write their own content, and no access to the other team's content. However in some cases it is necessary to share a particular Source (e.g. to work on a shared story) to the other team. -| Resource | Classes | Comments | -| -------------- | ------- | -------- | -| Source Sport A | sport | | -| Source Sport B | sport | | -| Source News X | news, sport_share | This item is shared across to Sport | -| Source News Y | news | | - -The permissions policies are then implemented as: - -- Users in the "sport" user group have read and write access to all content in the "sport" class -- Users in the "news" user group have read and write access to all content in the "news" class -- Users in the "sport" user group have read access to all content in the "sport_share" class +| Resource | Scopes | Comments | +| -------------- | ------------------------------------------------------ | --------------------------------------------------- | +| Source Sport A | `sport_read`, `sport_write`, `sport_delete` | Sport have full access. News have no access. | +| Source Sport B | `sport_read`, `sport_write`, `sport_delete` | Sport have full access. News have no access. | +| Source News X | `news_read`, `news_write`, `news_delete`, `sport_read` | News have full access. Sport have read access only. | +| Source News Y | `news_read`, `news_write`, `news_delete` | News have full access. Sport have no access. | + +As another example, consider an organisation which makes all content readable to all by default to promote reuse of content. +All users are assigned a special `global_read` scope. +The `global_read` scope is automatically assigned to all content by default, via some means not specified here. + +| Resource | Scopes | Comments | +| -------------- | -------------------------------------------- | --------------------------------------------- | +| Source Sport A | `global_read`, `sport_write`, `sport_delete` | All have read access. Sport have full access. | +| Source Sport B | `global_read`, `sport_write`, `sport_delete` | All have read access. Sport have full access. | +| Source News X | `global_read`, `news_write`, `news_delete` | All have read access. News have full access. | +| Source News Y | `global_read`, `news_write`, `news_delete` | All have read access. News have full access. | + +### Auth logic + +In order that implementations may have consistent expectations about which methods they may access, this section provides recommended auth logic for methods. + +It is assumed that the `admin` scope grants permission to execute all methods on all endpoints. +It is only explicitly called out in the listing below where `admin` is the only scope which grants permission. + +Where requests are rejected, they should return as follows: + +- `404` if the request has no permissions on the endpoint +- `403` if the request has any permission on the endpoint, but not sufficient to complete the request + +The listing below refers to requests having permissions, rather than users. +This is to account for cases where users only "claim" a subset of their permissions for a given request. +Note that in some circumstances, requests may have to claim more permissions than may initially be assumed. +For example - when editing scopes on a Source/Flow, requests must claim both write permissions and the permission they are changing. +i.e. If the request adds or removes delete permissions for any group, it must have valid delete permissions itself. +This is to prevent permission escalation attacks such as a user with write permissions adding delete permissions to themselves. + +Implementations may choose to additionally filter data based on the claimed scopes of a request. +For example, where a Source collections may be filtered to only include Sources the request has read/write/delete permissions on. +Implementers should consider the implications of hiding data. +For example - hiding collection relationships may result in clients deciding to delete a resource which, unknowingly, is still referenced by another. + +| Endpoint | Method | Auth logic | +| ------------------------------------ | ------------ | ------------------------------------------------------------------------------------------------- | +| `/` | `HEAD`/`GET` | Available to all | +| `/service` | `HEAD`/`GET` | Available to all | +| | `POST` | Execute if `admin` in claimed scopes. Otherwise reject. | +| `/service/storage-backends` | `HEAD`/`GET` | Available to all | +| `/service/webhooks` | `HEAD`/`GET` | Return if `admin` in claimed scopes. Otherwise reject. | +| | `POST` | Execute if `admin` in claimed scopes. Otherwise reject. | +| `/sources` | `HEAD`/`GET` | Restrict returned data by adding list of claimed scopes to `tag.scope.includes`. If the incoming request has `tag.scope.includes` set, the request must be processed with `tag.scope.includes` set to the intersection of the claimed scopes and the provided list in `tag.scope.includes`. | +| `/sources/{sourceId}` | `HEAD`/`GET` | Return result if any claimed read scope is in `/sources/{sourceId}/tags/scope`. Otherwise reject. | +| `/sources/{sourceId}/tags` | `HEAD`/`GET` | Return result if any claimed read scope is in `/sources/{sourceId}/tags/scope`. Otherwise reject. | +| `/sources/{sourceId}/tags/{name}` | `HEAD`/`GET` | Return result if any claimed read scope is in `/sources/{sourceId}/tags/scope`. Otherwise reject. | +| | `PUT` | Execute if any claimed write scope is in `/sources/{sourceId}/tags/scope`. If the request is to `/sources/{sourceId}/tags/scope` the request must have the permissions being edited. i.e. If the request adds or removes delete permissions for any group, it must have delete claim which is present in `/sources/{sourceId}/tags/scope`. Otherwise, reject. | +| | `DELETE` | Execute if any claimed write scope is in `/sources/{sourceId}/tags/scope`. If the request is to `/sources/{sourceId}/tags/scope` the request must have the permissions being edited. i.e. If the request adds or removes delete permissions for any group, it must have delete claim which is present in `/sources/{sourceId}/tags/scope`. Otherwise, reject. | +| `/sources/{sourceId}/description` | `HEAD`/`GET` | Return result if any claimed read scope is in `/sources/{sourceId}/tags/scope`. Otherwise reject. | +| | `PUT` | Execute if any claimed write scope is in `/sources/{sourceId}/tags/scope`. Otherwise, reject. | +| | `DELETE` | Execute if any claimed write scope is in `/sources/{sourceId}/tags/scope`. Otherwise, reject. | +| `/sources/{sourceId}/label` | `HEAD`/`GET` | Return result if any claimed read scope is in `/sources/{sourceId}/tags/scope`. Otherwise reject. | +| | `PUT` | Execute if any claimed write scope is in `/sources/{sourceId}/tags/scope`. Otherwise, reject. | +| | `DELETE` | Execute if any claimed write scope is in `/sources/{sourceId}/tags/scope`. Otherwise, reject. | +| `/flows` | `HEAD`/`GET` | Restrict returned data by adding list of claimed scopes to `tag.scope.includes`. If the incoming request has `tag.scope.includes` set, the request must be processed with `tag.scope.includes` set to the intersection of the claimed scopes and the provided list in `tag.scope.includes`. | +| `/flows/{flowId}` | `HEAD`/`GET` | Return result if any claimed read scope is in `/flows/{flowId}/tags/scope`. Otherwise reject. | +| | `PUT` | Execute if any claimed write scope is in `/sources/{sourceId}/tags/scope` for the Flow's Source ID, or the Source ID doesn't currently exist in this TAMS instance. Otherwise, reject. | +| | `DELETE` | Return result if any claimed delete scope is in `/flows/{flowId}/tags/scope`. Otherwise reject. | +| `/flows/{flowId}/tags` | `HEAD`/`GET` | Return result if any claimed read scope is in `/flows/{flowId}/tags/scope`. Otherwise reject. | +| `/flows/{flowId}/tags/{name}` | `HEAD`/`GET` | Return result if any claimed read scope is in `/flows/{flowId}/tags/scope`. Otherwise reject. | +| | `PUT` | Execute if any claimed write scope is in `/flows/{flowId}/tags/scope`. If the request is to `/flows/{flowId}/tags/scope` the request must have the permissions being edited. i.e. If the request adds or removes delete permissions for any group, it must have delete claim which is present in `/flows/{flowId}/tags/scope`. Otherwise, reject. | +| | `DELETE` | Execute if any claimed write scope is in `/flows/{flowId}/tags/scope`. If the request is to `/flows/{flowId}/tags/scope` the request must have the permissions being edited. i.e. If the request adds or removes delete permissions for any group, it must have delete claim which is present in `/flows/{flowId}/tags/scope`. Otherwise, reject. | +| `/flows/{flowId}/description` | `HEAD`/`GET` | Return result if any claimed read scope is in `/flows/{flowId}/tags/scope`. Otherwise reject. | +| | `PUT` | Execute if any claimed write scope is in `/flows/{flowId}/tags/scope`. Otherwise reject. | +| | `DELETE` | Execute if any claimed write scope is in `/flows/{flowId}/tags/scope`. Otherwise reject. | +| `/flows/{flowId}/label` | `HEAD`/`GET` | Return result if any claimed read scope is in `/flows/{flowId}/tags/scope`. Otherwise reject. | +| | `PUT` | Execute if any claimed write scope is in `/flows/{flowId}/tags/scope`. Otherwise reject. | +| | `DELETE` | Execute if any claimed write scope is in `/flows/{flowId}/tags/scope`. Otherwise reject. | +| `/flows/{flowId}/read_only` | `HEAD`/`GET` | Return result if any claimed read scope is in `/flows/{flowId}/tags/scope`. Otherwise reject. | +| | `PUT` | Execute if any claimed write scope is in `/flows/{flowId}/tags/scope`. Otherwise reject. | +| `/flows/{flowId}/flow_collection` | `HEAD`/`GET` | Return result if any claimed read scope is in `/flows/{flowId}/tags/scope`. Otherwise reject. | +| | `PUT` | Execute if any claimed write scope is in `/flows/{flowId}/tags/scope`. Otherwise reject. | +| | `DELETE` | Execute if any claimed write scope is in `/flows/{flowId}/tags/scope`. Otherwise reject. | +| `/flows/{flowId}/max_bit_rate` | `HEAD`/`GET` | Return result if any claimed read scope is in `/flows/{flowId}/tags/scope`. Otherwise reject. | +| | `PUT` | Execute if any claimed write scope is in `/flows/{flowId}/tags/scope`. Otherwise reject. | +| | `DELETE` | Execute if any claimed write scope is in `/flows/{flowId}/tags/scope`. Otherwise reject. | +| `/flows/{flowId}/avg_bit_rate` | `HEAD`/`GET` | Return result if any claimed read scope is in `/flows/{flowId}/tags/scope`. Otherwise reject. | +| | `PUT` | Execute if any claimed write scope is in `/flows/{flowId}/tags/scope`. Otherwise reject. | +| | `DELETE` | Execute if any claimed write scope is in `/flows/{flowId}/tags/scope`. Otherwise reject. | +| `/flows/{flowId}/segments` | `HEAD`/`GET` | Return result if any claimed read scope is in `/flows/{flowId}/tags/scope`. Otherwise reject. | +| | `POST` | Execute if any claimed write scope is in `/flows/{flowId}/tags/scope`, and any flows are listed in `referenced_by_flows` at `/objects/{objectId}` with `flow_tag.scope.includes` set to claimed read scopes for each Object ID being written. Otherwise reject. | +| | `DELETE` | Execute if any claimed write scope is in `/flows/{flowId}/tags/scope`. Otherwise reject. | +| `/flows/{flowId}/storage` | `POST` | Execute if any claimed write scope is in `/flows/{flowId}/tags/scope`. Otherwise reject. | +| `/objects/{objectId}` | `HEAD`/`GET` | Restrict returned data by adding list of claimed scopes to `flow_tag.scope.includes`. If the incoming request has `flow_tag.scope.includes` set, the request must be processed with `flow_tag.scope.includes` set to the intersection of the claimed scopes and the provided list in `flow_tag.scope.includes`. | +| `/flow-delete-requests` | `HEAD`/`GET` | Return if `admin` in claimed scopes. Otherwise reject. | +| `/flow-delete-requests/{request-id}` | `HEAD`/`GET` | Return result if any claimed delete scope is in `/flows/{flowId}/tags/scope` for the Delete Request's Flow ID. Otherwise reject. | ### Implementation -To implement the model above, way to hold the classes in the store is needed, along with a system to store the permissions and how they map on to classes. +To implement the model above, away to hold the scopes in TAMS is needed, along with a system to store the permissions and the authorisation logic that maps them to scopes. For the latter, [Amazon Verified Permissions](https://aws.amazon.com/verified-permissions/) and [Permify](https://github.com/Permify/permify) both serve as permissions management tools. -They allow authorisation decisions to be made by taking a set of policies defined in some domain-specific language, along with the attributes of the user (group membership) and resource (Source/Flow class), and computing whether to allow the request. +They allow authorisation decisions to be made by taking a set of policies defined in some domain-specific language, along with the attributes of the user (group membership) and resource (Source/Flow scope), and computing whether to allow the request. This decision process is intended to be run inline for each request, for example at an authenticating proxy placed in front of the API server. -For storing classes, an initial proof-of-concept could be built using Source and Flow tags: for example defining "special" tags such as `authz_class.news = 1` (using one tag per class to enable querying for presence of that tag, which is not possible with e.g. a comma-separated list). -The authenticating proxy would need to take steps to prevent unauthorised modification of this special tag. +For storing scopes an initial proof-of-concept could be built, as described above, using Source and Flow tags. +A "special" `scope` tag would store a comma-separated list of scopes assigned to a Flow or Source +The authenticating proxy would need to take steps to prevent unauthorised modification of this special tag, as described above. -In addition, it should be possible to set a class on a multi-essence Source or Flow, and apply that permissions downwards to all the Sources or Flows it collects. -Similarly, classes should be set by default on a Source (and apply to all Flows), but be settable on individual Flows as well for additional flexibility. +In addition, it should be possible to set a scope on a multi-essence Source or Flow, and apply that permissions downwards to all the Sources or Flows it collects. +Similarly, scopes should be set by default on a Source (and apply to all Flows), but be settable on individual Flows as well for additional flexibility. To avoid a complex traversal of potentially a large hierarchy (and to simplify the listing endpoints), it may be useful to denormalise the tag on write, writing it to all the Sources and Flows it would affect as well. As a result, the process of authorising a request is: -1. Read the list of classes assigned to the resource -2. Read the user's groups from their provided token +1. Read the list of scopes assigned to the resource +2. Read the user's claimed scopes from their provided token 3. Request a decision from the permissions system based on those data -4. (Write requests only): Check whether the request would modify a special `authz_*` tag, and confirm the user has permission to make that modification +4. (Write requests only): Check whether the request would modify the special `scope` tag, and confirm the user has permission to make that modification 5. (Flow segment write requests only): Check if the object already exists in the store using the `/objects` endpoint, and if it does, confirm the user would have access to read it -6. (Write requests only): Propagate any changes to `authz_class` tags to Flows and Sources collected by this one +6. (Write requests only): Propagate any changes to the `scope` tag to Flows and Sources collected by this one ## Where to Enforce Authorisation @@ -159,15 +255,22 @@ In this case it would make sense to treat all clients of that TAMS instance as i Another deployment approach might see a MAM or other tool expose a TAMS API interface itself, which is proxied through to some simpler backing store. In this case the MAM might manage and enforce the other policies and rules around access to content, so it would make more sense to do the same in the TAMS API interface, and then use the MAM's own credentials to access the backing TAMS instance. +## Use Cases + +### Providing access to a subset of a Flow's timerange + +The model described above allows access to content to be controlled at the Source/Flow level. +Some use cases may require finer grained control. +Object-level access control was deemed to be too inefficient to implement. +It may, however, be achieved by creating a new flow with the relevant permissions that refers to the Objects of interest. +Caution should be taken where the boundary timestamps land partway through an Object. +Where the material around the boundaries is sensitive, new trimmed Objects should be created at the boundaries that only include the media used in the new Flow. + ## Future Work The model described above allows for more use cases than coarse-grained RBAC: especially use cases where multiple tenants share a single store. However it would be useful to allow more attributes to be used in rules: for example making the tags of the Sources/Flows available to write policies upon as well. -Furthermore the finer-grained model makes listing Sources and Flows difficult: items in the list from the backing database that the user does not have access to should be removed. -For a small number of policies, this may be achievable by appending `?tag=` queries in the authenticating proxy (and then merging the results if multiple classes are involved). -However for a larger and more complex system, it may be necessary to integrate policies into the TAMS implementation itself, using queries to the underlying database to limit results based on policies. - One of the areas noted in [ADR0028: Authentication Methods](../adr/0028-authentication-methods.md) is being able to issue credentials restricted to a limited subset of Sources or Flows, which must also be supported by the authorisation system. This could be achieved by issuing JWT bearer tokens using the [RFC9396 authorization details](https://www.rfc-editor.org/rfc/rfc9396.html#name-authorization-request) field to embed permissions granted directly into the token. It would also allow for a suitable authenticating proxy to validate access without making a query to the authorisation server or permissions system, instead relying on the access claimed in the token, along with the cryptographic properties of the token itself. From ff638553f538d033b0b965c1e43325be7f2a91c1 Mon Sep 17 00:00:00 2001 From: James Sandford Date: Wed, 2 Jul 2025 12:38:04 +0100 Subject: [PATCH 05/33] =?UTF-8?q?Remove=20=E2=9D=8C=20emoji=20from=20coars?= =?UTF-8?q?e-grain=20auth=20table?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../0016-authorisation-in-tams-workflows.md | 96 +++++++++---------- 1 file changed, 47 insertions(+), 49 deletions(-) diff --git a/docs/appnotes/0016-authorisation-in-tams-workflows.md b/docs/appnotes/0016-authorisation-in-tams-workflows.md index ca4d1d7c..9cf1e85f 100644 --- a/docs/appnotes/0016-authorisation-in-tams-workflows.md +++ b/docs/appnotes/0016-authorisation-in-tams-workflows.md @@ -30,60 +30,58 @@ These are the recommended permissions (or "scopes" in OAuth 2.0): | ------------------------------------ | --------------- | ---------------- | --------------- | ---------------- | ----------------- | | `/` | `HEAD`/`GET` ⚠️ | ✅ | ✅ | ✅ | ✅ | | `/service` | `HEAD`/`GET` ⚠️ | ✅ | ✅ | ✅ | ✅ | -| | `POST` ⚠️ | ✅ | ❌ | ❌ | ❌ | +| | `POST` ⚠️ | ✅ | | | | | `/service/storage-backends` | `HEAD`/`GET` ⚠️ | ✅ | ✅ | ✅ | ✅ | -| `/service/webhooks` | `HEAD`/`GET` ⚠️ | ✅ | ❌ | ❌ | ❌ | -| | `POST` ⚠️ | ✅ | ❌ | ❌ | ❌ | -| `/sources` | `HEAD`/`GET` | ❌ | ✅ | ❌ | ❌ | -| `/sources/{sourceId}` | `HEAD`/`GET` | ❌ | ✅ | ❌ | ❌ | -| `/sources/{sourceId}/tags` | `HEAD`/`GET` | ❌ | ✅ | ❌ | ❌ | -| `/sources/{sourceId}/tags/{name}` | `HEAD`/`GET` | ❌ | ✅ | ❌ | ❌ | -| | `PUT` | ❌ | ❌ | ✅ | ❌ | -| | `DELETE` ⚠️ | ❌ | ❌ | ✅ | ❌ | -| `/sources/{sourceId}/description` | `HEAD`/`GET` | ❌ | ✅ | ❌ | ❌ | -| | `PUT` | ❌ | ❌ | ✅ | ❌ | -| | `DELETE` ⚠️ | ❌ | ❌ | ✅ | ❌ | -| `/sources/{sourceId}/label` | `HEAD`/`GET` | ❌ | ✅ | ❌ | ❌ | -| | `PUT` | ❌ | ❌ | ✅ | ❌ | -| | `DELETE` ⚠️ | ❌ | ❌ | ✅ | ❌ | -| `/flows` | `HEAD`/`GET` | ❌ | ✅ | ❌ | ❌ | -| `/flows/{flowId}` | `HEAD`/`GET` | ❌ | ✅ | ❌ | ❌ | -| | `PUT` | ❌ | ❌ | ✅ | ❌ | -| | `DELETE` | ❌ | ❌ | ❌ | ✅ | -| `/flows/{flowId}/tags` | `HEAD`/`GET` | ❌ | ✅ | ❌ | ❌ | -| `/flows/{flowId}/tags/{name}` | `HEAD`/`GET` | ❌ | ✅ | ❌ | ❌ | -| | `PUT` | ❌ | ❌ | ✅ | ❌ | -| | `DELETE` ⚠️ | ❌ | ❌ | ✅ | ❌ | -| `/flows/{flowId}/description` | `HEAD`/`GET` | ❌ | ✅ | ❌ | ❌ | -| | `PUT` | ❌ | ❌ | ✅ | ❌ | -| | `DELETE` ⚠️ | ❌ | ❌ | ✅ | ❌ | -| `/flows/{flowId}/label` | `HEAD`/`GET` | ❌ | ✅ | ❌ | ❌ | -| | `PUT` | ❌ | ❌ | ✅ | ❌ | -| | `DELETE` ⚠️ | ❌ | ❌ | ✅ | ❌ | -| `/flows/{flowId}/read_only` | `HEAD`/`GET` | ❌ | ✅ | ❌ | ❌ | -| | `PUT` | ❌ | ❌ | ✅ | ❌ | -| `/flows/{flowId}/flow_collection` | `HEAD`/`GET` | ❌ | ✅ | ❌ | ❌ | -| | `PUT` | ❌ | ❌ | ✅ | ❌ | -| | `DELETE` ⚠️ | ❌ | ❌ | ✅ | ❌ | -| `/flows/{flowId}/max_bit_rate` | `HEAD`/`GET` | ❌ | ✅ | ❌ | ❌ | -| | `PUT` | ❌ | ❌ | ✅ | ❌ | -| | `DELETE` ⚠️ | ❌ | ❌ | ✅ | ❌ | -| `/flows/{flowId}/avg_bit_rate` | `HEAD`/`GET` | ❌ | ✅ | ❌ | ❌ | -| | `PUT` | ❌ | ❌ | ✅ | ❌ | -| | `DELETE` ⚠️ | ❌ | ❌ | ✅ | ❌ | -| `/flows/{flowId}/segments` | `HEAD`/`GET` | ❌ | ✅ | ❌ | ❌ | -| | `POST` | ❌ | ❌ | ✅ | ❌ | -| | `DELETE` | ❌ | ❌ | ❌ | ✅ | -| `/flows/{flowId}/storage` | `POST` | ❌ | ❌ | ✅ | ❌ | -| `/objects/{objectId}` | `HEAD`/`GET` | ❌ | ✅ | ❌ | ❌ | -| `/flow-delete-requests` | `HEAD`/`GET` ⚠️ | ✅ | ❌ | ❌ | ❌ | -| `/flow-delete-requests/{request-id}` | `HEAD`/`GET` ⚠️ | ❌ | ❌ | ❌ | ✅ | +| `/service/webhooks` | `HEAD`/`GET` ⚠️ | ✅ | | | | +| | `POST` ⚠️ | ✅ | | | | +| `/sources` | `HEAD`/`GET` | | ✅ | | | +| `/sources/{sourceId}` | `HEAD`/`GET` | | ✅ | | | +| `/sources/{sourceId}/tags` | `HEAD`/`GET` | | ✅ | | | +| `/sources/{sourceId}/tags/{name}` | `HEAD`/`GET` | | ✅ | | | +| | `PUT` | | | ✅ | | +| | `DELETE` ⚠️ | | | ✅ | | +| `/sources/{sourceId}/description` | `HEAD`/`GET` | | ✅ | | | +| | `PUT` | | | ✅ | | +| | `DELETE` ⚠️ | | | ✅ | | +| `/sources/{sourceId}/label` | `HEAD`/`GET` | | ✅ | | | +| | `PUT` | | | ✅ | | +| | `DELETE` ⚠️ | | | ✅ | | +| `/flows` | `HEAD`/`GET` | | ✅ | | | +| `/flows/{flowId}` | `HEAD`/`GET` | | ✅ | | | +| | `PUT` | | | ✅ | | +| | `DELETE` | | | | ✅ | +| `/flows/{flowId}/tags` | `HEAD`/`GET` | | ✅ | | | +| `/flows/{flowId}/tags/{name}` | `HEAD`/`GET` | | ✅ | | | +| | `PUT` | | | ✅ | | +| | `DELETE` ⚠️ | | | ✅ | | +| `/flows/{flowId}/description` | `HEAD`/`GET` | | ✅ | | | +| | `PUT` | | | ✅ | | +| | `DELETE` ⚠️ | | | ✅ | | +| `/flows/{flowId}/label` | `HEAD`/`GET` | | ✅ | | | +| | `PUT` | | | ✅ | | +| | `DELETE` ⚠️ | | | ✅ | | +| `/flows/{flowId}/read_only` | `HEAD`/`GET` | | ✅ | | | +| | `PUT` | | | ✅ | | +| `/flows/{flowId}/flow_collection` | `HEAD`/`GET` | | ✅ | | | +| | `PUT` | | | ✅ | | +| | `DELETE` ⚠️ | | | ✅ | | +| `/flows/{flowId}/max_bit_rate` | `HEAD`/`GET` | | ✅ | | | +| | `PUT` | | | ✅ | | +| | `DELETE` ⚠️ | | | ✅ | | +| `/flows/{flowId}/avg_bit_rate` | `HEAD`/`GET` | | ✅ | | | +| | `PUT` | | | ✅ | | +| | `DELETE` ⚠️ | | | ✅ | | +| `/flows/{flowId}/segments` | `HEAD`/`GET` | | ✅ | | | +| | `POST` | | | ✅ | | +| | `DELETE` | | | | ✅ | +| `/flows/{flowId}/storage` | `POST` | | | ✅ | | +| `/objects/{objectId}` | `HEAD`/`GET` | | ✅ | | | +| `/flow-delete-requests` | `HEAD`/`GET` ⚠️ | ✅ | | | | +| `/flow-delete-requests/{request-id}` | `HEAD`/`GET` ⚠️ | | | | ✅ | Key for the listing: - ✅: Allow method with this OAuth scope -- ❌: Do not allow method with this OAuth scope. -Other claimed scopes may still allow this method - ⚠️: Method does not follow the basic mapping of `tams-api/read` to `HEAD`/`GET`, `tams-api/write` to `POST`/`PUT`, and `tams-api/delete` to `DELETE` Users may be assigned combinations of these roles for different purposes, for example: From b9845ae6e60d463c9d2f72a223db26666371d419 Mon Sep 17 00:00:00 2001 From: James Sandford Date: Wed, 2 Jul 2025 12:54:04 +0100 Subject: [PATCH 06/33] Update tag listing --- docs/appnotes/0003-tag-names.md | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/docs/appnotes/0003-tag-names.md b/docs/appnotes/0003-tag-names.md index f5b3c87a..8a34cc1b 100644 --- a/docs/appnotes/0003-tag-names.md +++ b/docs/appnotes/0003-tag-names.md @@ -195,14 +195,12 @@ The type is a `boolean`. It is used to indictate the Flow should be excluded from HLS manifest generation. Defaults to `false` if the tag is not set. -### authz_class.${classname} +### scope Status: **Experimental** Suggested as a way to build lightweight Attribute-based Access Control in [AppNote0016: Authorisation in TAMS workflows](./0016-authorisation-in-tams-workflows.md). -The tag would actually be created once for each "class" associated with a Flow: for example there could be an `authz_class.news = 1` and `authz_class.sport = 1`. -As a result, queries can be constructed on the presence of specific tags, which would not be possible if it were a comma-seperated list for example `authz_class = news, sport`. -The value has no particular meaning, but tags must have a value. +A comma seperated list of scopes providing permissions on the Flow. No known implementations yet. @@ -217,3 +215,12 @@ Used in the TAMS demonstration at NAB 2025. The type is a `boolean`. It is used to indictate the Source should be excluded from HLS manifest generation. Defaults to `false` if the tag is not set. + +### scope + +Status: **Experimental** + +Suggested as a way to build lightweight Attribute-based Access Control in [AppNote0016: Authorisation in TAMS workflows](./0016-authorisation-in-tams-workflows.md). +A comma seperated list of scopes providing permissions on the Source. + +No known implementations yet. From 6344f9e73f0126e6950594cf0d0260c153b180d7 Mon Sep 17 00:00:00 2001 From: James Sandford Date: Tue, 8 Jul 2025 14:04:40 +0100 Subject: [PATCH 07/33] Align coarse-grain logic with fine-grain --- .../0016-authorisation-in-tams-workflows.md | 88 +++++++++---------- 1 file changed, 44 insertions(+), 44 deletions(-) diff --git a/docs/appnotes/0016-authorisation-in-tams-workflows.md b/docs/appnotes/0016-authorisation-in-tams-workflows.md index 9cf1e85f..c4c4ce31 100644 --- a/docs/appnotes/0016-authorisation-in-tams-workflows.md +++ b/docs/appnotes/0016-authorisation-in-tams-workflows.md @@ -33,51 +33,51 @@ These are the recommended permissions (or "scopes" in OAuth 2.0): | | `POST` ⚠️ | ✅ | | | | | `/service/storage-backends` | `HEAD`/`GET` ⚠️ | ✅ | ✅ | ✅ | ✅ | | `/service/webhooks` | `HEAD`/`GET` ⚠️ | ✅ | | | | -| | `POST` ⚠️ | ✅ | | | | -| `/sources` | `HEAD`/`GET` | | ✅ | | | -| `/sources/{sourceId}` | `HEAD`/`GET` | | ✅ | | | -| `/sources/{sourceId}/tags` | `HEAD`/`GET` | | ✅ | | | -| `/sources/{sourceId}/tags/{name}` | `HEAD`/`GET` | | ✅ | | | -| | `PUT` | | | ✅ | | -| | `DELETE` ⚠️ | | | ✅ | | -| `/sources/{sourceId}/description` | `HEAD`/`GET` | | ✅ | | | -| | `PUT` | | | ✅ | | -| | `DELETE` ⚠️ | | | ✅ | | -| `/sources/{sourceId}/label` | `HEAD`/`GET` | | ✅ | | | -| | `PUT` | | | ✅ | | -| | `DELETE` ⚠️ | | | ✅ | | -| `/flows` | `HEAD`/`GET` | | ✅ | | | -| `/flows/{flowId}` | `HEAD`/`GET` | | ✅ | | | -| | `PUT` | | | ✅ | | -| | `DELETE` | | | | ✅ | -| `/flows/{flowId}/tags` | `HEAD`/`GET` | | ✅ | | | -| `/flows/{flowId}/tags/{name}` | `HEAD`/`GET` | | ✅ | | | -| | `PUT` | | | ✅ | | -| | `DELETE` ⚠️ | | | ✅ | | -| `/flows/{flowId}/description` | `HEAD`/`GET` | | ✅ | | | -| | `PUT` | | | ✅ | | -| | `DELETE` ⚠️ | | | ✅ | | -| `/flows/{flowId}/label` | `HEAD`/`GET` | | ✅ | | | -| | `PUT` | | | ✅ | | -| | `DELETE` ⚠️ | | | ✅ | | -| `/flows/{flowId}/read_only` | `HEAD`/`GET` | | ✅ | | | -| | `PUT` | | | ✅ | | -| `/flows/{flowId}/flow_collection` | `HEAD`/`GET` | | ✅ | | | -| | `PUT` | | | ✅ | | -| | `DELETE` ⚠️ | | | ✅ | | -| `/flows/{flowId}/max_bit_rate` | `HEAD`/`GET` | | ✅ | | | -| | `PUT` | | | ✅ | | -| | `DELETE` ⚠️ | | | ✅ | | -| `/flows/{flowId}/avg_bit_rate` | `HEAD`/`GET` | | ✅ | | | -| | `PUT` | | | ✅ | | -| | `DELETE` ⚠️ | | | ✅ | | -| `/flows/{flowId}/segments` | `HEAD`/`GET` | | ✅ | | | -| | `POST` | | | ✅ | | -| | `DELETE` | | | | ✅ | -| `/flows/{flowId}/storage` | `POST` | | | ✅ | | -| `/objects/{objectId}` | `HEAD`/`GET` | | ✅ | | | +| | `POST` | ✅ | | ✅ | | +| `/sources` | `HEAD`/`GET` | ✅ | ✅ | | | +| `/sources/{sourceId}` | `HEAD`/`GET` | ✅ | ✅ | | | +| `/sources/{sourceId}/tags` | `HEAD`/`GET` | ✅ | ✅ | | | +| `/sources/{sourceId}/tags/{name}` | `HEAD`/`GET` | ✅ | ✅ | | | +| | `PUT` | ✅ | | ✅ | | +| | `DELETE` ⚠️ | ✅ | | ✅ | | +| `/sources/{sourceId}/description` | `HEAD`/`GET` | ✅ | ✅ | | | +| | `PUT` | ✅ | | ✅ | | +| | `DELETE` ⚠️ | ✅ | | ✅ | | +| `/sources/{sourceId}/label` | `HEAD`/`GET` | ✅ | ✅ | | | +| | `PUT` | ✅ | | ✅ | | +| | `DELETE` ⚠️ | ✅ | | ✅ | | +| `/flows` | `HEAD`/`GET` | ✅ | ✅ | | | +| `/flows/{flowId}` | `HEAD`/`GET` | ✅ | ✅ | | | +| | `PUT` | ✅ | | ✅ | | +| | `DELETE` | ✅ | | | ✅ | +| `/flows/{flowId}/tags` | `HEAD`/`GET` | ✅ | ✅ | | | +| `/flows/{flowId}/tags/{name}` | `HEAD`/`GET` | ✅ | ✅ | | | +| | `PUT` | ✅ | | ✅ | | +| | `DELETE` ⚠️ | ✅ | | ✅ | | +| `/flows/{flowId}/description` | `HEAD`/`GET` | ✅ | ✅ | | | +| | `PUT` | ✅ | | ✅ | | +| | `DELETE` ⚠️ | ✅ | | ✅ | | +| `/flows/{flowId}/label` | `HEAD`/`GET` | ✅ | ✅ | | | +| | `PUT` | ✅ | | ✅ | | +| | `DELETE` ⚠️ | ✅ | | ✅ | | +| `/flows/{flowId}/read_only` | `HEAD`/`GET` | ✅ | ✅ | | | +| | `PUT` | ✅ | | ✅ | | +| `/flows/{flowId}/flow_collection` | `HEAD`/`GET` | ✅ | ✅ | | | +| | `PUT` | ✅ | | ✅ | | +| | `DELETE` ⚠️ | ✅ | | ✅ | | +| `/flows/{flowId}/max_bit_rate` | `HEAD`/`GET` | ✅ | ✅ | | | +| | `PUT` | ✅ | | ✅ | | +| | `DELETE` ⚠️ | ✅ | | ✅ | | +| `/flows/{flowId}/avg_bit_rate` | `HEAD`/`GET` | ✅ | ✅ | | | +| | `PUT` | ✅ | | ✅ | | +| | `DELETE` ⚠️ | ✅ | | ✅ | | +| `/flows/{flowId}/segments` | `HEAD`/`GET` | ✅ | ✅ | | | +| | `POST` | ✅ | | ✅ | | +| | `DELETE` | ✅ | | | ✅ | +| `/flows/{flowId}/storage` | `POST` | ✅ | | ✅ | | +| `/objects/{objectId}` | `HEAD`/`GET` | ✅ | ✅ | | | | `/flow-delete-requests` | `HEAD`/`GET` ⚠️ | ✅ | | | | -| `/flow-delete-requests/{request-id}` | `HEAD`/`GET` ⚠️ | | | | ✅ | +| `/flow-delete-requests/{request-id}` | `HEAD`/`GET` ⚠️ | ✅ | | | ✅ | Key for the listing: From c134c6d3b9dcefa60e6ecd1f2d73440095c7a3e1 Mon Sep 17 00:00:00 2001 From: James Sandford Date: Tue, 8 Jul 2025 14:05:23 +0100 Subject: [PATCH 08/33] Improve split of auth logic, implementation details, and use cases --- .../0016-authorisation-in-tams-workflows.md | 163 +++++++++++------- 1 file changed, 98 insertions(+), 65 deletions(-) diff --git a/docs/appnotes/0016-authorisation-in-tams-workflows.md b/docs/appnotes/0016-authorisation-in-tams-workflows.md index c4c4ce31..5a04cdef 100644 --- a/docs/appnotes/0016-authorisation-in-tams-workflows.md +++ b/docs/appnotes/0016-authorisation-in-tams-workflows.md @@ -115,6 +115,7 @@ A permissions system then defines policies that apply to those scopes. These scopes could use the following patterns: | Scope name regex | Meaning | Example | +| ---------------- | --------------------------------- | ------------- | | `^admin$` | Grants all permission to the user | `admin` | | `^\w+_read$` | Grants read access to the user | `news_read` | | `^\w+_write$` | Grants write access to the user | `news_write` | @@ -131,16 +132,6 @@ However in some cases it is necessary to share a particular Source (e.g. to work | Source News X | `news_read`, `news_write`, `news_delete`, `sport_read` | News have full access. Sport have read access only. | | Source News Y | `news_read`, `news_write`, `news_delete` | News have full access. Sport have no access. | -As another example, consider an organisation which makes all content readable to all by default to promote reuse of content. -All users are assigned a special `global_read` scope. -The `global_read` scope is automatically assigned to all content by default, via some means not specified here. - -| Resource | Scopes | Comments | -| -------------- | -------------------------------------------- | --------------------------------------------- | -| Source Sport A | `global_read`, `sport_write`, `sport_delete` | All have read access. Sport have full access. | -| Source Sport B | `global_read`, `sport_write`, `sport_delete` | All have read access. Sport have full access. | -| Source News X | `global_read`, `news_write`, `news_delete` | All have read access. News have full access. | -| Source News Y | `global_read`, `news_write`, `news_delete` | All have read access. News have full access. | ### Auth logic @@ -149,11 +140,6 @@ In order that implementations may have consistent expectations about which metho It is assumed that the `admin` scope grants permission to execute all methods on all endpoints. It is only explicitly called out in the listing below where `admin` is the only scope which grants permission. -Where requests are rejected, they should return as follows: - -- `404` if the request has no permissions on the endpoint -- `403` if the request has any permission on the endpoint, but not sufficient to complete the request - The listing below refers to requests having permissions, rather than users. This is to account for cases where users only "claim" a subset of their permissions for a given request. Note that in some circumstances, requests may have to claim more permissions than may initially be assumed. @@ -166,62 +152,104 @@ For example, where a Source collections may be filtered to only include Sources Implementers should consider the implications of hiding data. For example - hiding collection relationships may result in clients deciding to delete a resource which, unknowingly, is still referenced by another. -| Endpoint | Method | Auth logic | -| ------------------------------------ | ------------ | ------------------------------------------------------------------------------------------------- | -| `/` | `HEAD`/`GET` | Available to all | -| `/service` | `HEAD`/`GET` | Available to all | -| | `POST` | Execute if `admin` in claimed scopes. Otherwise reject. | -| `/service/storage-backends` | `HEAD`/`GET` | Available to all | -| `/service/webhooks` | `HEAD`/`GET` | Return if `admin` in claimed scopes. Otherwise reject. | -| | `POST` | Execute if `admin` in claimed scopes. Otherwise reject. | +| Endpoint | Method | Auth logic | +| ------------------------------------ | ------------ | --------------------------------------------------------------------------------------- | +| `/` | `HEAD`/`GET` | Available to all | +| `/service` | `HEAD`/`GET` | Available to all | +| | `POST` | Request must have admin permissions. Otherwise reject. | +| `/service/storage-backends` | `HEAD`/`GET` | Available to all | +| `/service/webhooks` | `HEAD`/`GET` | Restrict returned data by adding list of claimed scopes to `tag.scope.includes`. If the incoming request has `tag.scope.includes` set, the request must be processed with `tag.scope.includes` set to the intersection of the claimed scopes and the provided list in `tag.scope.includes`. | +| | `POST` | Request must have write permissions on the webhook being edited. If the request edits the `scope` tag of a webhook, the request must have the permissions being edited. i.e. If the request adds or removes delete permissions for any group, it must have delete permissions on the webhook. If the request includes Source or Flow filters, the request must have read permissions on all Source or Flow IDs. If an implementation is not capable dynamically assessing permissions of new Sources/Flows, it may reject requests which do not specify Source/Flow filters. Otherwise, reject. | | `/sources` | `HEAD`/`GET` | Restrict returned data by adding list of claimed scopes to `tag.scope.includes`. If the incoming request has `tag.scope.includes` set, the request must be processed with `tag.scope.includes` set to the intersection of the claimed scopes and the provided list in `tag.scope.includes`. | -| `/sources/{sourceId}` | `HEAD`/`GET` | Return result if any claimed read scope is in `/sources/{sourceId}/tags/scope`. Otherwise reject. | -| `/sources/{sourceId}/tags` | `HEAD`/`GET` | Return result if any claimed read scope is in `/sources/{sourceId}/tags/scope`. Otherwise reject. | -| `/sources/{sourceId}/tags/{name}` | `HEAD`/`GET` | Return result if any claimed read scope is in `/sources/{sourceId}/tags/scope`. Otherwise reject. | -| | `PUT` | Execute if any claimed write scope is in `/sources/{sourceId}/tags/scope`. If the request is to `/sources/{sourceId}/tags/scope` the request must have the permissions being edited. i.e. If the request adds or removes delete permissions for any group, it must have delete claim which is present in `/sources/{sourceId}/tags/scope`. Otherwise, reject. | -| | `DELETE` | Execute if any claimed write scope is in `/sources/{sourceId}/tags/scope`. If the request is to `/sources/{sourceId}/tags/scope` the request must have the permissions being edited. i.e. If the request adds or removes delete permissions for any group, it must have delete claim which is present in `/sources/{sourceId}/tags/scope`. Otherwise, reject. | -| `/sources/{sourceId}/description` | `HEAD`/`GET` | Return result if any claimed read scope is in `/sources/{sourceId}/tags/scope`. Otherwise reject. | -| | `PUT` | Execute if any claimed write scope is in `/sources/{sourceId}/tags/scope`. Otherwise, reject. | -| | `DELETE` | Execute if any claimed write scope is in `/sources/{sourceId}/tags/scope`. Otherwise, reject. | -| `/sources/{sourceId}/label` | `HEAD`/`GET` | Return result if any claimed read scope is in `/sources/{sourceId}/tags/scope`. Otherwise reject. | -| | `PUT` | Execute if any claimed write scope is in `/sources/{sourceId}/tags/scope`. Otherwise, reject. | -| | `DELETE` | Execute if any claimed write scope is in `/sources/{sourceId}/tags/scope`. Otherwise, reject. | +| `/sources/{sourceId}` | `HEAD`/`GET` | Request must have read permissions on {sourceId}. Otherwise reject. | +| `/sources/{sourceId}/tags` | `HEAD`/`GET` | Request must have read permissions on {sourceId}. Otherwise reject. | +| `/sources/{sourceId}/tags/{name}` | `HEAD`/`GET` | Request must have read permissions on {sourceId}. Otherwise reject. | +| | `PUT` | Request must have write permissions on {sourceId}. If the request is to `/sources/{sourceId}/tags/scope` the request must have the permissions being edited. i.e. If the request adds or removes delete permissions for any group, it must have delete permissions on {sourceId}. Otherwise, reject. | +| | `DELETE` | Request must have write permissions on {sourceId}. If the request is to `/sources/{sourceId}/tags/scope` the request must have the permissions being edited. i.e. If the request adds or removes delete permissions for any group, it must have delete permissions on {sourceId}. Otherwise, reject. | +| `/sources/{sourceId}/description` | `HEAD`/`GET` | Request must have read permissions on {sourceId}. Otherwise reject. | +| | `PUT` | Request must have write permissions on {sourceId}. Otherwise, reject. | +| | `DELETE` | Request must have write permissions on {sourceId}. Otherwise, reject. | +| `/sources/{sourceId}/label` | `HEAD`/`GET` | Request must have read permissions on {sourceId}. Otherwise reject. | +| | `PUT` | Request must have write permissions on {sourceId}. Otherwise, reject. | +| | `DELETE` | Request must have write permissions on {sourceId}. Otherwise, reject. | | `/flows` | `HEAD`/`GET` | Restrict returned data by adding list of claimed scopes to `tag.scope.includes`. If the incoming request has `tag.scope.includes` set, the request must be processed with `tag.scope.includes` set to the intersection of the claimed scopes and the provided list in `tag.scope.includes`. | -| `/flows/{flowId}` | `HEAD`/`GET` | Return result if any claimed read scope is in `/flows/{flowId}/tags/scope`. Otherwise reject. | -| | `PUT` | Execute if any claimed write scope is in `/sources/{sourceId}/tags/scope` for the Flow's Source ID, or the Source ID doesn't currently exist in this TAMS instance. Otherwise, reject. | -| | `DELETE` | Return result if any claimed delete scope is in `/flows/{flowId}/tags/scope`. Otherwise reject. | -| `/flows/{flowId}/tags` | `HEAD`/`GET` | Return result if any claimed read scope is in `/flows/{flowId}/tags/scope`. Otherwise reject. | -| `/flows/{flowId}/tags/{name}` | `HEAD`/`GET` | Return result if any claimed read scope is in `/flows/{flowId}/tags/scope`. Otherwise reject. | -| | `PUT` | Execute if any claimed write scope is in `/flows/{flowId}/tags/scope`. If the request is to `/flows/{flowId}/tags/scope` the request must have the permissions being edited. i.e. If the request adds or removes delete permissions for any group, it must have delete claim which is present in `/flows/{flowId}/tags/scope`. Otherwise, reject. | -| | `DELETE` | Execute if any claimed write scope is in `/flows/{flowId}/tags/scope`. If the request is to `/flows/{flowId}/tags/scope` the request must have the permissions being edited. i.e. If the request adds or removes delete permissions for any group, it must have delete claim which is present in `/flows/{flowId}/tags/scope`. Otherwise, reject. | -| `/flows/{flowId}/description` | `HEAD`/`GET` | Return result if any claimed read scope is in `/flows/{flowId}/tags/scope`. Otherwise reject. | -| | `PUT` | Execute if any claimed write scope is in `/flows/{flowId}/tags/scope`. Otherwise reject. | -| | `DELETE` | Execute if any claimed write scope is in `/flows/{flowId}/tags/scope`. Otherwise reject. | -| `/flows/{flowId}/label` | `HEAD`/`GET` | Return result if any claimed read scope is in `/flows/{flowId}/tags/scope`. Otherwise reject. | -| | `PUT` | Execute if any claimed write scope is in `/flows/{flowId}/tags/scope`. Otherwise reject. | -| | `DELETE` | Execute if any claimed write scope is in `/flows/{flowId}/tags/scope`. Otherwise reject. | -| `/flows/{flowId}/read_only` | `HEAD`/`GET` | Return result if any claimed read scope is in `/flows/{flowId}/tags/scope`. Otherwise reject. | -| | `PUT` | Execute if any claimed write scope is in `/flows/{flowId}/tags/scope`. Otherwise reject. | -| `/flows/{flowId}/flow_collection` | `HEAD`/`GET` | Return result if any claimed read scope is in `/flows/{flowId}/tags/scope`. Otherwise reject. | -| | `PUT` | Execute if any claimed write scope is in `/flows/{flowId}/tags/scope`. Otherwise reject. | -| | `DELETE` | Execute if any claimed write scope is in `/flows/{flowId}/tags/scope`. Otherwise reject. | -| `/flows/{flowId}/max_bit_rate` | `HEAD`/`GET` | Return result if any claimed read scope is in `/flows/{flowId}/tags/scope`. Otherwise reject. | -| | `PUT` | Execute if any claimed write scope is in `/flows/{flowId}/tags/scope`. Otherwise reject. | -| | `DELETE` | Execute if any claimed write scope is in `/flows/{flowId}/tags/scope`. Otherwise reject. | -| `/flows/{flowId}/avg_bit_rate` | `HEAD`/`GET` | Return result if any claimed read scope is in `/flows/{flowId}/tags/scope`. Otherwise reject. | -| | `PUT` | Execute if any claimed write scope is in `/flows/{flowId}/tags/scope`. Otherwise reject. | -| | `DELETE` | Execute if any claimed write scope is in `/flows/{flowId}/tags/scope`. Otherwise reject. | -| `/flows/{flowId}/segments` | `HEAD`/`GET` | Return result if any claimed read scope is in `/flows/{flowId}/tags/scope`. Otherwise reject. | -| | `POST` | Execute if any claimed write scope is in `/flows/{flowId}/tags/scope`, and any flows are listed in `referenced_by_flows` at `/objects/{objectId}` with `flow_tag.scope.includes` set to claimed read scopes for each Object ID being written. Otherwise reject. | -| | `DELETE` | Execute if any claimed write scope is in `/flows/{flowId}/tags/scope`. Otherwise reject. | -| `/flows/{flowId}/storage` | `POST` | Execute if any claimed write scope is in `/flows/{flowId}/tags/scope`. Otherwise reject. | +| `/flows/{flowId}` | `HEAD`/`GET` | Request must have read permissions on {flowID}. Otherwise reject. | +| | `PUT` | If {flowId} does not currently exist, request must have write permissions on the Flow's Source ID or the Source ID doesn't currently exist in this TAMS instance. If {flowId} already exists Request must have write permissions on {flowId}. If the request edits the `scope` tag, the request must have the permissions being edited. i.e. If the request adds or removes delete permissions for any group, it must have delete permissions on {flowId}. Otherwise, reject. | +| | `DELETE` | Request must have delete permissions on {flowId}. Otherwise reject. | +| `/flows/{flowId}/tags` | `HEAD`/`GET` | Request must have read permissions on {flowId}. Otherwise reject. | +| `/flows/{flowId}/tags/{name}` | `HEAD`/`GET` | Request must have read permissions on {flowId}. Otherwise reject. | +| | `PUT` | Request must have write permissions on {flowId}. If the request is to `/flows/{flowId}/tags/scope` the request must have the permissions being edited. i.e. If the request adds or removes delete permissions for any group, it must have delete permissions on {flowId}. Otherwise, reject. | +| | `DELETE` | Request must have write permissions on {flowId}. If the request is to `/flows/{flowId}/tags/scope` the request must have the permissions being edited. i.e. If the request adds or removes delete permissions for any group, it must have delete permissions on {flowId}. Otherwise, reject. | +| `/flows/{flowId}/description` | `HEAD`/`GET` | Request must have read permissions on {flowId}. Otherwise reject. | +| | `PUT` | Request must have write permissions on {flowId}. Otherwise reject. | +| | `DELETE` | Request must have write permissions on {flowId}. Otherwise reject. | +| `/flows/{flowId}/label` | `HEAD`/`GET` | Request must have read permissions on {flowId}. Otherwise reject. | +| | `PUT` | Request must have write permissions on {flowId}. Otherwise reject. | +| | `DELETE` | Request must have write permissions on {flowId}. Otherwise reject. | +| `/flows/{flowId}/read_only` | `HEAD`/`GET` | Request must have read permissions on {flowId}. Otherwise reject. | +| | `PUT` | Request must have write permissions on {flowId}. Otherwise reject. | +| `/flows/{flowId}/flow_collection` | `HEAD`/`GET` | Request must have read permissions on {flowId}. Otherwise reject. | +| | `PUT` | Request must have write permissions on {flowId}. Otherwise reject. | +| | `DELETE` | Request must have write permissions on {flowId}. Otherwise reject. | +| `/flows/{flowId}/max_bit_rate` | `HEAD`/`GET` | Request must have read permissions on {flowId}. Otherwise reject. | +| | `PUT` | Request must have write permissions on {flowId}. Otherwise reject. | +| | `DELETE` | Request must have write permissions on {flowId}. Otherwise reject. | +| `/flows/{flowId}/avg_bit_rate` | `HEAD`/`GET` | Request must have read permissions on {flowId}. Otherwise reject. | +| | `PUT` | Request must have write permissions on {flowId}. Otherwise reject. | +| | `DELETE` | Request must have write permissions on {flowId}. Otherwise reject. | +| `/flows/{flowId}/segments` | `HEAD`/`GET` | Request must have read permissions on {flowId}. Otherwise reject. | +| | `POST` | Request must have write permissions on {flowId}, and either this must be the first registration of the object (i.e. `/objects/{objectId}` returns 404) or `referenced_by_flows` at `/objects/{objectId}` must not be empty when `flow_tag.scope.includes` is set to claimed read scopes for each Object ID being written. Otherwise reject. | +| | `DELETE` | Request must have write permissions on {flowId}. Otherwise reject. | +| `/flows/{flowId}/storage` | `POST` | Request must have write permissions on {flowId}. Otherwise reject. | | `/objects/{objectId}` | `HEAD`/`GET` | Restrict returned data by adding list of claimed scopes to `flow_tag.scope.includes`. If the incoming request has `flow_tag.scope.includes` set, the request must be processed with `flow_tag.scope.includes` set to the intersection of the claimed scopes and the provided list in `flow_tag.scope.includes`. | -| `/flow-delete-requests` | `HEAD`/`GET` | Return if `admin` in claimed scopes. Otherwise reject. | -| `/flow-delete-requests/{request-id}` | `HEAD`/`GET` | Return result if any claimed delete scope is in `/flows/{flowId}/tags/scope` for the Delete Request's Flow ID. Otherwise reject. | +| `/flow-delete-requests` | `HEAD`/`GET` | Request must have admin permissions. Otherwise reject. | +| `/flow-delete-requests/{request-id}` | `HEAD`/`GET` | Request must have delete permissions on the Delete Request's Flow ID. Otherwise reject. | + +### Determining base permissions + +#### Flows + +Read, write, and delete permissions on individual flows may be determined via scopes listed in the `scope` tag on the flow. +This may be done via the `/flows/{flowId}/tags/scope` endpoint. + +#### Sources + +Read, write, and delete permissions on individual sources may be determined via scopes listed in the `scope` tag on the source. +This may be done via the `/sources/{sourceId}/tags/scope` endpoint. + +#### Webhooks + +Read, write, and delete permissions on individual webhooks may be determined via scopes listed in the `scope` tag on the webhook. +This may be done via the `/sources/{sourceId}/tags/scope` endpoint. + +### Handling rejected requests + +Where requests are rejected, they should return as follows: + +- `404` if the request has no permissions on the endpoint +- `403` if the request has any permission on the endpoint, but not sufficient to complete the request + +### Fine-grained authorisation and webhook events + +A basic implementation may enumerate Flows and Sources a user has access to when creating/updating the webhook. +This approach is strongly discouraged as permissions may change over time. +It is recommended that implementations asses permissions on a per-event basis. +Implementations may use `scope` tags in Flow/Source updated events to maintain a cache of Flow/Sources a webhook has read permissions for. +Implementations should regularly inspect flow tags to guard against missed events. +Implementations should regularly check the user's permissions in the auth system for changes. + +### Adding Flows to Sources + +New Sources inherit permissions from the first Flow which references them. +In order to prevent malicious actors adding maliciously crafted Flows to an existing Source, Flows using an existing Source ID must have write write permissions on the Source. +This may be an impediment to some workflows, such as where dual-redundant ingesters capture the same Source. +Or where different teams within a business re-ingest the same Source in a different format. +Some deployments may choose to accept this risk and allow broader re-use of Sources. +Implementations may either apply default scopes to Sources which will grant all users write permissions, or they may use more permissive auth logic. ### Implementation -To implement the model above, away to hold the scopes in TAMS is needed, along with a system to store the permissions and the authorisation logic that maps them to scopes. +To implement the model above, a way to hold the scopes in TAMS is needed, along with a system to store the permissions and the authorisation logic that maps them to scopes. For the latter, [Amazon Verified Permissions](https://aws.amazon.com/verified-permissions/) and [Permify](https://github.com/Permify/permify) both serve as permissions management tools. They allow authorisation decisions to be made by taking a set of policies defined in some domain-specific language, along with the attributes of the user (group membership) and resource (Source/Flow scope), and computing whether to allow the request. @@ -264,6 +292,11 @@ It may, however, be achieved by creating a new flow with the relevant permission Caution should be taken where the boundary timestamps land partway through an Object. Where the material around the boundaries is sensitive, new trimmed Objects should be created at the boundaries that only include the media used in the new Flow. +### Global read access + +Some organisations/implementations may choose to provide read access to all Sources and Flows to promote content re-use, and reduce the writing of duplicate content to the store. +Implementations may provide this feature by either adding default groups to Sources and Flows that provide appropriate read access to users, or by using more permissive auth logic. + ## Future Work The model described above allows for more use cases than coarse-grained RBAC: especially use cases where multiple tenants share a single store. From 8426e895df86b5a40d4ebb242cc56b13b04506e6 Mon Sep 17 00:00:00 2001 From: James Sandford Date: Tue, 8 Jul 2025 14:05:46 +0100 Subject: [PATCH 09/33] Be explicit on returning 404 for objects which are not yet in use --- api/TimeAddressableMediaStore.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api/TimeAddressableMediaStore.yaml b/api/TimeAddressableMediaStore.yaml index cc5904e6..38f6c6ef 100644 --- a/api/TimeAddressableMediaStore.yaml +++ b/api/TimeAddressableMediaStore.yaml @@ -2177,7 +2177,7 @@ paths: "400": $ref: '#/components/responses/trait_resource_info_head_400' "404": - description: The requested Media Object does not exist. + description: The requested Media Object does not exist. 404 MUST be returned if the ID has been assigned via the storage endpoint, but not yet registered against a Flow Segment. get: summary: Media Object Information description: | From 244418c57137e65b90947e8f4f4900d88f765d4b Mon Sep 17 00:00:00 2001 From: James Sandford Date: Tue, 8 Jul 2025 14:37:03 +0100 Subject: [PATCH 10/33] Rename `scopes` to `auth_classes` --- .../0016-authorisation-in-tams-workflows.md | 91 +++++++++---------- 1 file changed, 41 insertions(+), 50 deletions(-) diff --git a/docs/appnotes/0016-authorisation-in-tams-workflows.md b/docs/appnotes/0016-authorisation-in-tams-workflows.md index 5a04cdef..347d5b92 100644 --- a/docs/appnotes/0016-authorisation-in-tams-workflows.md +++ b/docs/appnotes/0016-authorisation-in-tams-workflows.md @@ -107,47 +107,38 @@ This approach should be considered experimental at this point. Due its experimental nature, this approach makes use of the tags feature in TAMS. Future iterations of these proposals may elevate ABAC attributes to a specific field in the core specification. -### Scopes +### Scopes and Auth Classes -In practical TAMS solutions, ABAC could look like defining a "scope" tag which maps to OAuth scopes. -A permissions system then defines policies that apply to those scopes. - -These scopes could use the following patterns: - -| Scope name regex | Meaning | Example | -| ---------------- | --------------------------------- | ------------- | -| `^admin$` | Grants all permission to the user | `admin` | -| `^\w+_read$` | Grants read access to the user | `news_read` | -| `^\w+_write$` | Grants write access to the user | `news_write` | -| `^\w+_delete$` | Grants delete access to the user | `news_delete` | +In practical TAMS solutions, ABAC could look like defining an `auth_classes` tag. +A permissions system then defines policies that evaluates permissions based on to those `auth_classes` and a request's claimed OAuth scopes. For example, consider a store shared by multiple teams from the News and Sport production teams of an organisation. Each team have the ability to read and write their own content, and no access to the other team's content. However in some cases it is necessary to share a particular Source (e.g. to work on a shared story) to the other team. -| Resource | Scopes | Comments | -| -------------- | ------------------------------------------------------ | --------------------------------------------------- | -| Source Sport A | `sport_read`, `sport_write`, `sport_delete` | Sport have full access. News have no access. | -| Source Sport B | `sport_read`, `sport_write`, `sport_delete` | Sport have full access. News have no access. | -| Source News X | `news_read`, `news_write`, `news_delete`, `sport_read` | News have full access. Sport have read access only. | -| Source News Y | `news_read`, `news_write`, `news_delete` | News have full access. Sport have no access. | +| Resource | Auth classes | Comments | +| -------------- | ------------------ | --------------------------------------------------- | +| Source Sport A | `sport` | Sport have full access. News have no access. | +| Source Sport B | `sport` | Sport have full access. News have no access. | +| Source News X | `news`, `sport_ro` | News have full access. Sport have read access only. | +| Source News Y | `news` | News have full access. Sport have no access. | ### Auth logic In order that implementations may have consistent expectations about which methods they may access, this section provides recommended auth logic for methods. -It is assumed that the `admin` scope grants permission to execute all methods on all endpoints. -It is only explicitly called out in the listing below where `admin` is the only scope which grants permission. +It is assumed that admins have permission to execute all methods on all endpoints. +It is only explicitly called out in the listing below where admins are the only users granted permissions. The listing below refers to requests having permissions, rather than users. This is to account for cases where users only "claim" a subset of their permissions for a given request. Note that in some circumstances, requests may have to claim more permissions than may initially be assumed. -For example - when editing scopes on a Source/Flow, requests must claim both write permissions and the permission they are changing. +For example - when editing the `auth_classes` tag on a Source/Flow, requests must claim both write permissions and the permission they are changing. i.e. If the request adds or removes delete permissions for any group, it must have valid delete permissions itself. This is to prevent permission escalation attacks such as a user with write permissions adding delete permissions to themselves. -Implementations may choose to additionally filter data based on the claimed scopes of a request. +Implementations may choose to additionally filter data based on the permissions of a request. For example, where a Source collections may be filtered to only include Sources the request has read/write/delete permissions on. Implementers should consider the implications of hiding data. For example - hiding collection relationships may result in clients deciding to delete a resource which, unknowingly, is still referenced by another. @@ -158,28 +149,28 @@ For example - hiding collection relationships may result in clients deciding to | `/service` | `HEAD`/`GET` | Available to all | | | `POST` | Request must have admin permissions. Otherwise reject. | | `/service/storage-backends` | `HEAD`/`GET` | Available to all | -| `/service/webhooks` | `HEAD`/`GET` | Restrict returned data by adding list of claimed scopes to `tag.scope.includes`. If the incoming request has `tag.scope.includes` set, the request must be processed with `tag.scope.includes` set to the intersection of the claimed scopes and the provided list in `tag.scope.includes`. | -| | `POST` | Request must have write permissions on the webhook being edited. If the request edits the `scope` tag of a webhook, the request must have the permissions being edited. i.e. If the request adds or removes delete permissions for any group, it must have delete permissions on the webhook. If the request includes Source or Flow filters, the request must have read permissions on all Source or Flow IDs. If an implementation is not capable dynamically assessing permissions of new Sources/Flows, it may reject requests which do not specify Source/Flow filters. Otherwise, reject. | -| `/sources` | `HEAD`/`GET` | Restrict returned data by adding list of claimed scopes to `tag.scope.includes`. If the incoming request has `tag.scope.includes` set, the request must be processed with `tag.scope.includes` set to the intersection of the claimed scopes and the provided list in `tag.scope.includes`. | +| `/service/webhooks` | `HEAD`/`GET` | Restrict returned data by adding list of claimed auth_classes to `tag.auth_classes.includes`. If the incoming request has `tag.auth_classes.includes` set, the request must be processed with `tag.auth_classes.includes` set to the intersection of the claimed auth_classes and the provided list in `tag.auth_classes.includes`. | +| | `POST` | Request must have write permissions on the webhook being edited. If the request edits the `auth_classes` tag of a webhook, the request must have the permissions being edited. i.e. If the request adds or removes delete permissions for any group, it must have delete permissions on the webhook. If the request includes Source or Flow filters, the request must have read permissions on all Source or Flow IDs. If an implementation is not capable dynamically assessing permissions of new Sources/Flows, it may reject requests which do not specify Source/Flow filters. Otherwise, reject. | +| `/sources` | `HEAD`/`GET` | Restrict returned data by adding list of claimed auth_classes to `tag.auth_classes.includes`. If the incoming request has `tag.auth_classes.includes` set, the request must be processed with `tag.auth_classes.includes` set to the intersection of the claimed auth_classes and the provided list in `tag.auth_classes.includes`. | | `/sources/{sourceId}` | `HEAD`/`GET` | Request must have read permissions on {sourceId}. Otherwise reject. | | `/sources/{sourceId}/tags` | `HEAD`/`GET` | Request must have read permissions on {sourceId}. Otherwise reject. | | `/sources/{sourceId}/tags/{name}` | `HEAD`/`GET` | Request must have read permissions on {sourceId}. Otherwise reject. | -| | `PUT` | Request must have write permissions on {sourceId}. If the request is to `/sources/{sourceId}/tags/scope` the request must have the permissions being edited. i.e. If the request adds or removes delete permissions for any group, it must have delete permissions on {sourceId}. Otherwise, reject. | -| | `DELETE` | Request must have write permissions on {sourceId}. If the request is to `/sources/{sourceId}/tags/scope` the request must have the permissions being edited. i.e. If the request adds or removes delete permissions for any group, it must have delete permissions on {sourceId}. Otherwise, reject. | +| | `PUT` | Request must have write permissions on {sourceId}. If the request is to `/sources/{sourceId}/tags/auth_classes` the request must have the permissions being edited. i.e. If the request adds or removes delete permissions for any group, it must have delete permissions on {sourceId}. Otherwise, reject. | +| | `DELETE` | Request must have write permissions on {sourceId}. If the request is to `/sources/{sourceId}/tags/auth_classes` the request must have the permissions being edited. i.e. If the request adds or removes delete permissions for any group, it must have delete permissions on {sourceId}. Otherwise, reject. | | `/sources/{sourceId}/description` | `HEAD`/`GET` | Request must have read permissions on {sourceId}. Otherwise reject. | | | `PUT` | Request must have write permissions on {sourceId}. Otherwise, reject. | | | `DELETE` | Request must have write permissions on {sourceId}. Otherwise, reject. | | `/sources/{sourceId}/label` | `HEAD`/`GET` | Request must have read permissions on {sourceId}. Otherwise reject. | | | `PUT` | Request must have write permissions on {sourceId}. Otherwise, reject. | | | `DELETE` | Request must have write permissions on {sourceId}. Otherwise, reject. | -| `/flows` | `HEAD`/`GET` | Restrict returned data by adding list of claimed scopes to `tag.scope.includes`. If the incoming request has `tag.scope.includes` set, the request must be processed with `tag.scope.includes` set to the intersection of the claimed scopes and the provided list in `tag.scope.includes`. | +| `/flows` | `HEAD`/`GET` | Restrict returned data by adding list of claimed auth_classes to `tag.auth_classes.includes`. If the incoming request has `tag.auth_classes.includes` set, the request must be processed with `tag.auth_classes.includes` set to the intersection of the claimed auth_classes and the provided list in `tag.auth_classes.includes`. | | `/flows/{flowId}` | `HEAD`/`GET` | Request must have read permissions on {flowID}. Otherwise reject. | -| | `PUT` | If {flowId} does not currently exist, request must have write permissions on the Flow's Source ID or the Source ID doesn't currently exist in this TAMS instance. If {flowId} already exists Request must have write permissions on {flowId}. If the request edits the `scope` tag, the request must have the permissions being edited. i.e. If the request adds or removes delete permissions for any group, it must have delete permissions on {flowId}. Otherwise, reject. | +| | `PUT` | If {flowId} does not currently exist, request must have write permissions on the Flow's Source ID or the Source ID doesn't currently exist in this TAMS instance. If {flowId} already exists Request must have write permissions on {flowId}. If the request edits the `auth_classes` tag, the request must have the permissions being edited. i.e. If the request adds or removes delete permissions for any group, it must have delete permissions on {flowId}. Otherwise, reject. | | | `DELETE` | Request must have delete permissions on {flowId}. Otherwise reject. | | `/flows/{flowId}/tags` | `HEAD`/`GET` | Request must have read permissions on {flowId}. Otherwise reject. | | `/flows/{flowId}/tags/{name}` | `HEAD`/`GET` | Request must have read permissions on {flowId}. Otherwise reject. | -| | `PUT` | Request must have write permissions on {flowId}. If the request is to `/flows/{flowId}/tags/scope` the request must have the permissions being edited. i.e. If the request adds or removes delete permissions for any group, it must have delete permissions on {flowId}. Otherwise, reject. | -| | `DELETE` | Request must have write permissions on {flowId}. If the request is to `/flows/{flowId}/tags/scope` the request must have the permissions being edited. i.e. If the request adds or removes delete permissions for any group, it must have delete permissions on {flowId}. Otherwise, reject. | +| | `PUT` | Request must have write permissions on {flowId}. If the request is to `/flows/{flowId}/tags/auth_classes` the request must have the permissions being edited. i.e. If the request adds or removes delete permissions for any group, it must have delete permissions on {flowId}. Otherwise, reject. | +| | `DELETE` | Request must have write permissions on {flowId}. If the request is to `/flows/{flowId}/tags/auth_classes` the request must have the permissions being edited. i.e. If the request adds or removes delete permissions for any group, it must have delete permissions on {flowId}. Otherwise, reject. | | `/flows/{flowId}/description` | `HEAD`/`GET` | Request must have read permissions on {flowId}. Otherwise reject. | | | `PUT` | Request must have write permissions on {flowId}. Otherwise reject. | | | `DELETE` | Request must have write permissions on {flowId}. Otherwise reject. | @@ -198,10 +189,10 @@ For example - hiding collection relationships may result in clients deciding to | | `PUT` | Request must have write permissions on {flowId}. Otherwise reject. | | | `DELETE` | Request must have write permissions on {flowId}. Otherwise reject. | | `/flows/{flowId}/segments` | `HEAD`/`GET` | Request must have read permissions on {flowId}. Otherwise reject. | -| | `POST` | Request must have write permissions on {flowId}, and either this must be the first registration of the object (i.e. `/objects/{objectId}` returns 404) or `referenced_by_flows` at `/objects/{objectId}` must not be empty when `flow_tag.scope.includes` is set to claimed read scopes for each Object ID being written. Otherwise reject. | +| | `POST` | Request must have write permissions on {flowId}, and either this must be the first registration of the object (i.e. `/objects/{objectId}` returns 404) or `referenced_by_flows` at `/objects/{objectId}` must not be empty when `flow_tag.auth_classes.includes` is set to claimed read auth_classes for each Object ID being written. Otherwise reject. | | | `DELETE` | Request must have write permissions on {flowId}. Otherwise reject. | | `/flows/{flowId}/storage` | `POST` | Request must have write permissions on {flowId}. Otherwise reject. | -| `/objects/{objectId}` | `HEAD`/`GET` | Restrict returned data by adding list of claimed scopes to `flow_tag.scope.includes`. If the incoming request has `flow_tag.scope.includes` set, the request must be processed with `flow_tag.scope.includes` set to the intersection of the claimed scopes and the provided list in `flow_tag.scope.includes`. | +| `/objects/{objectId}` | `HEAD`/`GET` | Restrict returned data by adding list of claimed auth_classes to `flow_tag.auth_classes.includes`. If the incoming request has `flow_tag.auth_classes.includes` set, the request must be processed with `flow_tag.auth_classes.includes` set to the intersection of the claimed auth_classes and the provided list in `flow_tag.auth_classes.includes`. | | `/flow-delete-requests` | `HEAD`/`GET` | Request must have admin permissions. Otherwise reject. | | `/flow-delete-requests/{request-id}` | `HEAD`/`GET` | Request must have delete permissions on the Delete Request's Flow ID. Otherwise reject. | @@ -209,18 +200,18 @@ For example - hiding collection relationships may result in clients deciding to #### Flows -Read, write, and delete permissions on individual flows may be determined via scopes listed in the `scope` tag on the flow. -This may be done via the `/flows/{flowId}/tags/scope` endpoint. +Read, write, and delete permissions on individual flows may be determined via auth_classes listed in the `auth_classes` tag on the flow. +This may be done via the `/flows/{flowId}/tags/auth_classes` endpoint. #### Sources -Read, write, and delete permissions on individual sources may be determined via scopes listed in the `scope` tag on the source. -This may be done via the `/sources/{sourceId}/tags/scope` endpoint. +Read, write, and delete permissions on individual sources may be determined via auth_classes listed in the `auth_classes` tag on the source. +This may be done via the `/sources/{sourceId}/tags/auth_classes` endpoint. #### Webhooks -Read, write, and delete permissions on individual webhooks may be determined via scopes listed in the `scope` tag on the webhook. -This may be done via the `/sources/{sourceId}/tags/scope` endpoint. +Read, write, and delete permissions on individual webhooks may be determined via auth_classes listed in the `auth_classes` tag on the webhook. +This may be done via the `/sources/{sourceId}/tags/auth_classes` endpoint. ### Handling rejected requests @@ -234,7 +225,7 @@ Where requests are rejected, they should return as follows: A basic implementation may enumerate Flows and Sources a user has access to when creating/updating the webhook. This approach is strongly discouraged as permissions may change over time. It is recommended that implementations asses permissions on a per-event basis. -Implementations may use `scope` tags in Flow/Source updated events to maintain a cache of Flow/Sources a webhook has read permissions for. +Implementations may use `auth_classes` tags in Flow/Source updated events to maintain a cache of Flow/Sources a webhook has read permissions for. Implementations should regularly inspect flow tags to guard against missed events. Implementations should regularly check the user's permissions in the auth system for changes. @@ -245,32 +236,32 @@ In order to prevent malicious actors adding maliciously crafted Flows to an exis This may be an impediment to some workflows, such as where dual-redundant ingesters capture the same Source. Or where different teams within a business re-ingest the same Source in a different format. Some deployments may choose to accept this risk and allow broader re-use of Sources. -Implementations may either apply default scopes to Sources which will grant all users write permissions, or they may use more permissive auth logic. +Implementations may either apply default auth_classes to Sources which will grant all users write permissions, or they may use more permissive auth logic. ### Implementation -To implement the model above, a way to hold the scopes in TAMS is needed, along with a system to store the permissions and the authorisation logic that maps them to scopes. +To implement the model above, a way to hold the auth_classes in TAMS is needed, along with a system to store the permissions and the authorisation logic that maps them to auth_classes. For the latter, [Amazon Verified Permissions](https://aws.amazon.com/verified-permissions/) and [Permify](https://github.com/Permify/permify) both serve as permissions management tools. -They allow authorisation decisions to be made by taking a set of policies defined in some domain-specific language, along with the attributes of the user (group membership) and resource (Source/Flow scope), and computing whether to allow the request. +They allow authorisation decisions to be made by taking a set of policies defined in some domain-specific language, along with the attributes of the user (group membership) and resource (Source/Flow auth_classes), and computing whether to allow the request. This decision process is intended to be run inline for each request, for example at an authenticating proxy placed in front of the API server. -For storing scopes an initial proof-of-concept could be built, as described above, using Source and Flow tags. -A "special" `scope` tag would store a comma-separated list of scopes assigned to a Flow or Source +For storing auth_classes an initial proof-of-concept could be built, as described above, using Source and Flow tags. +A "special" `auth_classes` tag would store a comma-separated list of auth_classes assigned to a Flow or Source The authenticating proxy would need to take steps to prevent unauthorised modification of this special tag, as described above. -In addition, it should be possible to set a scope on a multi-essence Source or Flow, and apply that permissions downwards to all the Sources or Flows it collects. -Similarly, scopes should be set by default on a Source (and apply to all Flows), but be settable on individual Flows as well for additional flexibility. +In addition, it should be possible to set auth_classes on a multi-essence Source or Flow, and apply that permissions downwards to all the Sources or Flows it collects. +Similarly, auth_classes should be set by default on a Source (and apply to all Flows), but be settable on individual Flows as well for additional flexibility. To avoid a complex traversal of potentially a large hierarchy (and to simplify the listing endpoints), it may be useful to denormalise the tag on write, writing it to all the Sources and Flows it would affect as well. As a result, the process of authorising a request is: -1. Read the list of scopes assigned to the resource +1. Read the list of auth_classes assigned to the resource 2. Read the user's claimed scopes from their provided token 3. Request a decision from the permissions system based on those data -4. (Write requests only): Check whether the request would modify the special `scope` tag, and confirm the user has permission to make that modification +4. (Write requests only): Check whether the request would modify the special `auth_classes` tag, and confirm the user has permission to make that modification 5. (Flow segment write requests only): Check if the object already exists in the store using the `/objects` endpoint, and if it does, confirm the user would have access to read it -6. (Write requests only): Propagate any changes to the `scope` tag to Flows and Sources collected by this one +6. (Write requests only): Propagate any changes to the `auth_classes` tag to Flows and Sources collected by this one ## Where to Enforce Authorisation From 9744aad30dcecec5049a5a2928a504926dace7e8 Mon Sep 17 00:00:00 2001 From: James Sandford Date: Tue, 8 Jul 2025 14:41:49 +0100 Subject: [PATCH 11/33] Rename `scope` to `auth_classes` in tag listing --- docs/appnotes/0003-tag-names.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/appnotes/0003-tag-names.md b/docs/appnotes/0003-tag-names.md index 8a34cc1b..0beaa073 100644 --- a/docs/appnotes/0003-tag-names.md +++ b/docs/appnotes/0003-tag-names.md @@ -195,12 +195,12 @@ The type is a `boolean`. It is used to indictate the Flow should be excluded from HLS manifest generation. Defaults to `false` if the tag is not set. -### scope +### auth_classes Status: **Experimental** Suggested as a way to build lightweight Attribute-based Access Control in [AppNote0016: Authorisation in TAMS workflows](./0016-authorisation-in-tams-workflows.md). -A comma seperated list of scopes providing permissions on the Flow. +A comma seperated list of auth classes used to derive permissions on the Flow. No known implementations yet. @@ -216,11 +216,11 @@ The type is a `boolean`. It is used to indictate the Source should be excluded from HLS manifest generation. Defaults to `false` if the tag is not set. -### scope +### auth_classes Status: **Experimental** Suggested as a way to build lightweight Attribute-based Access Control in [AppNote0016: Authorisation in TAMS workflows](./0016-authorisation-in-tams-workflows.md). -A comma seperated list of scopes providing permissions on the Source. +A comma seperated list of auth classes used to derive permissions on the Source. No known implementations yet. From c3ab6a90bd6a4f8c5a71d43bc28cceb86f5cbe79 Mon Sep 17 00:00:00 2001 From: James Sandford Date: Tue, 8 Jul 2025 15:10:51 +0100 Subject: [PATCH 12/33] Add support for lists as tag values sem-ver: feature --- api/TimeAddressableMediaStore.yaml | 28 ++++++++++++++++++++-------- api/schemas/tags.json | 14 ++++++++++++-- 2 files changed, 32 insertions(+), 10 deletions(-) diff --git a/api/TimeAddressableMediaStore.yaml b/api/TimeAddressableMediaStore.yaml index 38f6c6ef..fb25b733 100644 --- a/api/TimeAddressableMediaStore.yaml +++ b/api/TimeAddressableMediaStore.yaml @@ -370,8 +370,11 @@ paths: - name: tag.{name} in: query description: | - Filter on Sources that have a tag named {name} and with the given value. - {name} and the value MUST be URL encoded where special characters are present. + Filter on Sources that have a tag named {name} with a value in the given comma-seperated list of values. + The {name} and the value MUST be URL encoded where special characters are present. + Where the tag's value is a string, at least one of the given values will match. + Where the tag's value is an array, at least one value in the array will match at least one of the given values. + Partial string matches of the values are not valid. schema: type: string - name: tag_exists.{name} @@ -428,8 +431,11 @@ paths: - name: tag.{name} in: query description: | - Filter on Sources that have a tag named {name} and with the given value. - {name} and the value MUST be URL encoded where special characters are present. + Filter on Sources that have a tag named {name} with a value in the given comma-seperated list of values. + The {name} and the value MUST be URL encoded where special characters are present. + Where the tag's value is a string, at least one of the given values will match. + Where the tag's value is an array, at least one value in the array will match at least one of the given values. + Partial string matches of the values are not valid. schema: type: string - name: tag_exists.{name} @@ -814,8 +820,11 @@ paths: - name: tag.{name} in: query description: | - Filter on Flows that have a tag named {name} and with the given value. - {name} and the value MUST be URL encoded where special characters are present. + Filter on flows that have a tag named {name} with a value in the given comma-seperated list of values. + The {name} and the value MUST be URL encoded where special characters are present. + Where the tag's value is a string, at least one of the given values will match. + Where the tag's value is an array, at least one value in the array will match at least one of the given values. + Partial string matches of the values are not valid. schema: type: string - name: tag_exists.{name} @@ -898,8 +907,11 @@ paths: - name: tag.{name} in: query description: | - Filter on Flows that have a tag named {name} and with the given value. - {name} and the value MUST be URL encoded where special characters are present. + Filter on flows that have a tag named {name} with a value in the given comma-seperated list of values. + The {name} and the value MUST be URL encoded where special characters are present. + Where the tag's value is a string, at least one of the given values will match. + Where the tag's value is an array, at least one value in the array will match at least one of the given values. + Partial string matches of the values are not valid. schema: type: string - name: tag_exists.{name} diff --git a/api/schemas/tags.json b/api/schemas/tags.json index fec402b5..3276e09d 100644 --- a/api/schemas/tags.json +++ b/api/schemas/tags.json @@ -1,8 +1,18 @@ { "title": "Tags", - "description": "Key value is a freeform string.", + "description": "Key is a freeform string. Value is a freeform string, or an array of freeform strings.", "type": "object", "additionalProperties": { - "type": "string" + "anyOf": [ + { + "type": "string" + }, + { + "type": "array", + "items": { + "type": "string" + } + } + ] } } \ No newline at end of file From a8c2d2e731293e92b01fe2805f7b401fcfb8f7d5 Mon Sep 17 00:00:00 2001 From: James Sandford Date: Tue, 8 Jul 2025 15:18:15 +0100 Subject: [PATCH 13/33] Add flow tag filters to objects endpoint --- api/TimeAddressableMediaStore.yaml | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/api/TimeAddressableMediaStore.yaml b/api/TimeAddressableMediaStore.yaml index fb25b733..d3a60502 100644 --- a/api/TimeAddressableMediaStore.yaml +++ b/api/TimeAddressableMediaStore.yaml @@ -2164,6 +2164,18 @@ paths: Where multiple filter query parameters are provided, the returned `get_urls` will match all filters. schema: type: boolean + - name: flow_tag.{name} + in: query + description: | + Filter `referenced_by_flows` on tag values. This option is the same as the `tag.{name}` query parameter on the `/flows/` API endpoint. + schema: + type: string + - name: flow_tag_exists.{name} + in: query + description: | + Filter `referenced_by_flows` on tag names. This option is the same as the `tag_exists.{name}` query parameter on the `/flows/` API endpoint. + schema: + type: boolean - $ref: '#/components/parameters/trait_resource_paged_key' - $ref: '#/components/parameters/trait_paged_limit' responses: @@ -2249,6 +2261,18 @@ paths: Where multiple filter query parameters are provided, the returned `get_urls` will match all filters. schema: type: boolean + - name: flow_tag.{name} + in: query + description: | + Filter `referenced_by_flows` on tag values. This option is the same as the `tag.{name}` query parameter on the `/flows/` API endpoint. + schema: + type: string + - name: flow_tag_exists.{name} + in: query + description: | + Filter `referenced_by_flows` on tag names. This option is the same as the `tag_exists.{name}` query parameter on the `/flows/` API endpoint. + schema: + type: boolean - $ref: '#/components/parameters/trait_resource_paged_key' - $ref: '#/components/parameters/trait_paged_limit' responses: From e7eef52ad902635223c8dd260c47c97b50f53c9d Mon Sep 17 00:00:00 2001 From: James Sandford Date: Tue, 8 Jul 2025 15:21:24 +0100 Subject: [PATCH 14/33] Fix linting --- docs/appnotes/0016-authorisation-in-tams-workflows.md | 1 - 1 file changed, 1 deletion(-) diff --git a/docs/appnotes/0016-authorisation-in-tams-workflows.md b/docs/appnotes/0016-authorisation-in-tams-workflows.md index 347d5b92..08602656 100644 --- a/docs/appnotes/0016-authorisation-in-tams-workflows.md +++ b/docs/appnotes/0016-authorisation-in-tams-workflows.md @@ -123,7 +123,6 @@ However in some cases it is necessary to share a particular Source (e.g. to work | Source News X | `news`, `sport_ro` | News have full access. Sport have read access only. | | Source News Y | `news` | News have full access. Sport have no access. | - ### Auth logic In order that implementations may have consistent expectations about which methods they may access, this section provides recommended auth logic for methods. From db82fcbd9438d6a80f0a4fd6428f59db1807302d Mon Sep 17 00:00:00 2001 From: James Sandford Date: Tue, 8 Jul 2025 15:42:09 +0100 Subject: [PATCH 15/33] Add tags to webhooks --- api/TimeAddressableMediaStore.yaml | 42 ++++++++++++++++++++++++++++++ api/schemas/webhook.json | 3 +++ 2 files changed, 45 insertions(+) diff --git a/api/TimeAddressableMediaStore.yaml b/api/TimeAddressableMediaStore.yaml index d3a60502..51b72345 100644 --- a/api/TimeAddressableMediaStore.yaml +++ b/api/TimeAddressableMediaStore.yaml @@ -154,6 +154,27 @@ paths: tags: - Webhooks parameters: + - name: tag.{name} + in: query + description: | + Filter on webhooks that have a tag named {name} with a value in the given comma-seperated list of values. + The {name} could contain escaped characters to allow it to be used in a + URL. + Where the tag's value is a string, at least one of the given values will match. + Where the tag's value is an array, at least one value in the array will match at least one of the given values. + Partial string matches of the values are not valid. + schema: + type: string + - name: tag_exists.{name} + in: query + description: | + Filter on webhooks that have a tag named {name} regardless of value. The + {name} could contain escaped characters to allow it to be used in a + URL. If set to true then the presence of the tag is filtered for. If set + to false then its absence is. If left out then no filtering on tag presence + is performed. + schema: + type: boolean - $ref: '#/components/parameters/trait_resource_paged_key' - $ref: '#/components/parameters/trait_paged_limit' responses: @@ -188,6 +209,27 @@ paths: tags: - Webhooks parameters: + - name: tag.{name} + in: query + description: | + Filter on webhooks that have a tag named {name} with a value in the given comma-seperated list of values. + The {name} could contain escaped characters to allow it to be used in a + URL. + Where the tag's value is a string, at least one of the given values will match. + Where the tag's value is an array, at least one value in the array will match at least one of the given values. + Partial string matches of the values are not valid. + schema: + type: string + - name: tag_exists.{name} + in: query + description: | + Filter on webhooks that have a tag named {name} regardless of value. The + {name} could contain escaped characters to allow it to be used in a + URL. If set to true then the presence of the tag is filtered for. If set + to false then its absence is. If left out then no filtering on tag presence + is performed. + schema: + type: boolean - $ref: '#/components/parameters/trait_resource_paged_key' - $ref: '#/components/parameters/trait_paged_limit' responses: diff --git a/api/schemas/webhook.json b/api/schemas/webhook.json index 6caa2364..e64bbccb 100644 --- a/api/schemas/webhook.json +++ b/api/schemas/webhook.json @@ -81,6 +81,9 @@ "verbose_storage": { "description": "Whether to include storage metadata in the `get_urls` property in `flows/segments_added` events. This option is the same as the `verbose_storage` query parameter for the [/flows/{flowId}/segments](#/operations/GET_flows-flowId-segments) API endpoint.", "type": "boolean" + }, + "tags": { + "$ref": "tags.json" } } } From 2ce7d421f2312070c56fd2570185cc88d5aaf5a1 Mon Sep 17 00:00:00 2001 From: James Sandford Date: Wed, 9 Jul 2025 16:37:49 +0100 Subject: [PATCH 16/33] Improve fine-grained webhook documentation --- docs/appnotes/0016-authorisation-in-tams-workflows.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/docs/appnotes/0016-authorisation-in-tams-workflows.md b/docs/appnotes/0016-authorisation-in-tams-workflows.md index 08602656..b19b00eb 100644 --- a/docs/appnotes/0016-authorisation-in-tams-workflows.md +++ b/docs/appnotes/0016-authorisation-in-tams-workflows.md @@ -149,7 +149,7 @@ For example - hiding collection relationships may result in clients deciding to | | `POST` | Request must have admin permissions. Otherwise reject. | | `/service/storage-backends` | `HEAD`/`GET` | Available to all | | `/service/webhooks` | `HEAD`/`GET` | Restrict returned data by adding list of claimed auth_classes to `tag.auth_classes.includes`. If the incoming request has `tag.auth_classes.includes` set, the request must be processed with `tag.auth_classes.includes` set to the intersection of the claimed auth_classes and the provided list in `tag.auth_classes.includes`. | -| | `POST` | Request must have write permissions on the webhook being edited. If the request edits the `auth_classes` tag of a webhook, the request must have the permissions being edited. i.e. If the request adds or removes delete permissions for any group, it must have delete permissions on the webhook. If the request includes Source or Flow filters, the request must have read permissions on all Source or Flow IDs. If an implementation is not capable dynamically assessing permissions of new Sources/Flows, it may reject requests which do not specify Source/Flow filters. Otherwise, reject. | +| | `POST` | Request must have write permissions on the webhook being edited. If the request edits the `auth_classes` tag of a webhook, the request must have the permissions being edited. i.e. If the request adds or removes delete permissions for any group, it must have delete permissions on the webhook. If the request includes Source or Flow filters, the request must have read permissions on all Source or Flow IDs. If an implementation is not capable of dynamically assessing permissions of new Sources/Flows, it may reject requests which do not specify Source/Flow filters. Otherwise, reject. | | `/sources` | `HEAD`/`GET` | Restrict returned data by adding list of claimed auth_classes to `tag.auth_classes.includes`. If the incoming request has `tag.auth_classes.includes` set, the request must be processed with `tag.auth_classes.includes` set to the intersection of the claimed auth_classes and the provided list in `tag.auth_classes.includes`. | | `/sources/{sourceId}` | `HEAD`/`GET` | Request must have read permissions on {sourceId}. Otherwise reject. | | `/sources/{sourceId}/tags` | `HEAD`/`GET` | Request must have read permissions on {sourceId}. Otherwise reject. | @@ -221,7 +221,8 @@ Where requests are rejected, they should return as follows: ### Fine-grained authorisation and webhook events -A basic implementation may enumerate Flows and Sources a user has access to when creating/updating the webhook. +Implementations must evaluate permissions against webhook events themselves as well as the API's HTTP endpoints. +A basic implementation may enumerate Flows and Sources a user has access to when creating/updating the webhook and use this to filter events. This approach is strongly discouraged as permissions may change over time. It is recommended that implementations asses permissions on a per-event basis. Implementations may use `auth_classes` tags in Flow/Source updated events to maintain a cache of Flow/Sources a webhook has read permissions for. From bd508f00218d131f422a35ae93ac61b55d0ddb90 Mon Sep 17 00:00:00 2001 From: James Sandford Date: Thu, 10 Jul 2025 15:26:17 +0100 Subject: [PATCH 17/33] Add ADR on fine-grained auth --- docs/README.md | 1 + docs/adr/0035-fine-grained-auth.md | 173 ++++++++++++++++++ .../0016-authorisation-in-tams-workflows.md | 2 +- 3 files changed, 175 insertions(+), 1 deletion(-) create mode 100644 docs/adr/0035-fine-grained-auth.md diff --git a/docs/README.md b/docs/README.md index e63b507a..ea0350c0 100644 --- a/docs/README.md +++ b/docs/README.md @@ -69,6 +69,7 @@ For more information on how we use ADRs, see [here](./adr/README.md). | [0031](./adr/0031-flow-image-support.md) | Add new flow type to support still images | | [0032](./adr/0032-specifying-storage-backend.md) | Specifying storage backend when requesting storage allocation | | [0034](./adr/0034-storage-allow-object_ids.md) | Add object_ids option to Flow Storage request | +| [0035](./adr/0035-fine-grained-auth.md) | Fine-grained Authorisation in TAMS Workflows | | [0037](./adr/0037-improve-webhooks.md) | Proposal for improvements to the Webhooks endpoints | | [0038](./adr/0038-improved-storage-management.md) | Improved Storage Management | | [0039](./adr/0039-remove-pre-actions.md) | Proposal to remove pre-actions from storage allocation response | diff --git a/docs/adr/0035-fine-grained-auth.md b/docs/adr/0035-fine-grained-auth.md new file mode 100644 index 00000000..03b2d224 --- /dev/null +++ b/docs/adr/0035-fine-grained-auth.md @@ -0,0 +1,173 @@ +--- +status: "proposed" +--- +# Fine-grained Authorisation in TAMS Workflows + +## Context and Problem Statement + +TAMS provides multiple methods for authentication, as described in [ADR0028](./0028-authentication-methods.md). +The most commonly used method is currently OAuth2. +OAuth2 allows for auth tokens to claim [scopes](https://oauth.net/2/scope/) as a means to restrict the permissions of clients and requests. +This has been used in TAMS implementations to provide coarse grained authorization. + +Emerging TAMS use cases are making use of TAMS' media re-use capabilities over increasingly large numbers of users, teams, and organisations. +As the number of clients accessing content in a TAMS store grows, the need for finer-grained control of that content becomes more acute. + +This ADR presents the decisions and considerations that informed the initial approach to fine-grained auth in TAMS. + + +## Decision Drivers + +* Be prescriptive enough to enable interoperability of service and client implementations +* Be permissive enough to facilitate integration with existing workflows and systems +* As far as practical, maintain sensible parallels between coarse and fine-grained approaches +* Enable as fine-grained auth as is practical and sensible +* Must be possible to implement the design efficiently + +## Considered Options + +* Option 1a: Granularity of auth - Source +* Option 1b: Granularity of auth - Flow +* Option 1c: Granularity of auth - Segment +* Option 1d: Granularity of auth - Object +* Option 2a: Level of prescriptiveness - General principles +* Option 2b: Level of prescriptiveness - Auth logic +* Option 2c: Level of prescriptiveness - Specific API requests/pseudocode/scopes +* Option 3a: Supported architectures - Deep integration +* Option 3b: Supported architectures - Auth proxy +* Option 4a: Auth attributes - Multiple Tags +* Option 4b: Auth attributes - Single Tag with a list value +* Option 4c: Auth attributes - Specific parameters + +## Decision Outcome + +Chosen options: + +* Option 1b: Granularity of auth - Flow +* Option 2b: Level of prescriptiveness - Auth logic +* Option 3b: Supported architectures - Auth proxy +* Option 4b: Auth attributes - Single Tag with a list value + +This combination of options provides a good balance of well defined behaviour and flexibility. +It facilitates both interoperability and integration with existing auth systems and workflows. +The choice of Option 4b allows for us to experiment with, and refine our approach to fine-grained auth. +Once our approach is mature, we may wish to consider migrating to Option 4c to provide a more efficient solution. + +### Implementation + +Implemented by . + +## Pros and Cons of the Options + +### Option 1a: Granularity of auth - Source + +Define permissions at the Source level. +Permissions are then propagated down to Flows, Segments, and Objects. + +* Good, because it can be implemented with minimal changes to the API specification +* Good, because it requires the least additional data to be stored in/alongside the API +* Bad, because it provides limited control over different representations of media + * Prevents cost control (e.g. allow access to proxies, but not hi-res) + * Prevents management of access to specific storage backends + +### Option 1b: Granularity of auth - Flow + +Define permissions at the Source and Flow levels. +Permissions are then propagated down to Segments, and Objects. + +* Good, because it can be implemented with minimal changes to the API specification +* Good, because it requires a manageable amount of additional data to be stored in/alongside the API +* Good, because it provides control over different representations of media +* Neutral, because access can only be provided to entire flows + * Time-scoped access requires new flows to be created, potentially via zero-copy mechanism + +### Option 1c: Granularity of auth - Segment + +Define permissions at the Source, Flow, and Segment levels. +Permissions are then propagated down to Objects. + +* Good, because it can be implemented with minimal changes to the API specification +* Good, because it provides control over different representations of media +* Neutral, because it enables direct control of access to segments of flow timelines + * Allowing direct sub-segment access control would require significant modification +* Bad, because it requires significant amounts of additional data to be stored in/alongside the API + +### Option 1d: Granularity of auth - Object + +Define permissions at the Source, Flow, Segment, and Object levels. + +* Good, because it can be implemented with minimal changes to the API specification +* Good, because it provides control over different representations of media +* Neutral, because it enables direct control of access to segments of flow timelines + * Allowing direct sub-segment access control would require significant modification +* Bad, because it requires significant amounts of additional data to be stored in/alongside the API +* Bad, because it may make re-use of media very complicated + +### Option 2a: Level of prescriptiveness - General principles + +State some general principles implementations may follow, and technologies they may use. +Leave the definition of auth logic etc up to individual implementations. + +* Good, because it provides high levels of flexibility in implementations +* Bad, because it makes interoperability around fine-grained auth difficult +* Bad, because it requires implementers to derive all auth logic from scratch, including a few known non-trivial aspects + +### Option 2b: Level of prescriptiveness - Auth logic + +Define principles, and high-level auth logic. +Leave specific algorithms, and the requests permissions evaluation systems may make to the API up to the individual implementations. + +* Good, because it facilitates interoperability around fine-grained auth +* Good, because implementers don't have to derive auth logic from scratch +* Neutral, because it provides medium levels of flexibility to implementations + +### Option 2c: Level of prescriptiveness - Specific API requests/pseudocode/scopes + +Define all auth logic, algorithms that evaluate that auth logic, and API requests permission evaluation systems will make. + +* Good, because it facilitates interoperability around fine-grained auth +* Good, because implementers don't have to derive auth logic, or algorithms from scratch +* Bad, because it provides low levels of flexibility to implementations + * This may conflict with existing auth systems, and workflows in deployments + +### Option 3a: Supported architectures - Deep integration + +Assume all TAMS services that support fine-grained auth will implement it with deep integration into the API implementation. + +* Good, because its the most efficient option to implement +* Bad, because it provides low levels of flexibility when integrating with existing auth systems, and workflows in deployments + +### Option 3b: Supported architectures - Auth proxy + +Assume fine-grained auth may be implemented using the Auth Proxy pattern. + +* Good, because it provides high levels of flexibility when integrating with existing auth systems, and workflows in deployments +* Neutral, because its an acceptably efficient option to implement + +### Option 4a: Auth attributes - Multiple Tags + +Use existing tags implementation to provide/query attributes as part of an Attribute-Based Access Control (ABAC) system. + +* Good, because it requires little/no changes to the API specification +* Bad, because it requires un-intuitive use of the existing tag features +* Bad, because it requires multiple API calls when carrying out evaluation of permissions on certain endpoints + +### Option 4b: Auth attributes - Single Tag with a list value + +Expand the existing tag capabilities to allow lists as values to support an improved ABAC solution. + +* Good, because required changes to the API specification are useful beyond fine-grained auth +* Good, because it makes use of tags for this purpose more intuitive +* Good, because it requires very few API calls when carrying out evaluation of permissions on endpoints + +### Option 4c: Auth attributes - Specific parameters + +Define new parameters throughout the API specification for the purpose of storing auth-specific attributes. + +* Good, because it allows for an optimal number API calls when carrying out evaluation of permissions on endpoints +* Neutral, because it results in increased integration of auth mechanisms into the core specification +* Bad, because it requires significant changes to the API specification for a feature that has not been widely tested + +## More Information + +Information on TAMS' approach to authentication may be found in [ADR0028](./0028-authentication-methods.md). diff --git a/docs/appnotes/0016-authorisation-in-tams-workflows.md b/docs/appnotes/0016-authorisation-in-tams-workflows.md index b19b00eb..f749e810 100644 --- a/docs/appnotes/0016-authorisation-in-tams-workflows.md +++ b/docs/appnotes/0016-authorisation-in-tams-workflows.md @@ -3,7 +3,7 @@ ## Abstract Media workflows often contain sensitive or high-value content, and media organisations need to effectively manage access to that content across their estate. -That requires suitable approaches across both authentication (identifying the user) as discussed in [ADR0028](../adr/0028-authentication-methods.md), and also authorisation (deciding what the user can do), which is discussed here. +That requires suitable approaches across both authentication (identifying the user) as discussed in [ADR0028](../adr/0028-authentication-methods.md), and also authorisation (deciding what the user can do), which is discussed in [ADR035](../adr/0035-fine-grained-auth.md) and implemented here. In general, store implementations, and the organisations that deploy them, are free to define how the authorisation model works based on their needs, however this Application Note provides some guidelines and a starting point. ## Overall Principles From 0d6aef9ab445c342aa4093e1c12b28d988442a47 Mon Sep 17 00:00:00 2001 From: James Sandford Date: Thu, 10 Jul 2025 15:33:15 +0100 Subject: [PATCH 18/33] Change based on feedback --- docs/appnotes/0016-authorisation-in-tams-workflows.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/appnotes/0016-authorisation-in-tams-workflows.md b/docs/appnotes/0016-authorisation-in-tams-workflows.md index f749e810..7d9f1821 100644 --- a/docs/appnotes/0016-authorisation-in-tams-workflows.md +++ b/docs/appnotes/0016-authorisation-in-tams-workflows.md @@ -270,7 +270,7 @@ For example a TAMS instance could be deployed with fine-grained authorisation su In this case it would make sense to treat all clients of that TAMS instance as identical from an authentication/authorisation perspective: for example a user operating an NLE would be expected to provide suitable authorised credentials, but so too would the organisations MAM when it wants access to the store. Another deployment approach might see a MAM or other tool expose a TAMS API interface itself, which is proxied through to some simpler backing store. -In this case the MAM might manage and enforce the other policies and rules around access to content, so it would make more sense to do the same in the TAMS API interface, and then use the MAM's own credentials to access the backing TAMS instance. +In this case the MAM might manage and enforce other policies and rules around access to content, so it would make more sense to do the same in the TAMS API interface, and then use the MAM's own credentials to access the backing TAMS instance. ## Use Cases From a2f3646e1f31e91d042fdd875fc955b8af88559b Mon Sep 17 00:00:00 2001 From: James Sandford Date: Fri, 11 Jul 2025 13:33:23 +0100 Subject: [PATCH 19/33] Various tweaks --- .../0016-authorisation-in-tams-workflows.md | 66 +++++++++++-------- 1 file changed, 38 insertions(+), 28 deletions(-) diff --git a/docs/appnotes/0016-authorisation-in-tams-workflows.md b/docs/appnotes/0016-authorisation-in-tams-workflows.md index 7d9f1821..1f5378f5 100644 --- a/docs/appnotes/0016-authorisation-in-tams-workflows.md +++ b/docs/appnotes/0016-authorisation-in-tams-workflows.md @@ -5,6 +5,7 @@ Media workflows often contain sensitive or high-value content, and media organisations need to effectively manage access to that content across their estate. That requires suitable approaches across both authentication (identifying the user) as discussed in [ADR0028](../adr/0028-authentication-methods.md), and also authorisation (deciding what the user can do), which is discussed in [ADR035](../adr/0035-fine-grained-auth.md) and implemented here. In general, store implementations, and the organisations that deploy them, are free to define how the authorisation model works based on their needs, however this Application Note provides some guidelines and a starting point. +It is recommended to follow these guidelines where possible to aid in interoperability between TAMS components. ## Overall Principles @@ -32,7 +33,7 @@ These are the recommended permissions (or "scopes" in OAuth 2.0): | `/service` | `HEAD`/`GET` ⚠️ | ✅ | ✅ | ✅ | ✅ | | | `POST` ⚠️ | ✅ | | | | | `/service/storage-backends` | `HEAD`/`GET` ⚠️ | ✅ | ✅ | ✅ | ✅ | -| `/service/webhooks` | `HEAD`/`GET` ⚠️ | ✅ | | | | +| `/service/webhooks` | `HEAD`/`GET` | ✅ | ✅ | | | | | `POST` | ✅ | | ✅ | | | `/sources` | `HEAD`/`GET` | ✅ | ✅ | | | | `/sources/{sourceId}` | `HEAD`/`GET` | ✅ | ✅ | | | @@ -133,7 +134,7 @@ It is only explicitly called out in the listing below where admins are the only The listing below refers to requests having permissions, rather than users. This is to account for cases where users only "claim" a subset of their permissions for a given request. Note that in some circumstances, requests may have to claim more permissions than may initially be assumed. -For example - when editing the `auth_classes` tag on a Source/Flow, requests must claim both write permissions and the permission they are changing. +For example - when editing the `auth_classes` tag on a Source/Flow/webhook, requests must claim both write permissions and the permission they are changing. i.e. If the request adds or removes delete permissions for any group, it must have valid delete permissions itself. This is to prevent permission escalation attacks such as a user with write permissions adding delete permissions to themselves. @@ -148,9 +149,9 @@ For example - hiding collection relationships may result in clients deciding to | `/service` | `HEAD`/`GET` | Available to all | | | `POST` | Request must have admin permissions. Otherwise reject. | | `/service/storage-backends` | `HEAD`/`GET` | Available to all | -| `/service/webhooks` | `HEAD`/`GET` | Restrict returned data by adding list of claimed auth_classes to `tag.auth_classes.includes`. If the incoming request has `tag.auth_classes.includes` set, the request must be processed with `tag.auth_classes.includes` set to the intersection of the claimed auth_classes and the provided list in `tag.auth_classes.includes`. | -| | `POST` | Request must have write permissions on the webhook being edited. If the request edits the `auth_classes` tag of a webhook, the request must have the permissions being edited. i.e. If the request adds or removes delete permissions for any group, it must have delete permissions on the webhook. If the request includes Source or Flow filters, the request must have read permissions on all Source or Flow IDs. If an implementation is not capable of dynamically assessing permissions of new Sources/Flows, it may reject requests which do not specify Source/Flow filters. Otherwise, reject. | -| `/sources` | `HEAD`/`GET` | Restrict returned data by adding list of claimed auth_classes to `tag.auth_classes.includes`. If the incoming request has `tag.auth_classes.includes` set, the request must be processed with `tag.auth_classes.includes` set to the intersection of the claimed auth_classes and the provided list in `tag.auth_classes.includes`. | +| `/service/webhooks` | `HEAD`/`GET` | Restrict returned data by adding list of claimed auth classes to `tag.auth_classes.includes`. If the incoming request has `tag.auth_classes.includes` set, the request must be processed with `tag.auth_classes.includes` set to the intersection of the claimed auth classes and the provided list in `tag.auth_classes.includes`. | +| | `POST` | Request must have write permissions on the webhook being edited. If the request edits the `auth_classes` tag of a webhook, the request must have the permissions being edited. i.e. If the request adds or removes delete permissions for any group, it must have delete permissions on the webhook. If the request includes Source or Flow filters, the request must have read permissions on all Source or Flow IDs requested. Otherwise, reject. | +| `/sources` | `HEAD`/`GET` | Restrict returned data by adding list of claimed auth classes to `tag.auth_classes.includes`. If the incoming request has `tag.auth_classes.includes` set, the request must be processed with `tag.auth_classes.includes` set to the intersection of the claimed auth classes and the provided list in `tag.auth_classes.includes`. | | `/sources/{sourceId}` | `HEAD`/`GET` | Request must have read permissions on {sourceId}. Otherwise reject. | | `/sources/{sourceId}/tags` | `HEAD`/`GET` | Request must have read permissions on {sourceId}. Otherwise reject. | | `/sources/{sourceId}/tags/{name}` | `HEAD`/`GET` | Request must have read permissions on {sourceId}. Otherwise reject. | @@ -162,9 +163,9 @@ For example - hiding collection relationships may result in clients deciding to | `/sources/{sourceId}/label` | `HEAD`/`GET` | Request must have read permissions on {sourceId}. Otherwise reject. | | | `PUT` | Request must have write permissions on {sourceId}. Otherwise, reject. | | | `DELETE` | Request must have write permissions on {sourceId}. Otherwise, reject. | -| `/flows` | `HEAD`/`GET` | Restrict returned data by adding list of claimed auth_classes to `tag.auth_classes.includes`. If the incoming request has `tag.auth_classes.includes` set, the request must be processed with `tag.auth_classes.includes` set to the intersection of the claimed auth_classes and the provided list in `tag.auth_classes.includes`. | +| `/flows` | `HEAD`/`GET` | Restrict returned data by adding list of claimed auth classes to `tag.auth_classes.includes`. If the incoming request has `tag.auth_classes.includes` set, the request must be processed with `tag.auth_classes.includes` set to the intersection of the claimed auth classes and the provided list in `tag.auth_classes.includes`. | | `/flows/{flowId}` | `HEAD`/`GET` | Request must have read permissions on {flowID}. Otherwise reject. | -| | `PUT` | If {flowId} does not currently exist, request must have write permissions on the Flow's Source ID or the Source ID doesn't currently exist in this TAMS instance. If {flowId} already exists Request must have write permissions on {flowId}. If the request edits the `auth_classes` tag, the request must have the permissions being edited. i.e. If the request adds or removes delete permissions for any group, it must have delete permissions on {flowId}. Otherwise, reject. | +| | `PUT` | If {flowId} does not currently exist, request must have write permissions on the Flow's Source ID if it already exists in this TAMS instance. If {flowId} already exists, request must have write permissions on {flowId}. If the request edits the `auth_classes` tag, the request must have the permissions being edited. i.e. If the request adds or removes delete permissions for any group, it must have delete permissions on {flowId}. Otherwise, reject. | | | `DELETE` | Request must have delete permissions on {flowId}. Otherwise reject. | | `/flows/{flowId}/tags` | `HEAD`/`GET` | Request must have read permissions on {flowId}. Otherwise reject. | | `/flows/{flowId}/tags/{name}` | `HEAD`/`GET` | Request must have read permissions on {flowId}. Otherwise reject. | @@ -188,10 +189,10 @@ For example - hiding collection relationships may result in clients deciding to | | `PUT` | Request must have write permissions on {flowId}. Otherwise reject. | | | `DELETE` | Request must have write permissions on {flowId}. Otherwise reject. | | `/flows/{flowId}/segments` | `HEAD`/`GET` | Request must have read permissions on {flowId}. Otherwise reject. | -| | `POST` | Request must have write permissions on {flowId}, and either this must be the first registration of the object (i.e. `/objects/{objectId}` returns 404) or `referenced_by_flows` at `/objects/{objectId}` must not be empty when `flow_tag.auth_classes.includes` is set to claimed read auth_classes for each Object ID being written. Otherwise reject. | +| | `POST` | Request must have write permissions on {flowId}, and either this must be the first registration of the object(s) (i.e. `/objects/{objectId}` returns 404) or the request must have read access to the object(s) being written. Otherwise reject. | | | `DELETE` | Request must have write permissions on {flowId}. Otherwise reject. | | `/flows/{flowId}/storage` | `POST` | Request must have write permissions on {flowId}. Otherwise reject. | -| `/objects/{objectId}` | `HEAD`/`GET` | Restrict returned data by adding list of claimed auth_classes to `flow_tag.auth_classes.includes`. If the incoming request has `flow_tag.auth_classes.includes` set, the request must be processed with `flow_tag.auth_classes.includes` set to the intersection of the claimed auth_classes and the provided list in `flow_tag.auth_classes.includes`. | +| `/objects/{objectId}` | `HEAD`/`GET` | Restrict returned data in `referenced_by_flows` property by adding list of claimed auth classes to `flow_tag.auth_classes.includes`. If the incoming request has `flow_tag.auth_classes.includes` set, the request must be processed with `flow_tag.auth_classes.includes` set to the intersection of the claimed auth classes and the provided list in `flow_tag.auth_classes.includes`. | | `/flow-delete-requests` | `HEAD`/`GET` | Request must have admin permissions. Otherwise reject. | | `/flow-delete-requests/{request-id}` | `HEAD`/`GET` | Request must have delete permissions on the Delete Request's Flow ID. Otherwise reject. | @@ -199,17 +200,24 @@ For example - hiding collection relationships may result in clients deciding to #### Flows -Read, write, and delete permissions on individual flows may be determined via auth_classes listed in the `auth_classes` tag on the flow. +Read, write, and delete permissions on individual flows may be determined via auth classes listed in the `auth_classes` tag on the flow. This may be done via the `/flows/{flowId}/tags/auth_classes` endpoint. #### Sources -Read, write, and delete permissions on individual sources may be determined via auth_classes listed in the `auth_classes` tag on the source. +Read, write, and delete permissions on individual sources may be determined via auth classes listed in the `auth_classes` tag on the source. This may be done via the `/sources/{sourceId}/tags/auth_classes` endpoint. +#### Objects + +Read, write, and delete permissions on individual objects may be determined by filtering returned flows on the object. +This may be done by setting `flow_tag.auth_classes.includes` to relevant claimed auth classes (e.g. auth classes with read permissions if read permissions on the object are to be verified). +If `referenced_by_flows` in the returned data is empty, the request DOES NOT have the relevant permissions. +If `referenced_by_flows` in the returned data is empty, the request DOES have the relevant permissions. + #### Webhooks -Read, write, and delete permissions on individual webhooks may be determined via auth_classes listed in the `auth_classes` tag on the webhook. +Read, write, and delete permissions on individual webhooks may be determined via auth classes listed in the `auth_classes` tag on the webhook. This may be done via the `/sources/{sourceId}/tags/auth_classes` endpoint. ### Handling rejected requests @@ -226,37 +234,40 @@ A basic implementation may enumerate Flows and Sources a user has access to when This approach is strongly discouraged as permissions may change over time. It is recommended that implementations asses permissions on a per-event basis. Implementations may use `auth_classes` tags in Flow/Source updated events to maintain a cache of Flow/Sources a webhook has read permissions for. -Implementations should regularly inspect flow tags to guard against missed events. +Implementations should regularly inspect Source/Flow tags, via the HTTP API or other methods, to guard against missed events. Implementations should regularly check the user's permissions in the auth system for changes. +If permissions changes are observed, the set of permissions used to evaluate against events should only ever be reduced in scope and never increased. ### Adding Flows to Sources New Sources inherit permissions from the first Flow which references them. -In order to prevent malicious actors adding maliciously crafted Flows to an existing Source, Flows using an existing Source ID must have write write permissions on the Source. +In order to prevent malicious actors adding maliciously crafted Flows to an existing Source, Flows using an existing Source ID SHOULD have write write permissions on the Source. This may be an impediment to some workflows, such as where dual-redundant ingesters capture the same Source. Or where different teams within a business re-ingest the same Source in a different format. Some deployments may choose to accept this risk and allow broader re-use of Sources. -Implementations may either apply default auth_classes to Sources which will grant all users write permissions, or they may use more permissive auth logic. +Implementations may either apply default auth classes to Sources which will grant all users write permissions, or they may use more permissive auth logic. ### Implementation -To implement the model above, a way to hold the auth_classes in TAMS is needed, along with a system to store the permissions and the authorisation logic that maps them to auth_classes. +To implement the model above, a way to hold the auth classes in TAMS is needed, along with a system to store the permissions and the authorisation logic that maps them to auth classes. -For the latter, [Amazon Verified Permissions](https://aws.amazon.com/verified-permissions/) and [Permify](https://github.com/Permify/permify) both serve as permissions management tools. -They allow authorisation decisions to be made by taking a set of policies defined in some domain-specific language, along with the attributes of the user (group membership) and resource (Source/Flow auth_classes), and computing whether to allow the request. +For the latter, [Amazon Verified Permissions](https://aws.amazon.com/verified-permissions/) and [Permify](https://github.com/Permify/permify) may both serve as permissions management tools. +They allow authorisation decisions to be made by taking a set of policies defined in some domain-specific language, along with the attributes of the user (group membership) and resource (Source/Flow/webhook auth classes), and computing whether to allow the request. +They may also return the permitted resource attributes (Source/Flow/webhook auth classes) for a given user of a given endpoint. +This may be useful when filtering results in Flow listings, for example. This decision process is intended to be run inline for each request, for example at an authenticating proxy placed in front of the API server. -For storing auth_classes an initial proof-of-concept could be built, as described above, using Source and Flow tags. -A "special" `auth_classes` tag would store a comma-separated list of auth_classes assigned to a Flow or Source +For storing auth classes an initial proof-of-concept could be built using Source, Flow, and webhook tags as described above. +A "special" `auth_classes` tag would store a comma-separated list of auth classes assigned to a Flow, Source or webhook. The authenticating proxy would need to take steps to prevent unauthorised modification of this special tag, as described above. -In addition, it should be possible to set auth_classes on a multi-essence Source or Flow, and apply that permissions downwards to all the Sources or Flows it collects. -Similarly, auth_classes should be set by default on a Source (and apply to all Flows), but be settable on individual Flows as well for additional flexibility. +In addition, it should be possible to set auth classes on a multi-essence Source or Flow, and apply that permissions downwards to all the Sources or Flows it collects. +Similarly, auth classes should be set by default on a Source (and apply to all Flows), but be settable on individual Flows as well for additional flexibility. To avoid a complex traversal of potentially a large hierarchy (and to simplify the listing endpoints), it may be useful to denormalise the tag on write, writing it to all the Sources and Flows it would affect as well. As a result, the process of authorising a request is: -1. Read the list of auth_classes assigned to the resource +1. Read the list of auth classes assigned to the resource 2. Read the user's claimed scopes from their provided token 3. Request a decision from the permissions system based on those data 4. (Write requests only): Check whether the request would modify the special `auth_classes` tag, and confirm the user has permission to make that modification @@ -276,12 +287,11 @@ In this case the MAM might manage and enforce other policies and rules around ac ### Providing access to a subset of a Flow's timerange -The model described above allows access to content to be controlled at the Source/Flow level. +The model described above allows access control at the Source/Flow level. Some use cases may require finer grained control. -Object-level access control was deemed to be too inefficient to implement. -It may, however, be achieved by creating a new flow with the relevant permissions that refers to the Objects of interest. +This may be achieved by creating a new flow with the relevant permissions that refers to the Objects of interest. Caution should be taken where the boundary timestamps land partway through an Object. -Where the material around the boundaries is sensitive, new trimmed Objects should be created at the boundaries that only include the media used in the new Flow. +Where the material around the boundaries is sensitive, new trimmed Objects should be created at the boundaries that remove content outside the permitted range. ### Global read access @@ -291,7 +301,7 @@ Implementations may provide this feature by either adding default groups to Sour ## Future Work The model described above allows for more use cases than coarse-grained RBAC: especially use cases where multiple tenants share a single store. -However it would be useful to allow more attributes to be used in rules: for example making the tags of the Sources/Flows available to write policies upon as well. +However it would be useful to allow more attributes to be used in rules: for example allowing write access to the tags of Sources/Flows but not other properties. One of the areas noted in [ADR0028: Authentication Methods](../adr/0028-authentication-methods.md) is being able to issue credentials restricted to a limited subset of Sources or Flows, which must also be supported by the authorisation system. This could be achieved by issuing JWT bearer tokens using the [RFC9396 authorization details](https://www.rfc-editor.org/rfc/rfc9396.html#name-authorization-request) field to embed permissions granted directly into the token. From 5a1c2670c95d95ca0ffa88d60658e28a37fd268f Mon Sep 17 00:00:00 2001 From: James Sandford Date: Fri, 11 Jul 2025 16:09:54 +0100 Subject: [PATCH 20/33] Add info on "deny" permissions. Expand explanation of permissions propagation --- .../0016-authorisation-in-tams-workflows.md | 25 +++++++++++++++---- 1 file changed, 20 insertions(+), 5 deletions(-) diff --git a/docs/appnotes/0016-authorisation-in-tams-workflows.md b/docs/appnotes/0016-authorisation-in-tams-workflows.md index 1f5378f5..456ccd8a 100644 --- a/docs/appnotes/0016-authorisation-in-tams-workflows.md +++ b/docs/appnotes/0016-authorisation-in-tams-workflows.md @@ -261,10 +261,6 @@ For storing auth classes an initial proof-of-concept could be built using Source A "special" `auth_classes` tag would store a comma-separated list of auth classes assigned to a Flow, Source or webhook. The authenticating proxy would need to take steps to prevent unauthorised modification of this special tag, as described above. -In addition, it should be possible to set auth classes on a multi-essence Source or Flow, and apply that permissions downwards to all the Sources or Flows it collects. -Similarly, auth classes should be set by default on a Source (and apply to all Flows), but be settable on individual Flows as well for additional flexibility. -To avoid a complex traversal of potentially a large hierarchy (and to simplify the listing endpoints), it may be useful to denormalise the tag on write, writing it to all the Sources and Flows it would affect as well. - As a result, the process of authorising a request is: 1. Read the list of auth classes assigned to the resource @@ -283,7 +279,7 @@ In this case it would make sense to treat all clients of that TAMS instance as i Another deployment approach might see a MAM or other tool expose a TAMS API interface itself, which is proxied through to some simpler backing store. In this case the MAM might manage and enforce other policies and rules around access to content, so it would make more sense to do the same in the TAMS API interface, and then use the MAM's own credentials to access the backing TAMS instance. -## Use Cases +## Use cases and additional optional functionality ### Providing access to a subset of a Flow's timerange @@ -298,6 +294,25 @@ Where the material around the boundaries is sensitive, new trimmed Objects shoul Some organisations/implementations may choose to provide read access to all Sources and Flows to promote content re-use, and reduce the writing of duplicate content to the store. Implementations may provide this feature by either adding default groups to Sources and Flows that provide appropriate read access to users, or by using more permissive auth logic. +### Permissions propagation + +The basic implementation described above will populate auth classes on a new Source with those in the Flow that results in its creation. +This is a product of TAMS' general behaviour of populating Source metadata from Flows on creation. + +Some implementations may also find it useful to propagate changes of Source permissions to their Flows, and Source/Flow permissions down to Sources and Flows they collect. +For example, where Multi Source A collects Video Source B and Audio Source C, changes to permissions on Source A would be reflected on Sources B and C as well as the Flows of A, B, and C. +The propagation of these permissions should happen on write to avoid the need for potentially extensive tree traversal on read. +When changes are propagated, they must only be applied to resources where the request has the permission to edit auth classes. +Where propagation reaches a resource that the request doesn't have sufficient permissions to edit, the process will stop following that branch of the resource tree. +Propagation of permissions should only be performed after the successful modification of a parent resource. + +Propagation should also be triggered when a new Source/Flow is added to a Source/Flow collection, or when a Flow is added to an existing Source. + +### Deny permissions + +Implementations may wish to support auth classes and related auth logic that explicitly denies permissions against resources. +In these cases, a matching "deny" class takes precedent over an "allow" class. + ## Future Work The model described above allows for more use cases than coarse-grained RBAC: especially use cases where multiple tenants share a single store. From 67bfbbcf59135ac75ce14ba6463377084c297a12 Mon Sep 17 00:00:00 2001 From: James Sandford Date: Fri, 11 Jul 2025 16:16:08 +0100 Subject: [PATCH 21/33] Add regex to tag query strings --- api/TimeAddressableMediaStore.yaml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/api/TimeAddressableMediaStore.yaml b/api/TimeAddressableMediaStore.yaml index 51b72345..0ee751bc 100644 --- a/api/TimeAddressableMediaStore.yaml +++ b/api/TimeAddressableMediaStore.yaml @@ -165,6 +165,7 @@ paths: Partial string matches of the values are not valid. schema: type: string + pattern: ^([^,]+(,[^,]+)*)?$ - name: tag_exists.{name} in: query description: | @@ -220,6 +221,7 @@ paths: Partial string matches of the values are not valid. schema: type: string + pattern: ^([^,]+(,[^,]+)*)?$ - name: tag_exists.{name} in: query description: | @@ -419,6 +421,7 @@ paths: Partial string matches of the values are not valid. schema: type: string + pattern: ^([^,]+(,[^,]+)*)?$ - name: tag_exists.{name} in: query description: | @@ -480,6 +483,7 @@ paths: Partial string matches of the values are not valid. schema: type: string + pattern: ^([^,]+(,[^,]+)*)?$ - name: tag_exists.{name} in: query description: | @@ -869,6 +873,7 @@ paths: Partial string matches of the values are not valid. schema: type: string + pattern: ^([^,]+(,[^,]+)*)?$ - name: tag_exists.{name} in: query description: | @@ -956,6 +961,7 @@ paths: Partial string matches of the values are not valid. schema: type: string + pattern: ^([^,]+(,[^,]+)*)?$ - name: tag_exists.{name} in: query description: | From e99ae21a60a64bbb9990d62b355776fe8e5c6eb8 Mon Sep 17 00:00:00 2001 From: James Sandford Date: Fri, 11 Jul 2025 16:27:13 +0100 Subject: [PATCH 22/33] Apply suggestions from code review Co-authored-by: Sam Mesterton-Gibbons --- docs/adr/0035-fine-grained-auth.md | 14 ++++++++------ .../0016-authorisation-in-tams-workflows.md | 4 ++-- 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/docs/adr/0035-fine-grained-auth.md b/docs/adr/0035-fine-grained-auth.md index 03b2d224..187606e3 100644 --- a/docs/adr/0035-fine-grained-auth.md +++ b/docs/adr/0035-fine-grained-auth.md @@ -6,9 +6,9 @@ status: "proposed" ## Context and Problem Statement TAMS provides multiple methods for authentication, as described in [ADR0028](./0028-authentication-methods.md). -The most commonly used method is currently OAuth2. +The most commonly used method to implement authentication is Bearer tokens acquired using OAuth2 flows. OAuth2 allows for auth tokens to claim [scopes](https://oauth.net/2/scope/) as a means to restrict the permissions of clients and requests. -This has been used in TAMS implementations to provide coarse grained authorization. +This has been used in TAMS implementations to provide coarse grained authorisation. Emerging TAMS use cases are making use of TAMS' media re-use capabilities over increasingly large numbers of users, teams, and organisations. As the number of clients accessing content in a TAMS store grows, the need for finer-grained control of that content becomes more acute. @@ -74,6 +74,7 @@ Permissions are then propagated down to Flows, Segments, and Objects. Define permissions at the Source and Flow levels. Permissions are then propagated down to Segments, and Objects. +However the default approach should be to define permissions on Sources (since permissions are likely to apply to all renditions of a piece of content), with the option to use Flows where restricting specific variants is required. * Good, because it can be implemented with minimal changes to the API specification * Good, because it requires a manageable amount of additional data to be stored in/alongside the API @@ -98,6 +99,7 @@ Define permissions at the Source, Flow, Segment, and Object levels. * Good, because it can be implemented with minimal changes to the API specification * Good, because it provides control over different representations of media +* Good, because it maps the right to access content onto the content itself, regardless of how it is re-used * Neutral, because it enables direct control of access to segments of flow timelines * Allowing direct sub-segment access control would require significant modification * Bad, because it requires significant amounts of additional data to be stored in/alongside the API @@ -115,7 +117,7 @@ Leave the definition of auth logic etc up to individual implementations. ### Option 2b: Level of prescriptiveness - Auth logic Define principles, and high-level auth logic. -Leave specific algorithms, and the requests permissions evaluation systems may make to the API up to the individual implementations. +Leave specific algorithms, and the requests that permissions evaluation systems may make to the API up to the individual implementations. * Good, because it facilitates interoperability around fine-grained auth * Good, because implementers don't have to derive auth logic from scratch @@ -128,18 +130,18 @@ Define all auth logic, algorithms that evaluate that auth logic, and API request * Good, because it facilitates interoperability around fine-grained auth * Good, because implementers don't have to derive auth logic, or algorithms from scratch * Bad, because it provides low levels of flexibility to implementations - * This may conflict with existing auth systems, and workflows in deployments + * This may conflict with existing auth systems, and workflows in deployments and organisational structures and models ### Option 3a: Supported architectures - Deep integration -Assume all TAMS services that support fine-grained auth will implement it with deep integration into the API implementation. +Assume all TAMS services that support fine-grained auth will implement it with deep integration into the API implementation where policy decisions are made by the store implementation as part of processing a request, with full access to any backing databases etc. * Good, because its the most efficient option to implement * Bad, because it provides low levels of flexibility when integrating with existing auth systems, and workflows in deployments ### Option 3b: Supported architectures - Auth proxy -Assume fine-grained auth may be implemented using the Auth Proxy pattern. +Assume fine-grained auth may be implemented using the Auth Proxy pattern where an HTTP reverse proxy can receive incoming requests, amend them as needed and forward them onto a store (which may have no fine-grained authorisation model). The proxy can use the contents of the original request, the amended request(s) and the response(s) to make a decision on what to return to the user without modifying the underlying store. * Good, because it provides high levels of flexibility when integrating with existing auth systems, and workflows in deployments * Neutral, because its an acceptably efficient option to implement diff --git a/docs/appnotes/0016-authorisation-in-tams-workflows.md b/docs/appnotes/0016-authorisation-in-tams-workflows.md index 456ccd8a..362e621c 100644 --- a/docs/appnotes/0016-authorisation-in-tams-workflows.md +++ b/docs/appnotes/0016-authorisation-in-tams-workflows.md @@ -94,7 +94,7 @@ Users may be assigned combinations of these roles for different purposes, for ex - `store-cleanup-system`: Has `tams-api/delete` To implement the authorisation, the authorisation server checks the requested scopes against the user's access when issuing a token. -The TAMS server, or it's auth proxy, rejects requests without appropriate scopes. +The TAMS server, or its auth proxy, rejects requests without appropriate scopes. ## Finer Grained Authorisation @@ -103,7 +103,7 @@ However the implementation of this can become complex and unwieldy, especially i Attribute-Based Access Control (ABAC) is one approach to manage this complexity, by describing permissions policies based on the attributes of resources (Sources and Flows), and if necessary, users as well. However full ABAC can be challenging to implement and requires a degree of organisational maturity to construct and manage stable attributes. -This section describes a recommended approach to ABAC authorisation logic to aid interoperability between TAMS implementations. +This section describes a possible approach to ABAC authorisation logic to aid interoperability between TAMS implementations, in which content is assigned an attribute in the form of a "class". This approach should be considered experimental at this point. Due its experimental nature, this approach makes use of the tags feature in TAMS. Future iterations of these proposals may elevate ABAC attributes to a specific field in the core specification. From fd1cb4e099f8e8f4db6c0f03a25879bd74714877 Mon Sep 17 00:00:00 2001 From: James Sandford Date: Fri, 11 Jul 2025 16:42:56 +0100 Subject: [PATCH 23/33] Changes following review --- docs/adr/0035-fine-grained-auth.md | 10 +++++++--- .../0016-authorisation-in-tams-workflows.md | 14 +++++++------- 2 files changed, 14 insertions(+), 10 deletions(-) diff --git a/docs/adr/0035-fine-grained-auth.md b/docs/adr/0035-fine-grained-auth.md index 187606e3..042ac8bd 100644 --- a/docs/adr/0035-fine-grained-auth.md +++ b/docs/adr/0035-fine-grained-auth.md @@ -20,6 +20,7 @@ This ADR presents the decisions and considerations that informed the initial app * Be prescriptive enough to enable interoperability of service and client implementations * Be permissive enough to facilitate integration with existing workflows and systems +* Acknowledge that organisations will have different threat models, and interoperability should be equally possible in more open and more restrictive environments * As far as practical, maintain sensible parallels between coarse and fine-grained approaches * Enable as fine-grained auth as is practical and sensible * Must be possible to implement the design efficiently @@ -50,6 +51,7 @@ Chosen options: This combination of options provides a good balance of well defined behaviour and flexibility. It facilitates both interoperability and integration with existing auth systems and workflows. +The choice of Option 3b does not preclude the solution developed being implemented as described in Option 3a. The choice of Option 4b allows for us to experiment with, and refine our approach to fine-grained auth. Once our approach is mature, we may wish to consider migrating to Option 4c to provide a more efficient solution. @@ -89,8 +91,9 @@ Permissions are then propagated down to Objects. * Good, because it can be implemented with minimal changes to the API specification * Good, because it provides control over different representations of media -* Neutral, because it enables direct control of access to segments of flow timelines - * Allowing direct sub-segment access control would require significant modification +* Good, because it enables direct control of access to segments of flow timelines + * Note that segments may contain multiple video frames/audio samples etc + * Allowing direct sub-segment access control would require significant further modification * Bad, because it requires significant amounts of additional data to be stored in/alongside the API ### Option 1d: Granularity of auth - Object @@ -141,7 +144,8 @@ Assume all TAMS services that support fine-grained auth will implement it with d ### Option 3b: Supported architectures - Auth proxy -Assume fine-grained auth may be implemented using the Auth Proxy pattern where an HTTP reverse proxy can receive incoming requests, amend them as needed and forward them onto a store (which may have no fine-grained authorisation model). The proxy can use the contents of the original request, the amended request(s) and the response(s) to make a decision on what to return to the user without modifying the underlying store. +Assume fine-grained auth may be implemented using the Auth Proxy pattern where an HTTP reverse proxy can receive incoming requests, amend them as needed and forward them onto a store (which may have no fine-grained authorisation model). +The proxy can use the contents of the original request, the amended request(s) and the response(s) to make a decision on what to return to the user without modifying the underlying store. * Good, because it provides high levels of flexibility when integrating with existing auth systems, and workflows in deployments * Neutral, because its an acceptably efficient option to implement diff --git a/docs/appnotes/0016-authorisation-in-tams-workflows.md b/docs/appnotes/0016-authorisation-in-tams-workflows.md index 362e621c..0b0895d9 100644 --- a/docs/appnotes/0016-authorisation-in-tams-workflows.md +++ b/docs/appnotes/0016-authorisation-in-tams-workflows.md @@ -149,9 +149,9 @@ For example - hiding collection relationships may result in clients deciding to | `/service` | `HEAD`/`GET` | Available to all | | | `POST` | Request must have admin permissions. Otherwise reject. | | `/service/storage-backends` | `HEAD`/`GET` | Available to all | -| `/service/webhooks` | `HEAD`/`GET` | Restrict returned data by adding list of claimed auth classes to `tag.auth_classes.includes`. If the incoming request has `tag.auth_classes.includes` set, the request must be processed with `tag.auth_classes.includes` set to the intersection of the claimed auth classes and the provided list in `tag.auth_classes.includes`. | +| `/service/webhooks` | `HEAD`/`GET` | Restrict returned data by adding list of claimed auth classes to `tag.auth_classes`. If the incoming request has `tag.auth_classes` set, the request must be processed with `tag.auth_classes` set to the intersection of the claimed auth classes and the provided list in `tag.auth_classes`. | | | `POST` | Request must have write permissions on the webhook being edited. If the request edits the `auth_classes` tag of a webhook, the request must have the permissions being edited. i.e. If the request adds or removes delete permissions for any group, it must have delete permissions on the webhook. If the request includes Source or Flow filters, the request must have read permissions on all Source or Flow IDs requested. Otherwise, reject. | -| `/sources` | `HEAD`/`GET` | Restrict returned data by adding list of claimed auth classes to `tag.auth_classes.includes`. If the incoming request has `tag.auth_classes.includes` set, the request must be processed with `tag.auth_classes.includes` set to the intersection of the claimed auth classes and the provided list in `tag.auth_classes.includes`. | +| `/sources` | `HEAD`/`GET` | Restrict returned data by adding list of claimed auth classes to `tag.auth_classes`. If the incoming request has `tag.auth_classes` set, the request must be processed with `tag.auth_classes` set to the intersection of the claimed auth classes and the provided list in `tag.auth_classes`. | | `/sources/{sourceId}` | `HEAD`/`GET` | Request must have read permissions on {sourceId}. Otherwise reject. | | `/sources/{sourceId}/tags` | `HEAD`/`GET` | Request must have read permissions on {sourceId}. Otherwise reject. | | `/sources/{sourceId}/tags/{name}` | `HEAD`/`GET` | Request must have read permissions on {sourceId}. Otherwise reject. | @@ -163,7 +163,7 @@ For example - hiding collection relationships may result in clients deciding to | `/sources/{sourceId}/label` | `HEAD`/`GET` | Request must have read permissions on {sourceId}. Otherwise reject. | | | `PUT` | Request must have write permissions on {sourceId}. Otherwise, reject. | | | `DELETE` | Request must have write permissions on {sourceId}. Otherwise, reject. | -| `/flows` | `HEAD`/`GET` | Restrict returned data by adding list of claimed auth classes to `tag.auth_classes.includes`. If the incoming request has `tag.auth_classes.includes` set, the request must be processed with `tag.auth_classes.includes` set to the intersection of the claimed auth classes and the provided list in `tag.auth_classes.includes`. | +| `/flows` | `HEAD`/`GET` | Restrict returned data by adding list of claimed auth classes to `tag.auth_classes`. If the incoming request has `tag.auth_classes` set, the request must be processed with `tag.auth_classes` set to the intersection of the claimed auth classes and the provided list in `tag.auth_classes`. | | `/flows/{flowId}` | `HEAD`/`GET` | Request must have read permissions on {flowID}. Otherwise reject. | | | `PUT` | If {flowId} does not currently exist, request must have write permissions on the Flow's Source ID if it already exists in this TAMS instance. If {flowId} already exists, request must have write permissions on {flowId}. If the request edits the `auth_classes` tag, the request must have the permissions being edited. i.e. If the request adds or removes delete permissions for any group, it must have delete permissions on {flowId}. Otherwise, reject. | | | `DELETE` | Request must have delete permissions on {flowId}. Otherwise reject. | @@ -192,7 +192,7 @@ For example - hiding collection relationships may result in clients deciding to | | `POST` | Request must have write permissions on {flowId}, and either this must be the first registration of the object(s) (i.e. `/objects/{objectId}` returns 404) or the request must have read access to the object(s) being written. Otherwise reject. | | | `DELETE` | Request must have write permissions on {flowId}. Otherwise reject. | | `/flows/{flowId}/storage` | `POST` | Request must have write permissions on {flowId}. Otherwise reject. | -| `/objects/{objectId}` | `HEAD`/`GET` | Restrict returned data in `referenced_by_flows` property by adding list of claimed auth classes to `flow_tag.auth_classes.includes`. If the incoming request has `flow_tag.auth_classes.includes` set, the request must be processed with `flow_tag.auth_classes.includes` set to the intersection of the claimed auth classes and the provided list in `flow_tag.auth_classes.includes`. | +| `/objects/{objectId}` | `HEAD`/`GET` | Restrict returned data in `referenced_by_flows` property by adding list of claimed auth classes to `flow_tag.auth_classes`. If the incoming request has `flow_tag.auth_classes` set, the request must be processed with `flow_tag.auth_classes` set to the intersection of the claimed auth classes and the provided list in `flow_tag.auth_classes`. | | `/flow-delete-requests` | `HEAD`/`GET` | Request must have admin permissions. Otherwise reject. | | `/flow-delete-requests/{request-id}` | `HEAD`/`GET` | Request must have delete permissions on the Delete Request's Flow ID. Otherwise reject. | @@ -211,14 +211,14 @@ This may be done via the `/sources/{sourceId}/tags/auth_classes` endpoint. #### Objects Read, write, and delete permissions on individual objects may be determined by filtering returned flows on the object. -This may be done by setting `flow_tag.auth_classes.includes` to relevant claimed auth classes (e.g. auth classes with read permissions if read permissions on the object are to be verified). +This may be done by setting `flow_tag.auth_classes` to relevant claimed auth classes (e.g. auth classes with read permissions if read permissions on the object are to be verified). If `referenced_by_flows` in the returned data is empty, the request DOES NOT have the relevant permissions. If `referenced_by_flows` in the returned data is empty, the request DOES have the relevant permissions. #### Webhooks -Read, write, and delete permissions on individual webhooks may be determined via auth classes listed in the `auth_classes` tag on the webhook. -This may be done via the `/sources/{sourceId}/tags/auth_classes` endpoint. +Read, write, and delete permissions on webhooks may be determined via auth classes listed in the `auth_classes` tag on webhooks. +The webhooks endpoint may be filtered to those with specific auth classes using the `tag.auth_closses` query parameter. ### Handling rejected requests From 42f90a71c7e86b6ad2059c5e5c5070ae3cde9338 Mon Sep 17 00:00:00 2001 From: James Sandford Date: Fri, 11 Jul 2025 16:59:45 +0100 Subject: [PATCH 24/33] Add (rejected) option to ADR0035 on mandating proposed fine-grained auth approach --- docs/adr/0035-fine-grained-auth.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/docs/adr/0035-fine-grained-auth.md b/docs/adr/0035-fine-grained-auth.md index 042ac8bd..0b311eb4 100644 --- a/docs/adr/0035-fine-grained-auth.md +++ b/docs/adr/0035-fine-grained-auth.md @@ -34,6 +34,7 @@ This ADR presents the decisions and considerations that informed the initial app * Option 2a: Level of prescriptiveness - General principles * Option 2b: Level of prescriptiveness - Auth logic * Option 2c: Level of prescriptiveness - Specific API requests/pseudocode/scopes +* Option 2d: Level of prescriptiveness - Mandate the use of the proposed approach * Option 3a: Supported architectures - Deep integration * Option 3b: Supported architectures - Auth proxy * Option 4a: Auth attributes - Multiple Tags @@ -135,6 +136,14 @@ Define all auth logic, algorithms that evaluate that auth logic, and API request * Bad, because it provides low levels of flexibility to implementations * This may conflict with existing auth systems, and workflows in deployments and organisational structures and models +### Option 2d: Level of prescriptiveness - Mandate the use of the proposed approach + +Make the proposed approach to fine-grained auth mandatory. + +* Good, because it ensures full interoperability around fine-grained auth +* Bad, because the approach may conflict with existing auth systems, and workflows in deployments and organisational structures and models +* Bad, because such conflicts being a mandatory part of the specification may prevent/impede use of TAMS by some organisations + ### Option 3a: Supported architectures - Deep integration Assume all TAMS services that support fine-grained auth will implement it with deep integration into the API implementation where policy decisions are made by the store implementation as part of processing a request, with full access to any backing databases etc. From 9cedde57fe1de14f90a0c79579873265aa17bb18 Mon Sep 17 00:00:00 2001 From: Sam Mesterton-Gibbons Date: Wed, 30 Jul 2025 14:17:10 +0100 Subject: [PATCH 25/33] adr: Minor wording improvement on authz Also removes a stray optional marker tag. --- docs/adr/0035-fine-grained-auth.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/docs/adr/0035-fine-grained-auth.md b/docs/adr/0035-fine-grained-auth.md index 0b311eb4..7335f099 100644 --- a/docs/adr/0035-fine-grained-auth.md +++ b/docs/adr/0035-fine-grained-auth.md @@ -15,14 +15,13 @@ As the number of clients accessing content in a TAMS store grows, the need for f This ADR presents the decisions and considerations that informed the initial approach to fine-grained auth in TAMS. - ## Decision Drivers * Be prescriptive enough to enable interoperability of service and client implementations * Be permissive enough to facilitate integration with existing workflows and systems * Acknowledge that organisations will have different threat models, and interoperability should be equally possible in more open and more restrictive environments * As far as practical, maintain sensible parallels between coarse and fine-grained approaches -* Enable as fine-grained auth as is practical and sensible +* Enable authorization at a sufficiently fine-grained level for practical use cases * Must be possible to implement the design efficiently ## Considered Options From 7d3fe31a533c562ec69664b6eaf21659821a0cbf Mon Sep 17 00:00:00 2001 From: Sam Mesterton-Gibbons Date: Wed, 30 Jul 2025 14:17:33 +0100 Subject: [PATCH 26/33] appnote: Mention request context in authz Add a note that the context in which a request originates may be relevant for an authorization decision. --- docs/appnotes/0016-authorisation-in-tams-workflows.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/appnotes/0016-authorisation-in-tams-workflows.md b/docs/appnotes/0016-authorisation-in-tams-workflows.md index 0b0895d9..e2b67b5f 100644 --- a/docs/appnotes/0016-authorisation-in-tams-workflows.md +++ b/docs/appnotes/0016-authorisation-in-tams-workflows.md @@ -16,6 +16,7 @@ This might be appropriate for example in a newsroom, where staff are deliberatel Conversely a finer-grained approach may be required, where specific rules and policies are applied to each piece of content, and groups of users are carefully managed. This may be appropriate for example when working with a large number of third parties and freelancers in drama production, or when managing a large media archive of high-value content. +It may also be necessary to consider the context of a request: for example whether it originated from a managed device (such as an ingester or edit suite in a facility), from a particular network or in a particular setting: in some cases it may be appropriate to create "machine users" with broader permssions, and in others to have the user work interactively, using their own credentials. > [!NOTE] > Throughout this document the term "user" is used as a shorthand for all security principals, including human users, machine accounts, third-party SaaS integrations, etc. From 555f0dde40a54bebf49aaa30a73464697917cb6b Mon Sep 17 00:00:00 2001 From: James Sandford Date: Tue, 19 Aug 2025 10:25:53 +0100 Subject: [PATCH 27/33] Add missing note to 404 on objects endpoint GET --- api/TimeAddressableMediaStore.yaml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/api/TimeAddressableMediaStore.yaml b/api/TimeAddressableMediaStore.yaml index 0ee751bc..cfc70824 100644 --- a/api/TimeAddressableMediaStore.yaml +++ b/api/TimeAddressableMediaStore.yaml @@ -2348,7 +2348,7 @@ paths: "400": description: Bad request. Invalid query options. "404": - description: The requested media object does not exist. + description: The requested media object does not exist. 404 MUST be returned if the ID has been assigned via the storage endpoint, but not yet registered against a segment. /objects/{objectId}/instances: post: summary: Register a Media Object instance @@ -2399,7 +2399,7 @@ paths: "403": description: Forbidden. You do not have permission to modify this Media Object. "404": - description: The Media Object does not exist. + description: The Media Object does not exist. 404 MUST be returned if the ID has been assigned via the storage endpoint, but not yet registered against a segment. delete: summary: Delete a Media Object instance description: | @@ -2438,7 +2438,7 @@ paths: "403": description: Forbidden. You do not have permission to modify this Media Object. "404": - description: The requested Object ID in the path is invalid. + description: The requested Object ID in the path is invalid. 404 MUST be returned if the ID has been assigned via the storage endpoint, but not yet registered against a segment. /flow-delete-requests: head: From 896c00ad3211723751e09b077100c84181b61829 Mon Sep 17 00:00:00 2001 From: James Sandford Date: Tue, 19 Aug 2025 11:18:44 +0100 Subject: [PATCH 28/33] Fix typo --- docs/appnotes/0016-authorisation-in-tams-workflows.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/appnotes/0016-authorisation-in-tams-workflows.md b/docs/appnotes/0016-authorisation-in-tams-workflows.md index e2b67b5f..e6ccd04d 100644 --- a/docs/appnotes/0016-authorisation-in-tams-workflows.md +++ b/docs/appnotes/0016-authorisation-in-tams-workflows.md @@ -233,7 +233,7 @@ Where requests are rejected, they should return as follows: Implementations must evaluate permissions against webhook events themselves as well as the API's HTTP endpoints. A basic implementation may enumerate Flows and Sources a user has access to when creating/updating the webhook and use this to filter events. This approach is strongly discouraged as permissions may change over time. -It is recommended that implementations asses permissions on a per-event basis. +It is recommended that implementations assess permissions on a per-event basis. Implementations may use `auth_classes` tags in Flow/Source updated events to maintain a cache of Flow/Sources a webhook has read permissions for. Implementations should regularly inspect Source/Flow tags, via the HTTP API or other methods, to guard against missed events. Implementations should regularly check the user's permissions in the auth system for changes. From 5db39a0aebc1f7b5ca8ce05ca84452885c90bbdd Mon Sep 17 00:00:00 2001 From: James Sandford Date: Wed, 24 Sep 2025 10:10:12 +0100 Subject: [PATCH 29/33] Fixed typo in description of how to determine object permissions --- docs/appnotes/0016-authorisation-in-tams-workflows.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/appnotes/0016-authorisation-in-tams-workflows.md b/docs/appnotes/0016-authorisation-in-tams-workflows.md index e6ccd04d..d1139f02 100644 --- a/docs/appnotes/0016-authorisation-in-tams-workflows.md +++ b/docs/appnotes/0016-authorisation-in-tams-workflows.md @@ -214,7 +214,7 @@ This may be done via the `/sources/{sourceId}/tags/auth_classes` endpoint. Read, write, and delete permissions on individual objects may be determined by filtering returned flows on the object. This may be done by setting `flow_tag.auth_classes` to relevant claimed auth classes (e.g. auth classes with read permissions if read permissions on the object are to be verified). If `referenced_by_flows` in the returned data is empty, the request DOES NOT have the relevant permissions. -If `referenced_by_flows` in the returned data is empty, the request DOES have the relevant permissions. +If `referenced_by_flows` in the returned data is not empty, the request DOES have the relevant permissions. #### Webhooks From 3bffae85b5e28568cdaeefa6f6f86082327c262d Mon Sep 17 00:00:00 2001 From: James Sandford Date: Wed, 24 Sep 2025 16:17:23 +0100 Subject: [PATCH 30/33] Add new webhook and objects endpoints Note that object instance delete requires `write` as the API will not let a user remove all instances of an object directly. --- docs/appnotes/0016-authorisation-in-tams-workflows.md | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/docs/appnotes/0016-authorisation-in-tams-workflows.md b/docs/appnotes/0016-authorisation-in-tams-workflows.md index d1139f02..404d22f2 100644 --- a/docs/appnotes/0016-authorisation-in-tams-workflows.md +++ b/docs/appnotes/0016-authorisation-in-tams-workflows.md @@ -36,6 +36,9 @@ These are the recommended permissions (or "scopes" in OAuth 2.0): | `/service/storage-backends` | `HEAD`/`GET` ⚠️ | ✅ | ✅ | ✅ | ✅ | | `/service/webhooks` | `HEAD`/`GET` | ✅ | ✅ | | | | | `POST` | ✅ | | ✅ | | +| `/service/webhooks/{webhookId}` | `HEAD`/`GET` | ✅ | ✅ | | | +| | `PUT` | ✅ | | ✅ | | +| | `DELETE` ⚠️ | ✅ | | ✅ | | | `/sources` | `HEAD`/`GET` | ✅ | ✅ | | | | `/sources/{sourceId}` | `HEAD`/`GET` | ✅ | ✅ | | | | `/sources/{sourceId}/tags` | `HEAD`/`GET` | ✅ | ✅ | | | @@ -78,6 +81,8 @@ These are the recommended permissions (or "scopes" in OAuth 2.0): | | `DELETE` | ✅ | | | ✅ | | `/flows/{flowId}/storage` | `POST` | ✅ | | ✅ | | | `/objects/{objectId}` | `HEAD`/`GET` | ✅ | ✅ | | | +| `/objects/{objectId}/instances` | `POST` | ✅ | | ✅ | | +| | `DELETE` | ✅ | | ✅ | | | `/flow-delete-requests` | `HEAD`/`GET` ⚠️ | ✅ | | | | | `/flow-delete-requests/{request-id}` | `HEAD`/`GET` ⚠️ | ✅ | | | ✅ | @@ -152,6 +157,9 @@ For example - hiding collection relationships may result in clients deciding to | `/service/storage-backends` | `HEAD`/`GET` | Available to all | | `/service/webhooks` | `HEAD`/`GET` | Restrict returned data by adding list of claimed auth classes to `tag.auth_classes`. If the incoming request has `tag.auth_classes` set, the request must be processed with `tag.auth_classes` set to the intersection of the claimed auth classes and the provided list in `tag.auth_classes`. | | | `POST` | Request must have write permissions on the webhook being edited. If the request edits the `auth_classes` tag of a webhook, the request must have the permissions being edited. i.e. If the request adds or removes delete permissions for any group, it must have delete permissions on the webhook. If the request includes Source or Flow filters, the request must have read permissions on all Source or Flow IDs requested. Otherwise, reject. | +| `/service/webhooks/{webhookId}` | `HEAD`/`GET` | Request must have read permissions on {webhookId}. Otherwise reject. | +| | `PUT` | Request must have write permissions on {webhookId}. Otherwise, reject. | +| | `DELETE` | Request must have delete permissions on {webhookId}. Otherwise, reject. | | `/sources` | `HEAD`/`GET` | Restrict returned data by adding list of claimed auth classes to `tag.auth_classes`. If the incoming request has `tag.auth_classes` set, the request must be processed with `tag.auth_classes` set to the intersection of the claimed auth classes and the provided list in `tag.auth_classes`. | | `/sources/{sourceId}` | `HEAD`/`GET` | Request must have read permissions on {sourceId}. Otherwise reject. | | `/sources/{sourceId}/tags` | `HEAD`/`GET` | Request must have read permissions on {sourceId}. Otherwise reject. | @@ -194,6 +202,8 @@ For example - hiding collection relationships may result in clients deciding to | | `DELETE` | Request must have write permissions on {flowId}. Otherwise reject. | | `/flows/{flowId}/storage` | `POST` | Request must have write permissions on {flowId}. Otherwise reject. | | `/objects/{objectId}` | `HEAD`/`GET` | Restrict returned data in `referenced_by_flows` property by adding list of claimed auth classes to `flow_tag.auth_classes`. If the incoming request has `flow_tag.auth_classes` set, the request must be processed with `flow_tag.auth_classes` set to the intersection of the claimed auth classes and the provided list in `flow_tag.auth_classes`. | +| `/objects/{objectId}/instances` | `POST` | Request must have write permissions on {objectId}. Otherwise reject. | +| | `DELETE` | Request must have write permissions on {objectId}. Otherwise reject. | | `/flow-delete-requests` | `HEAD`/`GET` | Request must have admin permissions. Otherwise reject. | | `/flow-delete-requests/{request-id}` | `HEAD`/`GET` | Request must have delete permissions on the Delete Request's Flow ID. Otherwise reject. | From 1da95e7b849561e159e55e6fff95bcc4facc3ddb Mon Sep 17 00:00:00 2001 From: James Sandford Date: Wed, 24 Sep 2025 16:26:07 +0100 Subject: [PATCH 31/33] Tweak webhooks permissions following feedback, and to better represent the use of new endpoints. Note that coarse grained permissions for webhooks now require read, not write, permissions. This is to avoid a privilege escalation attack where a client with write, but not read, permissions uses websockets to perform read operations. --- docs/appnotes/0016-authorisation-in-tams-workflows.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/appnotes/0016-authorisation-in-tams-workflows.md b/docs/appnotes/0016-authorisation-in-tams-workflows.md index 404d22f2..25d61be2 100644 --- a/docs/appnotes/0016-authorisation-in-tams-workflows.md +++ b/docs/appnotes/0016-authorisation-in-tams-workflows.md @@ -37,8 +37,8 @@ These are the recommended permissions (or "scopes" in OAuth 2.0): | `/service/webhooks` | `HEAD`/`GET` | ✅ | ✅ | | | | | `POST` | ✅ | | ✅ | | | `/service/webhooks/{webhookId}` | `HEAD`/`GET` | ✅ | ✅ | | | -| | `PUT` | ✅ | | ✅ | | -| | `DELETE` ⚠️ | ✅ | | ✅ | | +| | `PUT` ⚠️ | ✅ | ✅ | | | +| | `DELETE` ⚠️ | ✅ | ✅ | | | | `/sources` | `HEAD`/`GET` | ✅ | ✅ | | | | `/sources/{sourceId}` | `HEAD`/`GET` | ✅ | ✅ | | | | `/sources/{sourceId}/tags` | `HEAD`/`GET` | ✅ | ✅ | | | @@ -156,9 +156,9 @@ For example - hiding collection relationships may result in clients deciding to | | `POST` | Request must have admin permissions. Otherwise reject. | | `/service/storage-backends` | `HEAD`/`GET` | Available to all | | `/service/webhooks` | `HEAD`/`GET` | Restrict returned data by adding list of claimed auth classes to `tag.auth_classes`. If the incoming request has `tag.auth_classes` set, the request must be processed with `tag.auth_classes` set to the intersection of the claimed auth classes and the provided list in `tag.auth_classes`. | -| | `POST` | Request must have write permissions on the webhook being edited. If the request edits the `auth_classes` tag of a webhook, the request must have the permissions being edited. i.e. If the request adds or removes delete permissions for any group, it must have delete permissions on the webhook. If the request includes Source or Flow filters, the request must have read permissions on all Source or Flow IDs requested. Otherwise, reject. | +| | `POST` | If the request includes Source or Flow filters, the request must have read permissions on all Source or Flow IDs requested. Otherwise, reject. Note that this endpoint only allows creation, not modification, of webhooks. | | `/service/webhooks/{webhookId}` | `HEAD`/`GET` | Request must have read permissions on {webhookId}. Otherwise reject. | -| | `PUT` | Request must have write permissions on {webhookId}. Otherwise, reject. | +| | `PUT` | Request must have write permissions on {webhookId}. If the request edits the `auth_classes` tag of a webhook, the request must have the permissions being edited. i.e. If the request adds or removes delete permissions for any group, it must have delete permissions on the webhook. Otherwise, reject. | | | `DELETE` | Request must have delete permissions on {webhookId}. Otherwise, reject. | | `/sources` | `HEAD`/`GET` | Restrict returned data by adding list of claimed auth classes to `tag.auth_classes`. If the incoming request has `tag.auth_classes` set, the request must be processed with `tag.auth_classes` set to the intersection of the claimed auth classes and the provided list in `tag.auth_classes`. | | `/sources/{sourceId}` | `HEAD`/`GET` | Request must have read permissions on {sourceId}. Otherwise reject. | From a246d955c5b083218f8208cb53313a18b5610043 Mon Sep 17 00:00:00 2001 From: James Sandford Date: Wed, 24 Sep 2025 16:45:45 +0100 Subject: [PATCH 32/33] Use style introduced in recent docs audit --- api/TimeAddressableMediaStore.yaml | 52 ++++++++----------- api/schemas/url-tag-list.json | 6 +++ .../0016-authorisation-in-tams-workflows.md | 28 +++++----- 3 files changed, 42 insertions(+), 44 deletions(-) create mode 100644 api/schemas/url-tag-list.json diff --git a/api/TimeAddressableMediaStore.yaml b/api/TimeAddressableMediaStore.yaml index cfc70824..2a168355 100644 --- a/api/TimeAddressableMediaStore.yaml +++ b/api/TimeAddressableMediaStore.yaml @@ -158,22 +158,20 @@ paths: in: query description: | Filter on webhooks that have a tag named {name} with a value in the given comma-seperated list of values. - The {name} could contain escaped characters to allow it to be used in a - URL. + The {name} could contain escaped characters to allow it to be used in a URL. Where the tag's value is a string, at least one of the given values will match. Where the tag's value is an array, at least one value in the array will match at least one of the given values. Partial string matches of the values are not valid. schema: - type: string - pattern: ^([^,]+(,[^,]+)*)?$ + $ref: 'schemas/url-tag-list.json' - name: tag_exists.{name} in: query description: | - Filter on webhooks that have a tag named {name} regardless of value. The - {name} could contain escaped characters to allow it to be used in a - URL. If set to true then the presence of the tag is filtered for. If set - to false then its absence is. If left out then no filtering on tag presence - is performed. + Filter on webhooks that have a tag named {name} regardless of value. + The {name} could contain escaped characters to allow it to be used in a URL. + If set to true then the presence of the tag is filtered for. + If set to false then its absence is. + If left out then no filtering on tag presence is performed. schema: type: boolean - $ref: '#/components/parameters/trait_resource_paged_key' @@ -214,22 +212,20 @@ paths: in: query description: | Filter on webhooks that have a tag named {name} with a value in the given comma-seperated list of values. - The {name} could contain escaped characters to allow it to be used in a - URL. + The {name} could contain escaped characters to allow it to be used in a URL. Where the tag's value is a string, at least one of the given values will match. Where the tag's value is an array, at least one value in the array will match at least one of the given values. Partial string matches of the values are not valid. schema: - type: string - pattern: ^([^,]+(,[^,]+)*)?$ + $ref: 'schemas/url-tag-list.json' - name: tag_exists.{name} in: query description: | - Filter on webhooks that have a tag named {name} regardless of value. The - {name} could contain escaped characters to allow it to be used in a - URL. If set to true then the presence of the tag is filtered for. If set - to false then its absence is. If left out then no filtering on tag presence - is performed. + Filter on webhooks that have a tag named {name} regardless of value. + The {name} could contain escaped characters to allow it to be used in a URL. + If set to true then the presence of the tag is filtered for. + If set to false then its absence is. + If left out then no filtering on tag presence is performed. schema: type: boolean - $ref: '#/components/parameters/trait_resource_paged_key' @@ -420,8 +416,7 @@ paths: Where the tag's value is an array, at least one value in the array will match at least one of the given values. Partial string matches of the values are not valid. schema: - type: string - pattern: ^([^,]+(,[^,]+)*)?$ + $ref: 'schemas/url-tag-list.json' - name: tag_exists.{name} in: query description: | @@ -482,8 +477,7 @@ paths: Where the tag's value is an array, at least one value in the array will match at least one of the given values. Partial string matches of the values are not valid. schema: - type: string - pattern: ^([^,]+(,[^,]+)*)?$ + $ref: 'schemas/url-tag-list.json' - name: tag_exists.{name} in: query description: | @@ -872,8 +866,7 @@ paths: Where the tag's value is an array, at least one value in the array will match at least one of the given values. Partial string matches of the values are not valid. schema: - type: string - pattern: ^([^,]+(,[^,]+)*)?$ + $ref: 'schemas/url-tag-list.json' - name: tag_exists.{name} in: query description: | @@ -960,8 +953,7 @@ paths: Where the tag's value is an array, at least one value in the array will match at least one of the given values. Partial string matches of the values are not valid. schema: - type: string - pattern: ^([^,]+(,[^,]+)*)?$ + $ref: 'schemas/url-tag-list.json' - name: tag_exists.{name} in: query description: | @@ -2249,7 +2241,7 @@ paths: "400": $ref: '#/components/responses/trait_resource_info_head_400' "404": - description: The requested Media Object does not exist. 404 MUST be returned if the ID has been assigned via the storage endpoint, but not yet registered against a Flow Segment. + description: The requested Media Object does not exist. 404 MUST be returned if the ID has been assigned via the [`/flows/{flowId}/storage`](#/operations/POST_flows-flowId-storage), but not yet registered against a Flow Segment. get: summary: Media Object Information description: | @@ -2348,7 +2340,7 @@ paths: "400": description: Bad request. Invalid query options. "404": - description: The requested media object does not exist. 404 MUST be returned if the ID has been assigned via the storage endpoint, but not yet registered against a segment. + description: The requested media object does not exist. 404 MUST be returned if the ID has been assigned via the [`/flows/{flowId}/storage`](#/operations/POST_flows-flowId-storage), but not yet registered against a Flow Segment. /objects/{objectId}/instances: post: summary: Register a Media Object instance @@ -2399,7 +2391,7 @@ paths: "403": description: Forbidden. You do not have permission to modify this Media Object. "404": - description: The Media Object does not exist. 404 MUST be returned if the ID has been assigned via the storage endpoint, but not yet registered against a segment. + description: The Media Object does not exist. 404 MUST be returned if the ID has been assigned via the [`/flows/{flowId}/storage`](#/operations/POST_flows-flowId-storage), but not yet registered against a Flow Segment. delete: summary: Delete a Media Object instance description: | @@ -2438,7 +2430,7 @@ paths: "403": description: Forbidden. You do not have permission to modify this Media Object. "404": - description: The requested Object ID in the path is invalid. 404 MUST be returned if the ID has been assigned via the storage endpoint, but not yet registered against a segment. + description: The requested Object ID in the path is invalid. 404 MUST be returned if the ID has been assigned via the [`/flows/{flowId}/storage`](#/operations/POST_flows-flowId-storage), but not yet registered against a Flow Segment. /flow-delete-requests: head: diff --git a/api/schemas/url-tag-list.json b/api/schemas/url-tag-list.json new file mode 100644 index 00000000..7cbd6bb2 --- /dev/null +++ b/api/schemas/url-tag-list.json @@ -0,0 +1,6 @@ +{ + "title": "Query String Tag value list", + "description": "A list of tag values, formatted for use in query string parameters", + "type": "string", + "pattern": "^([^,]+(,[^,]+)*)?$" +} \ No newline at end of file diff --git a/docs/appnotes/0016-authorisation-in-tams-workflows.md b/docs/appnotes/0016-authorisation-in-tams-workflows.md index 25d61be2..faaa6c5c 100644 --- a/docs/appnotes/0016-authorisation-in-tams-workflows.md +++ b/docs/appnotes/0016-authorisation-in-tams-workflows.md @@ -4,14 +4,14 @@ Media workflows often contain sensitive or high-value content, and media organisations need to effectively manage access to that content across their estate. That requires suitable approaches across both authentication (identifying the user) as discussed in [ADR0028](../adr/0028-authentication-methods.md), and also authorisation (deciding what the user can do), which is discussed in [ADR035](../adr/0035-fine-grained-auth.md) and implemented here. -In general, store implementations, and the organisations that deploy them, are free to define how the authorisation model works based on their needs, however this Application Note provides some guidelines and a starting point. +In general, service implementations, and the organisations that deploy them, are free to define how the authorisation model works based on their needs, however this Application Note provides some guidelines and a starting point. It is recommended to follow these guidelines where possible to aid in interoperability between TAMS components. ## Overall Principles Implementers should consider the material they need to protect, the nature of their business and their threat model when deciding how to build authorisation into TAMS-based media workflows. -For some organisations a coarse-grained approach is sufficient: for example allowing groups of users to have read- or write-access to a store or large blocks of content. +For some organisations a coarse-grained approach is sufficient: for example allowing groups of users to have read- or write-access to a service instance or large blocks of content. This might be appropriate for example in a newsroom, where staff are deliberately enabled to work together and access each other's material. Conversely a finer-grained approach may be required, where specific rules and policies are applied to each piece of content, and groups of users are carefully managed. @@ -119,7 +119,7 @@ Future iterations of these proposals may elevate ABAC attributes to a specific f In practical TAMS solutions, ABAC could look like defining an `auth_classes` tag. A permissions system then defines policies that evaluates permissions based on to those `auth_classes` and a request's claimed OAuth scopes. -For example, consider a store shared by multiple teams from the News and Sport production teams of an organisation. +For example, consider a service instance shared by multiple teams from the News and Sport production teams of an organisation. Each team have the ability to read and write their own content, and no access to the other team's content. However in some cases it is necessary to share a particular Source (e.g. to work on a shared story) to the other team. @@ -198,7 +198,7 @@ For example - hiding collection relationships may result in clients deciding to | | `PUT` | Request must have write permissions on {flowId}. Otherwise reject. | | | `DELETE` | Request must have write permissions on {flowId}. Otherwise reject. | | `/flows/{flowId}/segments` | `HEAD`/`GET` | Request must have read permissions on {flowId}. Otherwise reject. | -| | `POST` | Request must have write permissions on {flowId}, and either this must be the first registration of the object(s) (i.e. `/objects/{objectId}` returns 404) or the request must have read access to the object(s) being written. Otherwise reject. | +| | `POST` | Request must have write permissions on {flowId}, and either this must be the first registration of the Media Object(s) (i.e. `/objects/{objectId}` returns 404) or the request must have read access to the Media Object(s) being written. Otherwise reject. | | | `DELETE` | Request must have write permissions on {flowId}. Otherwise reject. | | `/flows/{flowId}/storage` | `POST` | Request must have write permissions on {flowId}. Otherwise reject. | | `/objects/{objectId}` | `HEAD`/`GET` | Restrict returned data in `referenced_by_flows` property by adding list of claimed auth classes to `flow_tag.auth_classes`. If the incoming request has `flow_tag.auth_classes` set, the request must be processed with `flow_tag.auth_classes` set to the intersection of the claimed auth classes and the provided list in `flow_tag.auth_classes`. | @@ -211,18 +211,18 @@ For example - hiding collection relationships may result in clients deciding to #### Flows -Read, write, and delete permissions on individual flows may be determined via auth classes listed in the `auth_classes` tag on the flow. +Read, write, and delete permissions on individual Flows may be determined via auth classes listed in the `auth_classes` tag on the Flow. This may be done via the `/flows/{flowId}/tags/auth_classes` endpoint. #### Sources -Read, write, and delete permissions on individual sources may be determined via auth classes listed in the `auth_classes` tag on the source. +Read, write, and delete permissions on individual Sources may be determined via auth classes listed in the `auth_classes` tag on the Source. This may be done via the `/sources/{sourceId}/tags/auth_classes` endpoint. -#### Objects +#### Media Objects -Read, write, and delete permissions on individual objects may be determined by filtering returned flows on the object. -This may be done by setting `flow_tag.auth_classes` to relevant claimed auth classes (e.g. auth classes with read permissions if read permissions on the object are to be verified). +Read, write, and delete permissions on individual Media Objects may be determined by filtering returned Flows on the Media Object. +This may be done by setting `flow_tag.auth_classes` to relevant claimed auth classes (e.g. auth classes with read permissions if read permissions on the Media Object are to be verified). If `referenced_by_flows` in the returned data is empty, the request DOES NOT have the relevant permissions. If `referenced_by_flows` in the returned data is not empty, the request DOES have the relevant permissions. @@ -278,14 +278,14 @@ As a result, the process of authorising a request is: 2. Read the user's claimed scopes from their provided token 3. Request a decision from the permissions system based on those data 4. (Write requests only): Check whether the request would modify the special `auth_classes` tag, and confirm the user has permission to make that modification -5. (Flow segment write requests only): Check if the object already exists in the store using the `/objects` endpoint, and if it does, confirm the user would have access to read it +5. (Flow Segment write requests only): Check if the Media Object already exists in the service instance using the `/objects` endpoint, and if it does, confirm the user would have access to read it 6. (Write requests only): Propagate any changes to the `auth_classes` tag to Flows and Sources collected by this one ## Where to Enforce Authorisation Some consideration should be given for where to apply the authorisation step, depending on how TAMS is deployed and integrated. For example a TAMS instance could be deployed with fine-grained authorisation support, and used directly by systems across an organisation. -In this case it would make sense to treat all clients of that TAMS instance as identical from an authentication/authorisation perspective: for example a user operating an NLE would be expected to provide suitable authorised credentials, but so too would the organisations MAM when it wants access to the store. +In this case it would make sense to treat all clients of that TAMS instance as identical from an authentication/authorisation perspective: for example a user operating an NLE would be expected to provide suitable authorised credentials, but so too would the organisations MAM when it wants access to the service instance. Another deployment approach might see a MAM or other tool expose a TAMS API interface itself, which is proxied through to some simpler backing store. In this case the MAM might manage and enforce other policies and rules around access to content, so it would make more sense to do the same in the TAMS API interface, and then use the MAM's own credentials to access the backing TAMS instance. @@ -296,13 +296,13 @@ In this case the MAM might manage and enforce other policies and rules around ac The model described above allows access control at the Source/Flow level. Some use cases may require finer grained control. -This may be achieved by creating a new flow with the relevant permissions that refers to the Objects of interest. +This may be achieved by creating a new Flow with the relevant permissions that refers to the Objects of interest. Caution should be taken where the boundary timestamps land partway through an Object. Where the material around the boundaries is sensitive, new trimmed Objects should be created at the boundaries that remove content outside the permitted range. ### Global read access -Some organisations/implementations may choose to provide read access to all Sources and Flows to promote content re-use, and reduce the writing of duplicate content to the store. +Some organisations/implementations may choose to provide read access to all Sources and Flows to promote content re-use, and reduce the writing of duplicate content to the service instance. Implementations may provide this feature by either adding default groups to Sources and Flows that provide appropriate read access to users, or by using more permissive auth logic. ### Permissions propagation @@ -326,7 +326,7 @@ In these cases, a matching "deny" class takes precedent over an "allow" class. ## Future Work -The model described above allows for more use cases than coarse-grained RBAC: especially use cases where multiple tenants share a single store. +The model described above allows for more use cases than coarse-grained RBAC: especially use cases where multiple tenants share a single service instance. However it would be useful to allow more attributes to be used in rules: for example allowing write access to the tags of Sources/Flows but not other properties. One of the areas noted in [ADR0028: Authentication Methods](../adr/0028-authentication-methods.md) is being able to issue credentials restricted to a limited subset of Sources or Flows, which must also be supported by the authorisation system. From 01f21403d7c5ebaa7aa5aebe678a7e6f5b3c18a0 Mon Sep 17 00:00:00 2001 From: James Sandford Date: Thu, 25 Sep 2025 11:21:51 +0100 Subject: [PATCH 33/33] Require read permissions on sources & flows when editing webhook filters --- docs/appnotes/0016-authorisation-in-tams-workflows.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/appnotes/0016-authorisation-in-tams-workflows.md b/docs/appnotes/0016-authorisation-in-tams-workflows.md index faaa6c5c..e110f929 100644 --- a/docs/appnotes/0016-authorisation-in-tams-workflows.md +++ b/docs/appnotes/0016-authorisation-in-tams-workflows.md @@ -158,7 +158,7 @@ For example - hiding collection relationships may result in clients deciding to | `/service/webhooks` | `HEAD`/`GET` | Restrict returned data by adding list of claimed auth classes to `tag.auth_classes`. If the incoming request has `tag.auth_classes` set, the request must be processed with `tag.auth_classes` set to the intersection of the claimed auth classes and the provided list in `tag.auth_classes`. | | | `POST` | If the request includes Source or Flow filters, the request must have read permissions on all Source or Flow IDs requested. Otherwise, reject. Note that this endpoint only allows creation, not modification, of webhooks. | | `/service/webhooks/{webhookId}` | `HEAD`/`GET` | Request must have read permissions on {webhookId}. Otherwise reject. | -| | `PUT` | Request must have write permissions on {webhookId}. If the request edits the `auth_classes` tag of a webhook, the request must have the permissions being edited. i.e. If the request adds or removes delete permissions for any group, it must have delete permissions on the webhook. Otherwise, reject. | +| | `PUT` | Request must have write permissions on {webhookId}. If the request includes Source or Flow filters, the request must have read permissions on all Source or Flow IDs requested. If the request edits the `auth_classes` tag of a webhook, the request must have the permissions being edited. i.e. If the request adds or removes delete permissions for any group, it must have delete permissions on the webhook. Otherwise, reject. | | | `DELETE` | Request must have delete permissions on {webhookId}. Otherwise, reject. | | `/sources` | `HEAD`/`GET` | Restrict returned data by adding list of claimed auth classes to `tag.auth_classes`. If the incoming request has `tag.auth_classes` set, the request must be processed with `tag.auth_classes` set to the intersection of the claimed auth classes and the provided list in `tag.auth_classes`. | | `/sources/{sourceId}` | `HEAD`/`GET` | Request must have read permissions on {sourceId}. Otherwise reject. |