From 4479819347e65a358e72eaa8b25658dc80575937 Mon Sep 17 00:00:00 2001 From: Sam Mesterton-Gibbons Date: Wed, 1 Oct 2025 16:55:01 +0100 Subject: [PATCH 1/8] adr: Add ADR on fine grained authorisation Co-authored-by: James Sandford --- docs/README.md | 1 + docs/adr/0035-fine-grained-auth.md | 178 +++++++++++++++++++++++++++++ 2 files changed, 179 insertions(+) create mode 100644 docs/adr/0035-fine-grained-auth.md diff --git a/docs/README.md b/docs/README.md index 38706d3f..e0eaa713 100644 --- a/docs/README.md +++ b/docs/README.md @@ -68,6 +68,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 | | [0036](./adr/0036-specifying-partial-segment-usage.md) | Specifying partial segment usage | | [0037](./adr/0037-improve-webhooks.md) | Proposal for improvements to the Webhooks endpoints | | [0038](./adr/0038-improved-storage-management.md) | Improved Storage Management | diff --git a/docs/adr/0035-fine-grained-auth.md b/docs/adr/0035-fine-grained-auth.md new file mode 100644 index 00000000..6107afe8 --- /dev/null +++ b/docs/adr/0035-fine-grained-auth.md @@ -0,0 +1,178 @@ +--- +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 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 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. + +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 authorization at a sufficiently fine-grained level for practical use cases +* 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 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 - Single Tag with a list value +* Option 4b: 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 4a: 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 3b does not preclude the solution developed being implemented as described in Option 3a. +The choice of Option 4a allows for us to experiment with, and refine our approach to fine-grained auth. +Once our approach is mature, we may wish to consider implementing Option 1c in [ADR0040](./0040-tag-usability-enhancements.md) 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. +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 +* 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 +* 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 + +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 +* 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 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 +* 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 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. + +* 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 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 + +### Option 4a: Auth attributes - Single Tag with a list value + +Use the tag list support (added with [ADR0040](./0040-tag-usability-enhancements.md)) 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 beyond ADR0040 +* Good, because it makes use of tags for this purpose intuitive +* Good, because it requires very few API calls when carrying out evaluation of permissions on endpoints + +### Option 4b: 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). From 7f3644e8b3b90609415ff3280a8ec193c7ecc737 Mon Sep 17 00:00:00 2001 From: Sam Mesterton-Gibbons Date: Wed, 1 Oct 2025 16:55:04 +0100 Subject: [PATCH 2/8] appnote: Add AppNote describing authorisation Adds an application note describing authorisation in TAMS. Note that this is the state of the appnote at 01f2140 in https://github.com/bbc/tams/pull/115/ - at that point it was reworked into multiple PRs and the history flattened. Co-authored-by: James Sandford --- docs/README.md | 1 + docs/appnotes/0003-tag-names.md | 18 + .../0016-authorisation-in-tams-workflows.md | 343 ++++++++++++++++++ 3 files changed, 362 insertions(+) create mode 100644 docs/appnotes/0016-authorisation-in-tams-workflows.md diff --git a/docs/README.md b/docs/README.md index e0eaa713..bb3c89a8 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/0003-tag-names.md b/docs/appnotes/0003-tag-names.md index 69918f1e..0beaa073 100644 --- a/docs/appnotes/0003-tag-names.md +++ b/docs/appnotes/0003-tag-names.md @@ -195,6 +195,15 @@ 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. +### 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 auth classes used to derive permissions on the Flow. + +No known implementations yet. + ## Known Source Tags ### hls_exclude @@ -206,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. + +### 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 auth classes used to derive permissions on the Source. + +No known implementations yet. 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..22675f0f --- /dev/null +++ b/docs/appnotes/0016-authorisation-in-tams-workflows.md @@ -0,0 +1,343 @@ +# 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 in [ADR035](../adr/0035-fine-grained-auth.md) and implemented here. +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 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. +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. + +## 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. + +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` | ✅ | | ✅ | | +| `/service/webhooks/{webhookId}` | `HEAD`/`GET` | ✅ | ✅ | | | +| | `PUT` ⚠️ | ✅ | ✅ | | | +| | `DELETE` ⚠️ | ✅ | ✅ | | | +| `/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` | ✅ | ✅ | | | +| `/objects/{objectId}/instances` | `POST` | ✅ | | ✅ | | +| | `DELETE` | ✅ | | ✅ | | +| `/flow-delete-requests` | `HEAD`/`GET` ⚠️ | ✅ | | | | +| `/flow-delete-requests/{request-id}` | `HEAD`/`GET` ⚠️ | ✅ | | | ✅ | + +Key for the listing: + +- ✅: Allow method with this OAuth scope +- ⚠️: 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` + +To implement the authorisation, the authorisation server checks the requested scopes against the user's access when issuing a token. +The TAMS server, or its auth proxy, rejects requests without appropriate scopes. + +## 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 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. + +### Scopes and Auth Classes + +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 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. + +| 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 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 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. + +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. + +| 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 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 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. | +| `/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/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 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. | +| `/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/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. | +| `/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 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`: return 404 if adding this list causes none to be returned. If the incoming request has `flow_tag.auth_classes` set, the request should be processed with `flow_tag.auth_classes` set to the claimed classes first, and if any result is returned, then to the intersection of the claimed auth classes and the provided list in `flow_tag.auth_classes` (which should return an empty list, rather than a 404 ). | +| `/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. | + +### Determining base permissions + +#### Flows + +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. +This may be done via the `/sources/{sourceId}/tags/auth_classes` endpoint. + +#### Media Objects + +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. + +#### Webhooks + +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 + +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 + +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 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. +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 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. + +### 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. + +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 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. + +As a result, the process of authorising a request is: + +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 +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 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. + +## Use cases and additional optional functionality + +### Creating new Flows and Sources + +Along with granting access to existing Flows and Sources, some method needs to exist to decide whether a user can _create_ an new Flow or Source. +Depending on the model in use, it may be a requirement to specify the permissions of that resource at creation time: for example setting an `auth_classes` tag to a subset of the user's own groups, and rejecting requests that do not include one. +Alternatively an implementation may provide some signalling of a default set of permissions that should be applied when none is given: for example a user could have a default group or project, or an approach where users can own content themselves, and then share it as needed. +In addition, if a new Flow is created of an existing Source, it may be appropriate to set default permissions to match those of the Source. + +For systems that create resources on behalf of a user, the same approach could be taken, or placeholder resources could be created and assigned to that system: for example creating an empty Flow and then specifying the Flow ID to an ingester. + +### Providing access to a subset of a Flow's timerange + +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. +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 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 + +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 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. +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 c4598412ad3ebbf1010b06197825e42fcdd23b45 Mon Sep 17 00:00:00 2001 From: Sam Mesterton-Gibbons Date: Wed, 1 Oct 2025 16:55:07 +0100 Subject: [PATCH 3/8] api: Make object endpoint behaviour clearer Clarifies that object ID/URL pairs that were never registered as Flow Segments should not be returned in the listing. Co-authored-by: James Sandford --- api/TimeAddressableMediaStore.yaml | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/api/TimeAddressableMediaStore.yaml b/api/TimeAddressableMediaStore.yaml index c6bca505..f71c972f 100644 --- a/api/TimeAddressableMediaStore.yaml +++ b/api/TimeAddressableMediaStore.yaml @@ -2239,7 +2239,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 [`/flows/{flowId}/storage`](#/operations/POST_flows-flowId-storage), but not yet registered against a Flow Segment. get: summary: Media Object Information description: | @@ -2338,7 +2338,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 [`/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 @@ -2389,7 +2389,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 [`/flows/{flowId}/storage`](#/operations/POST_flows-flowId-storage), but not yet registered against a Flow Segment. delete: summary: Delete a Media Object instance description: | @@ -2428,7 +2428,8 @@ 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 [`/flows/{flowId}/storage`](#/operations/POST_flows-flowId-storage), but not yet registered against a Flow Segment. + /flow-delete-requests: head: summary: List Flow Delete Requests From 3698330a3b9f3483ae3aa2c3b4e9d52c052c965c Mon Sep 17 00:00:00 2001 From: Sam Mesterton-Gibbons Date: Wed, 1 Oct 2025 16:55:09 +0100 Subject: [PATCH 4/8] readme: Mention authorisation appnote Co-Authored-By: James Sandford --- README.md | 40 +++++++++++++++------------------------- 1 file changed, 15 insertions(+), 25 deletions(-) diff --git a/README.md b/README.md index b6c775c7..2b5e3615 100644 --- a/README.md +++ b/README.md @@ -183,30 +183,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. @@ -226,7 +203,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. From 314b900b272adeb6a64702ca9f9f95f59aa8abb7 Mon Sep 17 00:00:00 2001 From: Sam Mesterton-Gibbons Date: Wed, 1 Oct 2025 16:55:14 +0100 Subject: [PATCH 5/8] appnote: Simplify AuthZ appnote Restructures to move the details of the `auth_classes` tag based implementation out of the main application note, and instead talk in more general terms about how to implement it. --- .../0016-authorisation-in-tams-workflows.md | 286 ++++++++---------- 1 file changed, 125 insertions(+), 161 deletions(-) diff --git a/docs/appnotes/0016-authorisation-in-tams-workflows.md b/docs/appnotes/0016-authorisation-in-tams-workflows.md index 22675f0f..4242cd63 100644 --- a/docs/appnotes/0016-authorisation-in-tams-workflows.md +++ b/docs/appnotes/0016-authorisation-in-tams-workflows.md @@ -26,7 +26,8 @@ It may also be necessary to consider the context of a request: for example wheth 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. -These are the recommended permissions (or "scopes" in OAuth 2.0): +These are the recommended permissions (or "scopes" in OAuth 2.0). +This is a deliberately minimal set: for example implementations may wish to add a specific permission for managing webhooks or object instances. | Endpoint | Method | `tams-api/admin` | `tams-api/read` | `tams-api/write` | `tams-api/delete` | | ------------------------------------ | --------------- | ---------------- | --------------- | ---------------- | ----------------- | @@ -105,47 +106,30 @@ The TAMS server, or its auth proxy, rejects requests without appropriate scopes. ## 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. +Broadly those permissions can be aligned along the lines of the scopes in the coarse-grained model: granting read, write and delete access separately. +The policy that assigns read/write/delete permissions to users depends on the needs of the organisation, and may be based on their roles or group memberships, or use an Attribute-Based Access Control (ABAC) approach where other factors such as position in the organisation, time-of-day or location are considered. -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 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. - -### Scopes and Auth Classes - -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 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. +This section will describe the logic that needs to be applied to each endpoint depending on the user's resource permissions, along with some considerations for implementing fine-grained authorisation. +Later in the document the [Implementation using Tags](#implementation-using-tags) section describes a simple approach that could be taken using an `auth_classes` tag, and how a reverse proxy may be implemented to enforce user access accordingly. -| 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. | +To implement this, solutions such as [Amazon Verified Permissions](https://aws.amazon.com/verified-permissions/) and [Permify](https://github.com/Permify/permify) could be used 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), and computing whether to allow the request. +They may also return the permitted resource attributes for a given user of a given endpoint to aid filtering results in Flow listings, for example. +It may be necessary to add additional attributes specific to authorisation (such as the owner of a piece of content), which could be stored as tags (e.g. `auth_classes`) or another media library database. +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. ### 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 admins have permission to execute all methods on all endpoints. +It is assumed that along with read/write and delete, an "administrator" permission exists that can 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 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. 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. +For example, a Source's 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. @@ -153,157 +137,89 @@ For example - hiding collection relationships may result in clients deciding to | ------------------------------------ | ------------ | --------------------------------------------------------------------------------------- | | `/` | `HEAD`/`GET` | Available to all | | `/service` | `HEAD`/`GET` | Available to all | -| | `POST` | Request must have admin permissions. Otherwise reject. | +| | `POST` | Request must have admin permissions. | | `/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`. | +| `/service/webhooks` | `HEAD`/`GET` | Restrict returned data to only the webhooks that the request has read permission on. | | | `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 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. | -| `/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/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 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. | -| `/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/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. | -| `/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. | +| `/service/webhooks/{webhookId}` | `HEAD`/`GET` | Request must have read permissions on {webhookId}. | +| | `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. | +| | `DELETE` | Request must have delete permissions on {webhookId}. | +| `/sources` | `HEAD`/`GET` | Restrict the returned data to only the Sources that the request has read permission on. | +| `/sources/{sourceId}` | `HEAD`/`GET` | Request must have read permissions on {sourceId}. | +| `/sources/{sourceId}/tags` | `HEAD`/`GET` | Request must have read permissions on {sourceId}. | +| `/sources/{sourceId}/tags/{name}` | `HEAD`/`GET` | Request must have read permissions on {sourceId}. | +| | `PUT` | Request must have write permissions on {sourceId}. | +| | `DELETE` | Request must have write permissions on {sourceId}. | +| `/sources/{sourceId}/description` | `HEAD`/`GET` | Request must have read permissions on {sourceId}. | +| | `PUT` | Request must have write permissions on {sourceId}. | +| | `DELETE` | Request must have write permissions on {sourceId}. | +| `/sources/{sourceId}/label` | `HEAD`/`GET` | Request must have read permissions on {sourceId}. | +| | `PUT` | Request must have write permissions on {sourceId}. | +| | `DELETE` | Request must have write permissions on {sourceId}. | +| `/flows` | `HEAD`/`GET` | Restrict returned data to only the Flows that the request has read permission on. | +| `/flows/{flowId}` | `HEAD`/`GET` | Request must have read permissions on {flowID}. | +| | `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 neither {flowId} nor the Source ID exist, allow if the request has create permission (see below). If {flowId} already exists, request must have write permissions on {flowId}. | +| | `DELETE` | Request must have delete permissions on {flowId}. | +| `/flows/{flowId}/tags` | `HEAD`/`GET` | Request must have read permissions on {flowId}. | +| `/flows/{flowId}/tags/{name}` | `HEAD`/`GET` | Request must have read permissions on {flowId}. | +| | `PUT` | Request must have write permissions on {flowId}. | +| | `DELETE` | Request must have write permissions on {flowId}. | +| `/flows/{flowId}/description` | `HEAD`/`GET` | Request must have read permissions on {flowId}. | +| | `PUT` | Request must have write permissions on {flowId}. | +| | `DELETE` | Request must have write permissions on {flowId}. | +| `/flows/{flowId}/label` | `HEAD`/`GET` | Request must have read permissions on {flowId}. | +| | `PUT` | Request must have write permissions on {flowId}. | +| | `DELETE` | Request must have write permissions on {flowId}. | +| `/flows/{flowId}/read_only` | `HEAD`/`GET` | Request must have read permissions on {flowId}. | +| | `PUT` | Request must have write permissions on {flowId}. | +| `/flows/{flowId}/flow_collection` | `HEAD`/`GET` | Request must have read permissions on {flowId}. | +| | `PUT` | Request must have write permissions on {flowId}. | +| | `DELETE` | Request must have write permissions on {flowId}. | +| `/flows/{flowId}/max_bit_rate` | `HEAD`/`GET` | Request must have read permissions on {flowId}. | +| | `PUT` | Request must have write permissions on {flowId}. | +| | `DELETE` | Request must have write permissions on {flowId}. | +| `/flows/{flowId}/avg_bit_rate` | `HEAD`/`GET` | Request must have read permissions on {flowId}. | +| | `PUT` | Request must have write permissions on {flowId}. | +| | `DELETE` | Request must have write permissions on {flowId}. | +| `/flows/{flowId}/segments` | `HEAD`/`GET` | Request must have read permissions on {flowId}. | | | `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`: return 404 if adding this list causes none to be returned. If the incoming request has `flow_tag.auth_classes` set, the request should be processed with `flow_tag.auth_classes` set to the claimed classes first, and if any result is returned, then to the intersection of the claimed auth classes and the provided list in `flow_tag.auth_classes` (which should return an empty list, rather than a 404 ). | -| `/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. | - -### Determining base permissions - -#### Flows - -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. -This may be done via the `/sources/{sourceId}/tags/auth_classes` endpoint. - -#### Media Objects - -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. - -#### Webhooks +| | `DELETE` | Request must have write permissions on {flowId}. | +| `/flows/{flowId}/storage` | `POST` | Request must have write permissions on {flowId}. | +| `/objects/{objectId}` | `HEAD`/`GET` | Restrict returned data in `referenced_by_flows` property to only the Flows that the request has read access to. If the request has read access to no Flows of this object, return 404, however if the request has access but all of the Flows have been filtered out, return the response with an empty `referenced_by_flows` list. | +| `/objects/{objectId}/instances` | `POST` | Request must have write permissions on {objectId}. | +| | `DELETE` | Request must have write permissions on {objectId}. | +| `/flow-delete-requests` | `HEAD`/`GET` | Request must have admin permissions. | +| `/flow-delete-requests/{request-id}` | `HEAD`/`GET` | Request must have delete permissions on the Delete Request's Flow ID. | -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 +#### 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 +#### Creating new Flows and Sources -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 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. -If permissions changes are observed, the set of permissions used to evaluate against events should only ever be reduced in scope and never increased. +Along with granting access to existing Flows and Sources, some method needs to exist to decide whether a user can _create_ an new Flow or Source. +Depending on the model in use, it may be a requirement to specify the permissions of that resource at creation time: for example setting an `auth_classes` tag to a subset of the user's own groups, and rejecting requests that do not include one. +Alternatively an implementation may provide some signalling of a default set of permissions that should be applied when none is given: for example a user could have a default group or project, or an approach where users can own content themselves, and then share it as needed. + +For systems that create resources on behalf of a user, the same approach could be taken, or placeholder resources could be created and assigned to that system: for example creating an empty Flow and then specifying the Flow ID to an ingester. -### Adding Flows to Sources +#### Adding Flows to existing 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 SHOULD 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 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. -### 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. - -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 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. - -As a result, the process of authorising a request is: - -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 -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 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. - ## Use cases and additional optional functionality -### Creating new Flows and Sources - -Along with granting access to existing Flows and Sources, some method needs to exist to decide whether a user can _create_ an new Flow or Source. -Depending on the model in use, it may be a requirement to specify the permissions of that resource at creation time: for example setting an `auth_classes` tag to a subset of the user's own groups, and rejecting requests that do not include one. -Alternatively an implementation may provide some signalling of a default set of permissions that should be applied when none is given: for example a user could have a default group or project, or an approach where users can own content themselves, and then share it as needed. -In addition, if a new Flow is created of an existing Source, it may be appropriate to set default permissions to match those of the Source. - -For systems that create resources on behalf of a user, the same approach could be taken, or placeholder resources could be created and assigned to that system: for example creating an empty Flow and then specifying the Flow ID to an ingester. - ### Providing access to a subset of a Flow's timerange -The model described above allows access control at the Source/Flow level. +The fine-grained authorisation model described above considers 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. Caution should be taken where the boundary timestamps land partway through an Object. @@ -314,15 +230,22 @@ 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 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. +### Fine-grained authorisation and webhook events + +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 assess permissions on a per-event basis, and cache this information as necessary in their internal implementation. + ### 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. +A basic implementation could set permissions on a new Source with those in the Flow that results in its creation. +This is consistent with 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. +The propagation of these permissions may need to 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 those permissions. 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. @@ -333,6 +256,47 @@ Propagation should also be triggered when a new Source/Flow is added to a Source 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. +## 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 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. + +## Implementation using Tags + +In practical TAMS solutions, implementing the [Finer Grained Authorisation](#finer-grained-authorisation) described above could look like defining an `auth_classes` tag, and assigning a "class" as a resource attribute for ABAC. + +An authenticating proxy then makes decisions based on those `auth_classes` and a request's OAuth claims (e.g. groups), along with the policies contained within a permissions system (or implemented by the proxy itself). +The authenticating proxy would need to take steps to prevent unauthorised modification of this special tag. +This approach should be considered experimental at this point. + +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. + +| 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. | + +As a result, the process of authorising a request is: + +1. Authenticate the user's token using standard OAuth2 techniques (e.g. online, or using JWKS) +2. Read the user's claimed groups from their provided token +3. Read the list of auth classes assigned to the resource +4. Request a decision from the permissions system based on those data +5. (Write requests only): Check whether the request would modify the special `auth_classes` tag, and confirm the user has permission to make that modification +6. (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 +7. (Write requests only): Propagate any changes to the `auth_classes` tag to Flows and Sources collected by this one + +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/webhook, requests must claim both write permissions and the permission they are changing. + ## Future Work The model described above allows for more use cases than coarse-grained RBAC: especially use cases where multiple tenants share a single service instance. From 5f40e731f6e48e064946d216dec8181019d69757 Mon Sep 17 00:00:00 2001 From: Sam Mesterton-Gibbons Date: Thu, 9 Oct 2025 11:37:28 +0100 Subject: [PATCH 6/8] appnote: Clarify wording in authz app note Improves various wording based on review comments --- docs/appnotes/0003-tag-names.md | 4 ---- .../0016-authorisation-in-tams-workflows.md | 18 ++++++++++++------ 2 files changed, 12 insertions(+), 10 deletions(-) diff --git a/docs/appnotes/0003-tag-names.md b/docs/appnotes/0003-tag-names.md index 0beaa073..26ab5e10 100644 --- a/docs/appnotes/0003-tag-names.md +++ b/docs/appnotes/0003-tag-names.md @@ -202,8 +202,6 @@ 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 auth classes used to derive permissions on the Flow. -No known implementations yet. - ## Known Source Tags ### hls_exclude @@ -222,5 +220,3 @@ 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 auth classes used to derive permissions on the Source. - -No known implementations yet. diff --git a/docs/appnotes/0016-authorisation-in-tams-workflows.md b/docs/appnotes/0016-authorisation-in-tams-workflows.md index 4242cd63..7b1b77b7 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 | | `DELETE` | Request must have write permissions on {sourceId}. | | `/flows` | `HEAD`/`GET` | Restrict returned data to only the Flows that the request has read permission on. | | `/flows/{flowId}` | `HEAD`/`GET` | Request must have read permissions on {flowID}. | -| | `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 neither {flowId} nor the Source ID exist, allow if the request has create permission (see below). If {flowId} already exists, request must have write permissions on {flowId}. | +| | `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 neither {flowId} nor the Source ID exist, allow if the request has create permission (see [Creating new Flows and Sources](#creating-new-flows-and-sources)). If {flowId} already exists, request must have write permissions on {flowId}. | | | `DELETE` | Request must have delete permissions on {flowId}. | | `/flows/{flowId}/tags` | `HEAD`/`GET` | Request must have read permissions on {flowId}. | | `/flows/{flowId}/tags/{name}` | `HEAD`/`GET` | Request must have read permissions on {flowId}. | @@ -204,7 +204,7 @@ Along with granting access to existing Flows and Sources, some method needs to e Depending on the model in use, it may be a requirement to specify the permissions of that resource at creation time: for example setting an `auth_classes` tag to a subset of the user's own groups, and rejecting requests that do not include one. Alternatively an implementation may provide some signalling of a default set of permissions that should be applied when none is given: for example a user could have a default group or project, or an approach where users can own content themselves, and then share it as needed. -For systems that create resources on behalf of a user, the same approach could be taken, or placeholder resources could be created and assigned to that system: for example creating an empty Flow and then specifying the Flow ID to an ingester. +For systems that create resources on behalf of a user, the same approach could be taken, or placeholder resources could be created by the user with permissions assigned to that system: for example creating an empty Flow and then specifying the Flow ID to an ingester. #### Adding Flows to existing Sources @@ -235,7 +235,7 @@ Implementations may provide this feature by either adding default groups to Sour 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 assess permissions on a per-event basis, and cache this information as necessary in their internal implementation. +It is recommended that implementations assess permissions on a per-event basis, and cache this information (with an appropriately short cache expiry time) in their internal implementation. ### Permissions propagation @@ -284,16 +284,22 @@ 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. | -As a result, the process of authorising a request is: +As a result, the process of authorising a request to a resource is: 1. Authenticate the user's token using standard OAuth2 techniques (e.g. online, or using JWKS) 2. Read the user's claimed groups from their provided token 3. Read the list of auth classes assigned to the resource 4. Request a decision from the permissions system based on those data -5. (Write requests only): Check whether the request would modify the special `auth_classes` tag, and confirm the user has permission to make that modification -6. (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 +5. (Write requests only): Check whether the request would modify the special `auth_classes` tag, and confirm the user has permission to make that modification. +6. (Flow Segment write requests only): Check if the Media Object is already referenced by a Flow Segment in the service instance using the `/objects` endpoint, and if it does, confirm the user would have access to read it 7. (Write requests only): Propagate any changes to the `auth_classes` tag to Flows and Sources collected by this one +For listing endpoints (e.g. `GET /flows`) it may be possible to add a `tag.auth_classes` query parameter to the request, and have the upstream API filter Flows returned accordingly. +In cases where the incoming request already filters on the same tag, it will need to be rewritten to prevent returning content the user should not have access to. + +> [!CAUTION] +> Write request handling should be carefully designed and analysed to ensure that user's cannot modify the `auth_classes` tag in such a way to execute a permission elevation attack. + 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/webhook, requests must claim both write permissions and the permission they are changing. From 5ed38ab9fa92efe82cade7bc69bae7c5cb5344d6 Mon Sep 17 00:00:00 2001 From: Sam Mesterton-Gibbons Date: Tue, 18 Nov 2025 15:36:13 +0000 Subject: [PATCH 7/8] adr: Accept 0035 Fine Grained Auth --- docs/adr/0035-fine-grained-auth.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/adr/0035-fine-grained-auth.md b/docs/adr/0035-fine-grained-auth.md index 6107afe8..a1b54f10 100644 --- a/docs/adr/0035-fine-grained-auth.md +++ b/docs/adr/0035-fine-grained-auth.md @@ -1,5 +1,5 @@ --- -status: "proposed" +status: "accepted" --- # Fine-grained Authorisation in TAMS Workflows From fdd3f37ea4a5971e38f6c3f0390dea8c8fa8eef5 Mon Sep 17 00:00:00 2001 From: Sam Mesterton-Gibbons Date: Wed, 19 Nov 2025 11:08:20 +0000 Subject: [PATCH 8/8] Fix delete permission typo on segments Co-authored-by: James Sandford --- 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 7b1b77b7..1fc8e860 100644 --- a/docs/appnotes/0016-authorisation-in-tams-workflows.md +++ b/docs/appnotes/0016-authorisation-in-tams-workflows.md @@ -183,7 +183,7 @@ For example - hiding collection relationships may result in clients deciding to | | `DELETE` | Request must have write permissions on {flowId}. | | `/flows/{flowId}/segments` | `HEAD`/`GET` | Request must have read permissions on {flowId}. | | | `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}. | +| | `DELETE` | Request must have delete permissions on {flowId}. | | `/flows/{flowId}/storage` | `POST` | Request must have write permissions on {flowId}. | | `/objects/{objectId}` | `HEAD`/`GET` | Restrict returned data in `referenced_by_flows` property to only the Flows that the request has read access to. If the request has read access to no Flows of this object, return 404, however if the request has access but all of the Flows have been filtered out, return the response with an empty `referenced_by_flows` list. | | `/objects/{objectId}/instances` | `POST` | Request must have write permissions on {objectId}. |