From 15c9ccf79177917e6a74a4d713af023f39c77f3b Mon Sep 17 00:00:00 2001 From: James Sandford Date: Thu, 24 Jul 2025 14:53:58 +0100 Subject: [PATCH 01/16] Re-locate properties of media objects to object. Reveal those under the objects endpoint. Update references to flow segments that should be to media objects. --- api/TimeAddressableMediaStore.yaml | 86 +++++++++++++++++++++ api/schemas/flow-segment-post.json | 6 +- api/schemas/flow-segment.json | 120 ++++++++--------------------- api/schemas/object-core.json | 53 +++++++++++++ api/schemas/object.json | 50 +++++++----- 5 files changed, 203 insertions(+), 112 deletions(-) create mode 100644 api/schemas/object-core.json diff --git a/api/TimeAddressableMediaStore.yaml b/api/TimeAddressableMediaStore.yaml index 31a918c3..61769b58 100644 --- a/api/TimeAddressableMediaStore.yaml +++ b/api/TimeAddressableMediaStore.yaml @@ -1985,6 +1985,49 @@ paths: required: true schema: type: string + - name: verbose_storage + in: query + description: | + Include storage metadata in `get_urls`. + When `verbose_storage` is `false` only `url`, `presigned`, and `label` will be included in `get_urls`. + schema: + default: false + type: boolean + - name: accept_get_urls + in: query + description: | + A comma separated list of labels of media object `get_urls` to include in the response. + Omitting `accept_get_urls` will result in no filtering of `get_urls`. + An empty `accept_get_urls` results in an empty or no `get_urls` in the response. + Media object `get_urls` with no label or storage ID cannot be filtered for; they will only be returned if `accept_get_urls` is omitted, and `accept_storage_ids` is omitted or empty. + Without `get_urls`, the response from the service could be substantially faster if it is not required to + generate a large number of pre-signed URLs for example. + Where multiple filter query parameters are provided, the returned `get_urls` will match all filters. + schema: + type: string + pattern: ^([^,]+(,[^,]+)*)?$ + - name: accept_storage_ids + in: query + description: | + A comma separated list of `storage_id`s of media object `get_urls` to include in the response. + Omitting `accept_storage_ids`, or providing an empty `accept_storage_ids` will result in no filtering of `get_urls`. + Media object `get_urls` with no label or storage ID cannot be filtered for; they will only be returned if `accept_get_urls` is omitted, and `accept_storage_ids` is omitted or empty. + A full list of available `storage_id`s may be found at the `service/storage-backends` endpoint. + Where multiple filter query parameters are provided, the returned `get_urls` will match all filters. + schema: + type: string + pattern: ^([0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12})(,[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12})*$ + - name: presigned + in: query + description: | + If set to `true`, only presigned URLs (i.e. those whos `presigned` property is `true`) will be returned in `get_urls`. + If set to `false`, only non-presigned URLs (i.e. those whos `presigned` property is `false`) will be returned in `get_urls`. + If omitted, both presigned and non-presigned URLs will be returned. + If `presigned` is set to `false`, the response from the service could be substantially faster if it is not required to + generate a large number of pre-signed URLs. + Where multiple filter query parameters are provided, the returned `get_urls` will match all filters. + schema: + type: boolean - $ref: '#/components/parameters/trait_resource_paged_key' - $ref: '#/components/parameters/trait_paged_limit' responses: @@ -2028,6 +2071,49 @@ paths: required: true schema: type: string + - name: verbose_storage + in: query + description: | + Include storage metadata in `get_urls`. + When `verbose_storage` is `false` only `url`, `presigned`, and `label` will be included in `get_urls`. + schema: + default: false + type: boolean + - name: accept_get_urls + in: query + description: | + A comma separated list of labels of media object `get_urls` to include in the response. + Omitting `accept_get_urls` will result in no filtering of `get_urls`. + An empty `accept_get_urls` results in an empty or no `get_urls` in the response. + Media object `get_urls` with no label or storage ID cannot be filtered for; they will only be returned if `accept_get_urls` is omitted, and `accept_storage_ids` is omitted or empty. + Without `get_urls`, the response from the service could be substantially faster if it is not required to + generate a large number of pre-signed URLs for example. + Where multiple filter query parameters are provided, the returned `get_urls` will match all filters. + schema: + type: string + pattern: ^([^,]+(,[^,]+)*)?$ + - name: accept_storage_ids + in: query + description: | + A comma separated list of `storage_id`s of media object `get_urls` to include in the response. + Omitting `accept_storage_ids`, or providing an empty `accept_storage_ids` will result in no filtering of `get_urls`. + Media object `get_urls` with no label or storage ID cannot be filtered for; they will only be returned if `accept_get_urls` is omitted, and `accept_storage_ids` is omitted or empty. + A full list of available `storage_id`s may be found at the `service/storage-backends` endpoint. + Where multiple filter query parameters are provided, the returned `get_urls` will match all filters. + schema: + type: string + pattern: ^([0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12})(,[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12})*$ + - name: presigned + in: query + description: | + If set to `true`, only presigned URLs (i.e. those whos `presigned` property is `true`) will be returned in `get_urls`. + If set to `false`, only non-presigned URLs (i.e. those whos `presigned` property is `false`) will be returned in `get_urls`. + If omitted, both presigned and non-presigned URLs will be returned. + If `presigned` is set to `false`, the response from the service could be substantially faster if it is not required to + generate a large number of pre-signed URLs. + Where multiple filter query parameters are provided, the returned `get_urls` will match all filters. + schema: + type: boolean - $ref: '#/components/parameters/trait_resource_paged_key' - $ref: '#/components/parameters/trait_paged_limit' responses: diff --git a/api/schemas/flow-segment-post.json b/api/schemas/flow-segment-post.json index 2dbf17b1..bac1a5a9 100644 --- a/api/schemas/flow-segment-post.json +++ b/api/schemas/flow-segment-post.json @@ -41,7 +41,7 @@ }, "get_urls": { - "description": "A list of URLs to which a GET request can be made to directly retrieve the contents of the Segment. This is required by the `http_object_store` Storage Backend type, which is the only one currently described. Clients may choose any URL in the list and treat them as identical, however service instances may sort the list such that the preferred URL is first. `get_urls` should only be used to add uncontrolled URLs. URLs for the provided object_id controlled by the service instance will be populated automatically by the service instance.", + "description": "A list of URLs to which a GET request can be made to directly retrieve the contents of the Media Object. This is required by the `http_object_store` Storage Backend type, which is the only one currently described. Clients may choose any URL in the list and treat them as identical, however service instances may sort the list such that the preferred URL is first. `get_urls` should only be used to add uncontrolled URLs. URLs for the provided object_id controlled by the service instance will be populated automatically by the service instance.", "type": "array", "items": { @@ -54,7 +54,7 @@ { "url": { - "description": "A URL to which a GET request can be made to directly retrieve the contents of the Segment. Clients should include credentials if the provide URL is on the same origin as the API endpoint", + "description": "A URL to which a GET request can be made to directly retrieve the contents of the Media Object. Clients should include credentials if the provide URL is on the same origin as the API endpoint", "type": "string" }, "label": @@ -67,7 +67,7 @@ }, "key_frame_count": { - "description": "The number of key frames in the Segment. This should be set greater than zero when the Segment contains key frames that serve as a stream access point", + "description": "The number of key frames in the Media Object. This should be set greater than zero when the Media Object contains key frames that serve as a stream access point", "type": "integer" } } diff --git a/api/schemas/flow-segment.json b/api/schemas/flow-segment.json index b6921dad..683a10df 100644 --- a/api/schemas/flow-segment.json +++ b/api/schemas/flow-segment.json @@ -1,99 +1,43 @@ { - "title": "Flow Segment", - "description": "Provides the location and metadata of the media files corresponding to timerange Segments of a Flow.", - "type": "object", - "required": - [ + "type": "object", + "description": "Provides the location and metadata of the media files corresponding to timerange segments of a Flow.", + "title": "Flow Segment", + "allOf":[ + { + "type": "object", + "required": [ "object_id", "timerange" - ], - "properties": - { - "object_id": - { - "description": "The Object identifier for the Media Object.", - "type": "string" - }, - "ts_offset": - { - "description": "The timestamp offset between the sample timestamps stored in, or inferred from, the media file and the corresponding timestamp in the Segment, ie. ts_offset = segment ts - media object ts. Assumed to be 0:0 if not set. Format as described by the [Timestamp](#/schemas/timestamp) type", - "$ref": "timestamp.json" + ], + "properties": { + "object_id" : { + "description": "The object store identifier for the media object.", + "type": "string" }, - "timerange": - { - "description": "The timerange for the samples contained in the Segment. The timerange start is always inclusive. If samples have a duration then the timerange end is exclusive and covers at least the duration of the last sample. The exclusive timerange end will typically be set to the timestamp of the next sample. If the samples don't have a duration then the timerange end is inclusive. Format is described by the [TimeRange](#/schemas/timerange) type. Note that where temporal re-ordering is used, the timerange and samples refers to the presentation timeline.", - "$ref": "timerange.json" + "ts_offset": { + "description": "The timestamp offset between the sample timestamps stored in the media file and the corresponding timestamp in the segment, ie. ts_offset = segment ts - media object ts. Assumed to be 0:0 if not set. Format as described by the [Timestamp](../schemas/timestamp#top) type", + "$ref": "timestamp.json" }, - "last_duration": - { - "description": "The difference between the exclusive end of the `timerange` and the last sample timestamp. Format as described by the [Timestamp](#/schemas/timestamp) type, but cannot be negative", - "$ref": "timestamp.json" + "timerange": { + "description": "The timerange for the samples contained in the segment. The timerange start is always inclusive. If samples have a duration then the timerange end is exclusive and covers at least the duration of the last sample. The exclusive timerange end will typically be set to the timestamp of the next sample. If the samples don't have a duration then the timerange end is inclusive. Format is described by the [TimeRange](../schemas/timerange#top) type. Note that where temporal re-ordering is used, the timerange and samples refers to the presentation timeline.", + "$ref": "timerange.json" }, - "sample_offset": - { - "description": "The start of the Segment represented as a count of samples from the start of the Object. Note that a sample is a video frame or audio sample. A (coded) audio frame has multiple audio samples. Assumed to be 0 if not set. Must be set if the Flow Segment doesn't start at the beginning of the Media Object.", - "type": "integer" + "last_duration": { + "description": "The difference between the exclusive end of the `timerange` and the last sample timestamp. Format as described by the [Timestamp](../schemas/timestamp#top) type, but cannot be negative", + "$ref": "timestamp.json" }, - "sample_count": - { - "description": "The count of samples in the Segment (which may be fewer than in the Object). The count could be less than expected given the Segment duration and rate if there are gaps. If not set, every sample from sample_offset onwards is used. Must be set if the Flow Segment doesn't use the entire Media Object. Note that a sample is a video frame or audio sample. A (coded) audio frame has multiple audio samples", - "type": "integer" + "sample_offset": { + "description": "The start of the segment represented as a count of samples from the start of the object. Note that a sample is a video frame or audio sample. A (coded) audio frame has multiple audio samples. Assumed to be 0 if not set.", + "type": "integer" }, - "get_urls": - { - "description": "A list of URLs to which a GET request can be made to directly retrieve the contents of the Segment. This is required by the `http_object_store` Storage Backend type, which is the only one currently described. Clients may choose any URL in the list and treat the content returned as identical, however service instances may sort the list such that the preferred URL is first. Storage backend metadata for controlled URLs should be populated by the service instance based on the storage backend the Media Object copy resides in.", - "type": "array", - "items": - { - "type": "object", - "unevaluatedProperties": false, - "allOf": - [ - { - "$ref": "storage-backend.json" - }, - { - "type": "object", - "required": - [ - "url" - ], - "properties": - { - "storage_id": - { - "description": "Storage backend identifier", - "$ref": "uuid.json" - }, - "url": - { - "description": "A URL to which a GET request can be made to directly retrieve the contents of the Segment. Clients should include credentials if the provide URL is on the same origin as the API endpoint", - "type": "string" - }, - "presigned": - { - "description": "If `true`, this URL is pre-signed. If this parameter is unset, the URL is NOT pre-signed.", - "type": "boolean" - }, - "label": - { - "description": "Label identifying this URL. If the URL is controlled by the service instance, this is the Storage Backend's label. If the URL is uncontrolled, this is the label provided when a client registered the URL. If the 'label' is not set then this URL can't be filtered for using the 'accept_get_urls' API query parameter.", - "type": "string" - }, - "controlled": - { - "description": "If `true`, this URL is on a storage backend controlled by this service instance. If `false`, this URL is uncontrolled and does not have it's lifecycle managed by this instance. If this parameter is unset, assume `true`.", - "type": "boolean" - } - } - } - ] - } - }, - "key_frame_count": - { - "description": "The number of key frames in the Segment. This should be set greater than zero when the Segment contains key frames that serve as a stream access point", - "type": "integer" + "sample_count": { + "description": "The count of samples in the segment (which may be fewer than in the object). The count could be less than expected given the segment duration and rate if there are gaps. If not set, every sample from sample_offset onwards is used. Note that a sample is a video frame or audio sample. A (coded) audio frame has multiple audio samples", + "type": "integer" } + } + }, + { + "$ref": "object-core.json" } + ] } diff --git a/api/schemas/object-core.json b/api/schemas/object-core.json new file mode 100644 index 00000000..e188b391 --- /dev/null +++ b/api/schemas/object-core.json @@ -0,0 +1,53 @@ +{ + "type": "object", + "description": "Provides the location and metadata of the media files corresponding to a media Object.", + "title": "Object", + "properties": { + "get_urls": { + "description": "A list of URLs to which a GET request can be made to directly retrieve the contents of the media object. This is required by the `http_object_store` media store type, which is the only one currently described. Clients may choose any URL in the list and treat the content returned as identical, however servers may sort the list such that the preferred URL is first. Storage backend metadata for controlled URLs should be populated by the TAMS instance based on the storage backend the object copy resides in.", + "type": "array", + "items": { + "type": "object", + "unevaluatedProperties": false, + "allOf": [ + { + "$ref": "storage-backend.json" + }, + { + "type": "object", + "required": [ + "url" + ], + "properties": { + "storage_id": { + "description": "Storage backend identifier", + "type": "string", + "pattern": "^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$" + }, + "url": { + "description": "A URL to which a GET request can be made to directly retrieve the contents of the media object. Clients should include credentials if the provide URL is on the same origin as the API endpoint", + "type": "string" + }, + "presigned": { + "description": "If `true`, this URL is pre-signed. If this parameter is unset, the URL is NOT pre-signed.", + "type": "boolean" + }, + "label": { + "description": "Label identifying this URL. If the URL is controlled by the service instance, this is the Storage Backend's label. If the URL is uncontrolled, this is the label provided when a client registered the URL. If the 'label' is not set then this URL can't be filtered for using the 'accept_get_urls' API query parameter.", + "type": "string" + }, + "controlled": { + "description": "If `true`, this URL is on a storage backend controlled by this service instance. If `false`, this URL is uncontrolled and does not have it's lifecycle managed by this instance. If this parameter is unset, assume `true`.", + "type": "boolean" + } + } + } + ] + } + }, + "key_frame_count": { + "description": "The number of key frames in the media object. This should be set greater than zero when the media object contains key frames that serve as a stream access point", + "type": "integer" + } + } +} diff --git a/api/schemas/object.json b/api/schemas/object.json index 643c97c0..7726b223 100644 --- a/api/schemas/object.json +++ b/api/schemas/object.json @@ -1,26 +1,34 @@ { "title": "Object", - "description": "Describes a Media Object in the service instance.", - "type": "object", - "required": [ - "id", - "referenced_by_flows" - ], - "properties": { - "id": { - "description": "The Media Object identifier.", - "type": "string" - }, - "referenced_by_flows": { - "description": "List of Flows that reference this Media Object via Flow Segments in this service instance.", - "type": "array", - "items": { - "$ref": "uuid.json" + "allOf": [ + { + "type": "object", + "required": [ + "id", + "referenced_by_flows" + ], + "properties": { + "id": { + "description": "The media object identifier.", + "type": "string" + }, + "referenced_by_flows": { + "type": "array", + "description": "List of Flows that reference this media object via Flow Segments in this store.", + "items": { + "type": "string", + "pattern": "^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$" + } + }, + "first_referenced_by_flow": { + "description": "The first Flow that had a Flow Segment reference the media object in this store. This Flow is also present in 'referenced_by_flows' if it is still referenced by the Flow. This property is optional and may in some implementations become unset if the Flow no longer references the media object, e.g. because it was deleted.", + "type": "string", + "pattern": "^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$" + } } }, - "first_referenced_by_flow": { - "description": "The first Flow that had a Flow Segment reference the Media Object in this service instance. This Flow is also present in 'referenced_by_flows' if it is still referenced by the Flow. This property is optional and may, in some service implementations, become unset if the Flow no longer references the Media Object, e.g. because it was deleted.", - "$ref": "uuid.json" + { + "$ref": "object-core.json" } - } -} \ No newline at end of file + ] +} From 3408a28412b271575a361e433d1f6651bb3432f8 Mon Sep 17 00:00:00 2001 From: James Sandford Date: Fri, 25 Jul 2025 10:34:04 +0100 Subject: [PATCH 02/16] Add endpoints for allocating additional media object storage, registering additional object URLs, and deleting object URLs --- api/TimeAddressableMediaStore.yaml | 153 +++++++++++++++++- api/examples/object-storage-post-201.json | 21 +++ api/examples/object-storage-post.json | 3 + .../objects-instances-controlled-post.json | 3 + .../objects-instances-uncontrolled-post.json | 4 + api/schemas/object-storage-post.json | 13 ++ api/schemas/object-storage.json | 56 +++++++ api/schemas/objects-instances-post.json | 40 +++++ 8 files changed, 290 insertions(+), 3 deletions(-) create mode 100644 api/examples/object-storage-post-201.json create mode 100644 api/examples/object-storage-post.json create mode 100644 api/examples/objects-instances-controlled-post.json create mode 100644 api/examples/objects-instances-uncontrolled-post.json create mode 100644 api/schemas/object-storage-post.json create mode 100644 api/schemas/object-storage.json create mode 100644 api/schemas/objects-instances-post.json diff --git a/api/TimeAddressableMediaStore.yaml b/api/TimeAddressableMediaStore.yaml index 61769b58..78c6dd9f 100644 --- a/api/TimeAddressableMediaStore.yaml +++ b/api/TimeAddressableMediaStore.yaml @@ -1922,9 +1922,9 @@ paths: $ref: 'schemas/uuid.json' description: The Flow identifier. post: - summary: Allocate Flow Storage + summary: Allocate Initial Flow Storage description: | - Allocate storage locations for writing Media Objects. + Allocate initial storage locations for writing Media Objects. The Storage Backend type, which is indicated in the [/service](#/operations/GET_service) resource, determines the information provided in the response. The examples and description below are for the "http_object_store" Storage Backend type. @@ -2141,7 +2141,154 @@ 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. + /objects/{objectId}/instances: + post: + summary: Register a Media Object instance + description: | + Signal that a media object instance is now available on a new storage backend and may be advertised in `get_urls`. Or add a new uncontrolled URL to `get_urls`. + + Where a client has written a new object instance, the client is responsible for ensuring that the object written is complete and correct before registering it with this method. All instances of an object MUST be identical. + + API implementations MAY also support managed copying of media objects using this method. Where implementations support this feature, clients may POST a `storage_id` to this endpoint without previously allocating storage for media object `objectId` on storage backend `storage_id`. The API will then: + + - Allocate storage for media object `objectId` on storage backend `storage_id` + - Copy the media object from an existing location to the newly allocated storage + - Start advertising the new copy in `get_urls` once ready + + Where the above mechanism is not supported and the URL being registered is controlled, API implementations SHOULD verify that storage has previously been allocated on the requested storage backend `storage_id` for the media object `objectId`. + operationId: POST_objects-instances + tags: + - Objects + parameters: + - name: objectId + in: path + description: The media object identifier. + required: true + schema: + type: string + requestBody: + content: + application/json: + examples: + controlled: + summary: Registering a controlled instance + value: + $ref: examples/objects-instances-controlled-post.json + uncontrolled: + summary: Registering a uncontrolled instance + value: + $ref: examples/objects-instances-uncontrolled-post.json + schema: + $ref: schemas/objects-instances-post.json + required: true + responses: + "201": + description: Object instance successfully registered. + "400": + description: Bad request. Invalid request JSON. + "403": + description: Forbidden. You do not have permission to modify this Media Object. + "404": + description: The media object does not exist. + delete: + summary: Delete a Media Object instance + description: | + Delete an instance of a media object. + + One of `storage_id` or `label` MUST be specified in the query parameters. `storage_id` SHOULD be used where `controlled` is `True` for the instance. + + API instances should remove the Media Object instance from the `get_urls` list and then, if the instance is controlled, delete the object instance from storage. + + API instances SHOULD prevent clients from deleting all Object instances. Where clients wish to remove all copies of an Object from the store, they should do so by deleting all Flows or Flow Segments which reference the Object. + operationId: DELETE_objects-instances + tags: + - Objects + parameters: + - name: objectId + in: path + description: The Media Object identifier. + required: true + schema: + type: string + - name: storge_id + in: query + description: The storage_id identifying the Media Object instance to be deleted. + schema: + type: string + - name: label + in: query + description: The label identifying the Media Object instance to be deleted. + schema: + type: string + responses: + "204": + description: No content. The Media Object instance has been deleted. + "400": + description: Bad request. Invalid query options. + "403": + description: Forbidden. You do not have permission to modify this Media Object. + "404": + description: The requested object ID in the path is invalid. + /objects/{objectId}/storage: + parameters: + - name: objectId + in: path + required: true + schema: + $ref: '#/components/schemas/uuid' + description: The object identifier. + post: + summary: Allocate Additional Object Storage + description: | + Allocate additional storage locations for writing media objects. + + Storage for object `objectId` will be allocated against storage backend `storage_id`. If the object already exists on the specified storage backend, the request will be rejected. + + The media store type, which is indicated in the /service resource, determines the information provided + in the response. The examples and description below are for the "http_object_store" media store type. + This media store type provides HTTP URLs for uploading and downloading media objects in buckets. + + The response will include a PUT URL that a client uses to upload the media object. The client is expected + to register the object instance using the `/objects/{objectId}/instances` endpoint once the upload is complete. + Implementations need to handle situations where objects were uploaded but no Media Object instance was registered + successfully. + + The response may include PUT URLs for creating buckets for the media objects. These PUT URLs should + be used before uploading media objects. The object_id associated with each storage location has the + bucket name as its prefix. + + The response may include PUT URLs for setting the CORS properties for the buckets and media objects. + + When making requests to the provided `put_url`, clients should include credentials if the provided + URL is on the same origin as the API itself, akin to the `same-origin` mode in the + [WhatWG Fetch Standard](https://fetch.spec.whatwg.org/#concept-request-credentials-mode). + operationId: POST_objects-objectId-storage + tags: + - MediaStorage + requestBody: + content: + application/json: + example: + $ref: examples/object-storage-post.json + schema: + $ref: schemas/object-storage-post.json + responses: + "201": + description: Storage locations for writing media objects. + content: + application/json: + schema: + $ref: schemas/object-storage.json + example: + $ref: examples/object-storage-post-201.json + "400": + description: Bad request. Invalid object storage request JSON. The storage backend `storage_id` may not exist. The object may already exist on storage backend. + "403": + description: Forbidden. You do not have permission to modify this media object. + "404": + description: The requested object does not exist. + /flow-delete-requests: head: summary: List Flow Delete Requests diff --git a/api/examples/object-storage-post-201.json b/api/examples/object-storage-post-201.json new file mode 100644 index 00000000..15a6b7fb --- /dev/null +++ b/api/examples/object-storage-post-201.json @@ -0,0 +1,21 @@ +{ + "pre": [ + { + "action": "create_bucket", + "bucket_id": "tams-e2b89b02-21e7-5f9d-aa2d-db38b01453c9", + "put_url": { + "url": "https://example.store.com/tams-e2b89b02-21e7-5f9d-aa2d-db38b01453c9?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=0&X-Amz-Date=20230316T120329Z&X-Amz-Expires=300&X-Amz-SignedHeaders=host&X-Amz-Signature=0", + "body": "\n default\n\n" + } + } + ], + "media_objects": [ + { + "object_id": "tams-e2b89b02-21e7-5f9d-aa2d-db38b01453c9/846023d3-612d-5014-bc47-88f6eb2d04bb", + "put_url": { + "url": "https://example.store.com/tams-e2b89b02-21e7-5f9d-aa2d-db38b01453c9/846023d3-612d-5014-bc47-88f6eb2d04bb?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=0&X-Amz-Date=20230316T120329Z&X-Amz-Expires=300&X-Amz-SignedHeaders=content-type%3Bhost&X-Amz-Signature=0", + "content-type": "video/mp2t" + } + } + ] +} diff --git a/api/examples/object-storage-post.json b/api/examples/object-storage-post.json new file mode 100644 index 00000000..07f74050 --- /dev/null +++ b/api/examples/object-storage-post.json @@ -0,0 +1,3 @@ +{ + "storage_id": "60af2ab4-e8a5-4c65-a09b-d35983680315" +} diff --git a/api/examples/objects-instances-controlled-post.json b/api/examples/objects-instances-controlled-post.json new file mode 100644 index 00000000..07f74050 --- /dev/null +++ b/api/examples/objects-instances-controlled-post.json @@ -0,0 +1,3 @@ +{ + "storage_id": "60af2ab4-e8a5-4c65-a09b-d35983680315" +} diff --git a/api/examples/objects-instances-uncontrolled-post.json b/api/examples/objects-instances-uncontrolled-post.json new file mode 100644 index 00000000..3f484127 --- /dev/null +++ b/api/examples/objects-instances-uncontrolled-post.json @@ -0,0 +1,4 @@ +{ + "url": "https://tams-b.s3.eu-west-1.amazonaws.com/35e9b447-be10-43e0-ab85-e1e9fe15d354?X-Amz-Security-Token=signature...", + "label": "pipeline-b" +} diff --git a/api/schemas/object-storage-post.json b/api/schemas/object-storage-post.json new file mode 100644 index 00000000..e59dd4c9 --- /dev/null +++ b/api/schemas/object-storage-post.json @@ -0,0 +1,13 @@ +{ + "title": "Flow Storage Post", + "description": "Post data for the flow storage endpoint", + "type": "object", + "required": ["storage_id"], + "properties": { + "storage_id": { + "description": "The storage backend to allocate storage in. A storage backend identifier as advertised at the `/service` endpoint. If not set the default, as advertised at the `/service` endpoint, will be used if available. An invalid storage backend identifier will result in a 400 error.", + "type": "string", + "pattern": "^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$" + } + } +} diff --git a/api/schemas/object-storage.json b/api/schemas/object-storage.json new file mode 100644 index 00000000..487bdaad --- /dev/null +++ b/api/schemas/object-storage.json @@ -0,0 +1,56 @@ +{ + "type": "object", + "title": "Media Bucket Object Store", + "description": "Gives information on storage for media objects. This schema is for the `http_object_store` media store type which provides URLs for storing media objects in bucket, and is the only store type currently implemented.", + "properties": { + "pre": { + "type": "array", + "description": "Actions that need to be taken before the media object can be written", + "maxItems": 1, + "items": { + "type": "object", + "description": "An action", + "required": [ + "action" + ], + "properties": { + "action": { + "type": "string", + "enum": [ + "create_bucket" + ] + }, + "bucket_id": { + "type": "string", + "description": "The name of the bucket that needs to be created" + }, + "put_url": { "$ref": "http-request.json" }, + "put_cors_url": { "$ref": "http-request.json" } + } + } + }, + "media_objects": { + "type": "array", + "description": "List of information for identifying and uploading media objects", + "minItems": 1, + "maxItems": 1, + "items": { + "type": "object", + "description": "Information for a media object", + "required": [ + "object_id", + "put_url" + ], + "properties": { + "object_id": { + "description": "The object store identifier for the media object.", + "type": "string" + }, + "put_url": { "$ref": "http-request.json" }, + "put_cors_url": { "$ref": "http-request.json" } + } + } + } + }, + "additionalProperties": false +} diff --git a/api/schemas/objects-instances-post.json b/api/schemas/objects-instances-post.json new file mode 100644 index 00000000..57e1c140 --- /dev/null +++ b/api/schemas/objects-instances-post.json @@ -0,0 +1,40 @@ +{ + "type": "object", + "description": "Register a Media Object instance in the store.", + "title": "Media object registration", + "oneOf": [ + { + "description": "Register a controlled Media Object instance via its `storage_id`.", + "title": "Controlled instance", + "type": "object", + "required": [ + "storage_id" + ], + "properties": { + "storage_id": { + "description": "Storage backend identifier", + "type": "string", + "pattern": "^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$" + } + } + }, + { + "description": "Register an uncontrolled Media Object instance via its `url`.", + "title": "Uncontrolled instance", + "type": "object", + "required": [ + "url" + ], + "properties": { + "url": { + "description": "A URL to which a GET request can be made to directly retrieve the contents of the media object. Clients should include credentials if the provide URL is on the same origin as the API endpoint", + "type": "string" + }, + "label": { + "description": "Label identifying this Media Object instance. Service implementations should reject any requests using labels that are already associated with Storage Backends. If the 'label' is not set then this instance can't be filtered for using the 'accept_get_urls' API query parameter.", + "type": "string" + } + } + } + ] +} \ No newline at end of file From cd8e0c39ef6575ba3b3250ccba045497b10ed0b3 Mon Sep 17 00:00:00 2001 From: James Sandford Date: Fri, 25 Jul 2025 12:17:36 +0100 Subject: [PATCH 03/16] Update expectations and behavior regarding modification of Flow Segments to avoid race conditions with Object garbage collection. --- api/TimeAddressableMediaStore.yaml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/api/TimeAddressableMediaStore.yaml b/api/TimeAddressableMediaStore.yaml index 78c6dd9f..5feb39d4 100644 --- a/api/TimeAddressableMediaStore.yaml +++ b/api/TimeAddressableMediaStore.yaml @@ -1826,6 +1826,10 @@ paths: A service instance MAY support Media Objects that are held in external storage in another TAMS or other media storage system. The Flow Segment may in that case require the `get_urls` property to provide the information needed by clients to access the Media Object. + Clients MAY modify Flow Segments, but this should only be done in exceptional circumstances to correct metadata as such operations will likely break the idempotency of Segments. + Properties of Media Objects, such as `get_urls`, SHOULD be modified via the `/objects` endpoints. + They SHOULD NOT be modified at this endpoint, and TAMS instences SHOULD reject such requests with a `400` error. + If a client needs to modify a Flow Segment, e.g. to correct metadata such as the `key_frame_count` or add additional URLs to `get_urls`, then the client SHOULD first delete the existing Segment and then write a new one. The behaviour is undefined if the Segment exists and the service may return a 400 error response. From 5907f98504a3afff575c08cb7503473401c08507 Mon Sep 17 00:00:00 2001 From: James Sandford Date: Fri, 25 Jul 2025 14:18:53 +0100 Subject: [PATCH 04/16] Add storage management ADR --- docs/README.md | 1 + docs/adr/0038-improved-storage-management.md | 123 +++++++++++++++++++ 2 files changed, 124 insertions(+) create mode 100644 docs/adr/0038-improved-storage-management.md diff --git a/docs/README.md b/docs/README.md index b27fbd59..6412e6a6 100644 --- a/docs/README.md +++ b/docs/README.md @@ -67,6 +67,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 | +| [0038](./adr/0038-improved-storage-management.md) | Improved Storage Management | | [0039](./adr/0039-remove-pre-actions.md) | Proposal to remove pre-actions from storage allocation response | \* Note: ADR 0004a was the unintended result of a number clash in the early development of TAMS which wasn't caught before publication diff --git a/docs/adr/0038-improved-storage-management.md b/docs/adr/0038-improved-storage-management.md new file mode 100644 index 00000000..5a4ba434 --- /dev/null +++ b/docs/adr/0038-improved-storage-management.md @@ -0,0 +1,123 @@ +--- +status: "proposed" +--- +# Improved Storage Management + +## Context and Problem Statement + +In [ADR0032](./0032-specifying-storage-backend.md), support was added for advertising multiple storage backends, and selecting one when allocating storage against Flows. +The TAMS specification has always had the ability to advertise multiple URLs for retrieving Media Objects. +But, so far, there has not been direct support for creating and managing duplicates of Media Objects under the control of a TAMS instance. + +An Objects endpoint was added in [ADR0027](./0027-add-objects-api-endpoint.md) that advertises the Flows where a given Object is referenced. +This has begun a transition from thinking about Objects being heavily tied to a Segment. +And a move from thinking about "Segment reuse" to "Object reuse". +Until now, the TAMS specifcation has been unclear on the ownership of `get_urls` being the Segment or Object. +In particular, whether re-use of an Object should result in `get_urls` and changes to them being reflected across all segments using of them. + +This ADR proposes explicitely linking ownership of `get_urls` to Objects, and providing mechanisms for adding and removing controlled and uncontrolled instances of Objects to their `get_urls` list. +This seperation of Objects and Segments does not require breaking changes, but does provide greater clarity in how the specification should be implemented. + +## Considered Options + +* Option 1a: Manage `get_urls` via the Flows endpoints +* Option 1b: Add `get_urls` management to the Objects endpoint +* Option 2a: Manage additional Object storage via Flow storage endpoint +* Option 2b: Manage additional Object storage via a Objects storage endpoint +* Option 2c: Manage additional Object storage AND initial Object storage via a Objects storage endpoint +* Option 3a: Call Object Instance management endpoint `get_urls` +* Option 3b: Call Object Instance management endpoint `instances` + +## Decision Outcome + +Chosen options: + +* Option 1b: Add `get_urls` management to the Objects endpoint +* Option 2b: Manage additional Object storage via a Objects storage endpoint +* Option 3b: Call Object Instance management endpoint `instances` + +These options have been chosen because they provide clearer boundaries between Media Objects and Segments in the data model and it's implementation. +They should avoid confusion arrising from changes to one Flow impacting another. +And they minimise un-needed changes to the API and common workflows. + +### Implementation + +{Once the proposal has been implemented, add a link to the relevant PRs here} + +## Pros and Cons of the Options + +### Option 1a: Manage `get_urls` via the Flows endpoints + +This option would see us add support for in-place editing of `get_urls` and see the edits propogated to other segments making use of the same Media Object. + +* Good, because it somewhat matches existing patterns for updating `get_urls` +* Good, because it would remove a race condition of the delete & re-create pattern with Object garbage-collection in implementations +* Bad, because changes to one Flow's segment may have an impact on other's +* Bad, because it persists a blurring of Segments and Media Objects in the TAMS data model + +### Option 1b: Add `get_urls` management to the Objects endpoint + +This option would see HTTP methods added to/under the `/objects` endpoint to facilitate management of `get_urls`. + +* Good, because it would remove a race condition of the delete & re-create pattern with Object garbage-collection in implementations +* Good, because it provides a clearer boundary between Media Objects and Segments +* Good, because edits are performed on the shared Media Objects, rather than as side affects between Flows +* Neutral, because it requires the replacement of a "delete & re-create" pattern with for-purpose endpoints + +### Option 2a: Manage additional Object storage via Flow storage endpoint + +The existing endpoint used for allocation of storage, which a client will upload media to, is under the `/flows/{flowId}` endpoint at `/flows/{flowId}/storage`. +This is because storage needs to be tied to a specific Flow initially so it can inherit permissions from that Flow, and so the correct MIME type may be obtained and applied to the object on the object storage backend. + +This option would see storage for additional instances of a Media Object be allocated via the existing endpoint. + +* Good, because it makes use of an existing endpoint and workflows +* Good, because it reduces required changes to the API +* Bad, because it persists a blurring of Segments and Media Objects in the TAMS data model +* Bad, because the allocation of storage against one flow, that may then be used by many could be confusing +* Bad, because it poorly communicates the shared management of Objects + * It may result in confusion where a client can edit properties of the Object in one location (e.g. Flow A's Segments), but not another (e.g. Flow B's Segments) + +### Option 2b: Manage additional Object storage via a Objects storage endpoint + +This option would see storage for additional instances of a Media Object be allocated via a new endpoint under the `/objects` endpoint. +Initial allocation would still be performed at `/flows/{flowId}/storage` for the reasons stated above. + +* Good, because it provides a clearer boundary between Media Objects and Segments +* Good, because addition of further Object instances is performed on the shared Media Objects, rather than as side affects between Flows +* Good, because it better communicates the shared management of Objects +* Neutral, because it requires new endpoints on the API +* Neutral, because it results in two endpoints for allocating storage, but for different purposes + +### Option 2c: Manage additional Object storage AND initial Object storage via a Objects storage endpoint + +This would see Option 2b extended such that all storage allocation happens under the `/objects` endpoint. +Allocation of storage via `/flows/{flowId}/storage` would be deprecated/removed. + +* Good, because it provides a clearer boundary between Media Objects and Segments +* Good, because it would provide a single endpoint for storage management +* Neutral, because it would require a new mechanism for conveying the initial Flow to inherit permissions and MIME type from +* Neutral, because it requires new endpoints on the API +* Bad, because it would be a breaking change to a core part of the API +* Bad, because it results in two endpoints for allocating storage for the same purpose +* Bad, because it poorly communicates the shared management of Objects + +### Option 3a: Call Object Instance management endpoint `get_urls` + +Where Option 1b is chosen, we would need to decide on a name for the new Objects endpoint. +As the purpose of this endpoint is to add/remove instances in the `get_urls` list, one option is to title the endpoint `get_urls`. + +* Good, because it matches the name of the property it affects +* Bad, because some instances may map to multiple URLs + * e.g. pre-signed/non-pre-signed variants of URLs +* Bad, because URLs may be generated by instances when retrieved + * e.g. pre-signed URLs + * This means the property being PUT/POSTed to the new endpoint doesn't directly match those in the list + +### Option 3b: Call Object Instance management endpoint `instances` + +Another option is to title the endpoint `instances`. + +* Good, because it could avoid confusion over the one-to-many relationship of instances and URLs +* Good, becuase it more clearly conveys that the client manages the instance, but the service manages the URL +* Neutral, because it doesn't match the name of the property it affects From fae9ba3d6b978d69d9e65e5d83933126fe30bbc6 Mon Sep 17 00:00:00 2001 From: James Sandford Date: Tue, 29 Jul 2025 15:47:44 +0100 Subject: [PATCH 05/16] Add application note on managing multiple object instances --- docs/README.md | 1 + ...0018-managing-multiple-object-instances.md | 221 ++++++++++++++++++ 2 files changed, 222 insertions(+) create mode 100644 docs/appnotes/0018-managing-multiple-object-instances.md diff --git a/docs/README.md b/docs/README.md index 6412e6a6..61a20e4f 100644 --- a/docs/README.md +++ b/docs/README.md @@ -23,6 +23,7 @@ For more information on how we use application notes, see [here](./appnotes/READ | [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 | | [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 | ## ADRs diff --git a/docs/appnotes/0018-managing-multiple-object-instances.md b/docs/appnotes/0018-managing-multiple-object-instances.md new file mode 100644 index 00000000..602735f2 --- /dev/null +++ b/docs/appnotes/0018-managing-multiple-object-instances.md @@ -0,0 +1,221 @@ +# 0018: Managing Multiple Object Instances + +## Abstract + +[ADR0038](../adr/0038-improved-storage-management.md) added the ability to create multiple managed copies of the same Media Object in the same TAMS instance. +This application note describes how a client may create, reference, duplicate, and delete instances of a Media Object. +It also describes potential security considerations for deployments. + +## Managing Multiple Object Instances + +### Initial Object Creation + +When a Media Object is initially created, it must be allocated storage against a specific Flow. +This is so that the Media Object may inherit permissions and its MIME Type from the Flow. + +A request is made to [`/flows/{flowId}/storage`](https://bbc.github.io/tams/7.0/index.html#/operations/POST_flows-flowId-storage) with the `limit` property set to the number of Media Object storage locations required. +If a specific Storage Backend is required, or if the service instance does not provide a default, a `storage_id` may also be specified. +Available Storage Backends, and defaults, are advertised at the [`/service/storage-backends`](https://bbc.github.io/tams/7.0/index.html#/operations/GET_storage-backends) endpoint. + +Example POST body to `/flows/{flowId}/storage`: + +```json +{ + "limit": 1, + "storage_id": "60af2ab4-e8a5-4c65-a09b-d35983680315" +} +``` + +Example response: + +```json +{ + "pre": [ + { + "action": "create_bucket", + "bucket_id": "tams-e2b89b02-21e7-5f9d-aa2d-db38b01453c9", + "put_url": { + "url": "https://example.store.com/tams-e2b89b02-21e7-5f9d-aa2d-db38b01453c9?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=0&X-Amz-Date=20230316T120329Z&X-Amz-Expires=300&X-Amz-SignedHeaders=host&X-Amz-Signature=0", + "body": "\n default\n\n" + } + } + ], + "media_objects": [ + { + "object_id": "tams-e2b89b02-21e7-5f9d-aa2d-db38b01453c9/846023d3-612d-5014-bc47-88f6eb2d04bb", + "put_url": { + "url": "https://example.store.com/tams-e2b89b02-21e7-5f9d-aa2d-db38b01453c9/846023d3-612d-5014-bc47-88f6eb2d04bb?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=0&X-Amz-Date=20230316T120329Z&X-Amz-Expires=300&X-Amz-SignedHeaders=content-type%3Bhost&X-Amz-Signature=0", + "content-type": "video/mp2t" + } + } + ] +} +``` + +In the response above, you can see a "pre" action alongside the the Media Object. +This may be returned where a service implementation maintains multiple buckets on a storage backend. +There is no requirement for a client to use all Media Objects they request. +This allows a client to request allocation of Objects in bulk, improving efficiency. +But it also means that a bucket could be created and not used. +This could result in users incurring unnecessary costs. +A service may therefore require the client to trigger its creation. +Note that the request may include pre-actions for multiple buckets. +The bucket a Media Object is associated with is pre-fixed to the `object_id`. +As such, the client should only run the pre-action for a given bucket when it first wishes to populate a Media Object in that bucket. + +In the example above, a client would first trigger the creation of the bucket by a PUT request to the specified pre-signed URL with the specified body. +The client would then PUT the Media Object's file to the `put_url` for one of the Media Object's in the list, with the `content-type` on the request set to the specified value. + +Once the media Object has been uploaded, it should be registered on the Flow's timeline via a Segment. +The appropriate Object ID from the requests above, and the Timerange it covers should be registered via a POST request to the [`/flows/{flowId}/segments`](https://bbc.github.io/tams/7.0/index.html#/operations/POST_flows-flowId-segments) endpoint. + +Example POST body to `/flows/{flowId}/segments`: + +```json +{ + "object_id": "tams-e2b89b02-21e7-5f9d-aa2d-db38b01453c9/846023d3-612d-5014-bc47-88f6eb2d04bb", + "timerange": "[20:0_21:0)" +} +``` + +Note that the first time a Media Object is registered against a Flow Segment, the Flow ID of the Flow Segment MUST match the one the storage was allocated against. +i.e. The `flowId` MUST match in `/flows/{flowId}/storage` and `/flows/{flowId}/segments`. +This is to enable the correct inheritance of permissions and content-type. + +The Flow Segment making use of the Media Object will now be available for reading at [`/flows/{flowId}/segments`](https://bbc.github.io/tams/7.0/index.html#/operations/GET_flows-flowId-segments). + +### Referencing an Existing Object + +After initial registration with a Flow, a Media Object may be referenced by other Flows. +The client adding a reference to the existing Object MUST have read permissions on a Flow which already references the Object, and write permissions on the destination Flow. +For example, a client with read access to a Flow with ID `{flowId}` and write permissions on a Flow with ID `{flowId2}` may re-use Objects from `{flowId}` in `{flowId2}`. + +Example POST body to `/flows/{flowId2}/segments`: + +```json +{ + "object_id": "tams-e2b89b02-21e7-5f9d-aa2d-db38b01453c9/846023d3-612d-5014-bc47-88f6eb2d04bb", + "timerange": "[165:0_166:0)", + "ts_offset": "145:0" +} +``` + +Note the `ts_offset` which describes the difference between the timing internal to the media, and the Flow timeline. +The Segment above in `flowId` used the default `ts_offset` of `0:0`. +As the Segment it was used in in `flowId` started at `20:0`, but in `flowId2` it is placed at `165:0`, we must set a `ts_offset` of `145:0`. +For more information on `ts_offset`, see [here](https://bbc.github.io/tams/7.0/index.html#/operations/GET_flows-flowId-segments). + +### Duplicating an Existing Object (Client Managed) + +There are many reasons a client may want to create a duplicate instance of a Media Object. +To create a backup. +To create copies that are physically or logically closer to other systems. +To move content to archive storage. +All while being able to refer to the this collection of duplicates with the same Media Object ID in Flow Segments. + +There are two methods of creating duplicate instances of a Media Object. +The first requires the client to manage the duplication process. + +The client first has to allocate additional storage to the Media Object on the required Storage Backend. +This is done via a POST to `/objects/{objectId}/storage` with the required `storage_id`. + +Example POST body to `/objects/{objectId}/storage`: + +```json +{ + "storage_id": "323367fd-21bb-4f2e-ad38-faf048c4ccfc" +} +``` + +The response is identical in form to the `/flows/{flowId}/storage` response above: + +```json +{ + "pre": [ + { + "action": "create_bucket", + "bucket_id": "tams-c6b8e7cc-edd3-5f6d-9d79-4467d06eb8bf", + "put_url": { + "url": "https://example.store.com/tams-c6b8e7cc-edd3-5f6d-9d79-4467d06eb8bf?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=0&X-Amz-Date=20230316T120329Z&X-Amz-Expires=300&X-Amz-SignedHeaders=host&X-Amz-Signature=0", + "body": "\n default\n\n" + } + } + ], + "media_objects": [ + { + "object_id": "tams-c6b8e7cc-edd3-5f6d-9d79-4467d06eb8bf/846023d3-612d-5014-bc47-88f6eb2d04bb", + "put_url": { + "url": "https://example.store.com/tams-c6b8e7cc-edd3-5f6d-9d79-4467d06eb8bf/846023d3-612d-5014-bc47-88f6eb2d04bb?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=0&X-Amz-Date=20230316T120329Z&X-Amz-Expires=300&X-Amz-SignedHeaders=content-type%3Bhost&X-Amz-Signature=0", + "content-type": "video/mp2t" + } + } + ] +} +``` + +Once the media has been uploaded to the new Storage Backend, it must be registered as available. +This is done via a POST to `/objects/{objectId}/instances` with the `storage_id`. + +Example POST body to `/objects/{objectId}/instances`: + +```json +{ + "storage_id": "323367fd-21bb-4f2e-ad38-faf048c4ccfc" +} +``` + +The available instances of the Media Object will now be advertised in `get_urls` on the `/objects/{objectId}` endpoint and on the `/flows/{flowId}/segments` for all Flow Segments which use the Media Object. + +### Duplicating an Existing Object (Server Managed) + +Some TAMS implementations may support a second, server managed, method of duplicating Media Objects. + +For this method, clients POST the required `storage_id` to `/objects/{objectId}/instances`. +This is the same as the final step in the client managed approach. +The TAMS instance will identify that it has not previously allocated storage for the requested Media Object on the requested Storage Backend. +It will allocate storage, and populate it from an existing copy of the Media Object. +It will then begin advertising the copy in `get_urls` lists. + +Example POST body to `/objects/{objectId}/instances`: + +```json +{ + "storage_id": "323367fd-21bb-4f2e-ad38-faf048c4ccfc" +} +``` + +### Deleting an Object Instance + +Specific instances of a Media Object can be deleted by a DELETE request to `/objects/{objectId}/get_urls` with the relevant `storage_id` in the query string. + +Example DELETE request: + +```text +http://tams.example.com/objects/846023d3-612d-5014-bc47-88f6eb2d04bb/get_urls?storage_id=323367fd-21bb-4f2e-ad38-faf048c4ccfc +``` + +Once deleted, this instance will no longer be advertised in `get_urls` on the `/objects/{objectId}` endpoint or on the `/flows/{flowId}/segments` for Flow Segments which use the Media Object. + +## Deployment Considerations + +### Security + +The approach to supporting multiple Media Object instances in TAMS enables efficient re-use of media, changing ownership through the lifecycle of the media, and self-service re-location of media to meet the purposes of individual users +With this increased flexibility comes the potential for new attack vectors for malicious actors. + +Consider a Flow A with its Media Objects. +A malicious actor has read access to Flow A, but not read access. +The actor creates a new Flow, Flow B, and re-uses Media Objects from Flow A in Flow B. +The write permissions they have on Flow B allows them to add new instances to the Objects. +The malicious actor creates new malicious instances and adds them to the Objects. +Users of Flow A are now presented with the malicious instances, in addition to the original ones, on Flow A's Segments. + +This attack vector can be mitigated in multiple ways. + +The TAMS instance's authorisation logic may be configured to only allow those with write access to the original Flow A to add new instances. +This mitigates the attack vector described, but places more of a burden on the original owner to manage creation/deletion of duplicate instances. + +The TAMS instance may be configured to only allow managed duplication. +This guarantees all instances will be identical and removes the ability for malicious instances to be uploaded by the actor. + +An organisation may, of course, also assess and accept the risk associated with allowing user-managed duplication of Media Objects. From 3066e10f69cc1aafa01042f45b9652c4eeb3aac0 Mon Sep 17 00:00:00 2001 From: James Sandford Date: Mon, 4 Aug 2025 13:38:41 +0100 Subject: [PATCH 06/16] Apply suggestions from code review Co-authored-by: Sam Mesterton-Gibbons --- api/schemas/object-storage-post.json | 4 ++-- docs/adr/0038-improved-storage-management.md | 6 +++--- docs/appnotes/0018-managing-multiple-object-instances.md | 4 ++-- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/api/schemas/object-storage-post.json b/api/schemas/object-storage-post.json index e59dd4c9..0f41f840 100644 --- a/api/schemas/object-storage-post.json +++ b/api/schemas/object-storage-post.json @@ -1,6 +1,6 @@ { - "title": "Flow Storage Post", - "description": "Post data for the flow storage endpoint", + "title": "Object Storage Post", + "description": "Post data for the storage endpoint for working with existing objects", "type": "object", "required": ["storage_id"], "properties": { diff --git a/docs/adr/0038-improved-storage-management.md b/docs/adr/0038-improved-storage-management.md index 5a4ba434..6aa0f235 100644 --- a/docs/adr/0038-improved-storage-management.md +++ b/docs/adr/0038-improved-storage-management.md @@ -36,19 +36,19 @@ Chosen options: * Option 2b: Manage additional Object storage via a Objects storage endpoint * Option 3b: Call Object Instance management endpoint `instances` -These options have been chosen because they provide clearer boundaries between Media Objects and Segments in the data model and it's implementation. +These options have been chosen because they provide clearer boundaries between Media Objects and Segments in the data model and its implementation. They should avoid confusion arrising from changes to one Flow impacting another. And they minimise un-needed changes to the API and common workflows. ### Implementation -{Once the proposal has been implemented, add a link to the relevant PRs here} +Implemented by ## Pros and Cons of the Options ### Option 1a: Manage `get_urls` via the Flows endpoints -This option would see us add support for in-place editing of `get_urls` and see the edits propogated to other segments making use of the same Media Object. +This option would see us add support for in-place editing of `get_urls` and see the edits propagated to other segments making use of the same Media Object. * Good, because it somewhat matches existing patterns for updating `get_urls` * Good, because it would remove a race condition of the delete & re-create pattern with Object garbage-collection in implementations diff --git a/docs/appnotes/0018-managing-multiple-object-instances.md b/docs/appnotes/0018-managing-multiple-object-instances.md index 602735f2..ded182a9 100644 --- a/docs/appnotes/0018-managing-multiple-object-instances.md +++ b/docs/appnotes/0018-managing-multiple-object-instances.md @@ -111,7 +111,7 @@ There are many reasons a client may want to create a duplicate instance of a Med To create a backup. To create copies that are physically or logically closer to other systems. To move content to archive storage. -All while being able to refer to the this collection of duplicates with the same Media Object ID in Flow Segments. +All while being able to refer to this collection of duplicates with the same Media Object ID in Flow Segments. There are two methods of creating duplicate instances of a Media Object. The first requires the client to manage the duplication process. @@ -204,7 +204,7 @@ The approach to supporting multiple Media Object instances in TAMS enables effic With this increased flexibility comes the potential for new attack vectors for malicious actors. Consider a Flow A with its Media Objects. -A malicious actor has read access to Flow A, but not read access. +A malicious actor has read access to Flow A, but not write access. The actor creates a new Flow, Flow B, and re-uses Media Objects from Flow A in Flow B. The write permissions they have on Flow B allows them to add new instances to the Objects. The malicious actor creates new malicious instances and adds them to the Objects. From 35b782db3a3ff33414d237627b35c2eb0bc0576f Mon Sep 17 00:00:00 2001 From: James Sandford Date: Wed, 6 Aug 2025 15:45:09 +0100 Subject: [PATCH 07/16] Update docs/adr/0038-improved-storage-management.md Co-authored-by: Georgina Shippey <9370167+GeorginaShippey@users.noreply.github.com> --- docs/adr/0038-improved-storage-management.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/adr/0038-improved-storage-management.md b/docs/adr/0038-improved-storage-management.md index 6aa0f235..4a243033 100644 --- a/docs/adr/0038-improved-storage-management.md +++ b/docs/adr/0038-improved-storage-management.md @@ -15,7 +15,7 @@ And a move from thinking about "Segment reuse" to "Object reuse". Until now, the TAMS specifcation has been unclear on the ownership of `get_urls` being the Segment or Object. In particular, whether re-use of an Object should result in `get_urls` and changes to them being reflected across all segments using of them. -This ADR proposes explicitely linking ownership of `get_urls` to Objects, and providing mechanisms for adding and removing controlled and uncontrolled instances of Objects to their `get_urls` list. +This ADR proposes explicitly linking ownership of `get_urls` to Objects, and providing mechanisms for adding and removing controlled and uncontrolled instances of Objects to their `get_urls` list. This seperation of Objects and Segments does not require breaking changes, but does provide greater clarity in how the specification should be implemented. ## Considered Options From a372539a7d89b04d19a5580977ca225447ffdf12 Mon Sep 17 00:00:00 2001 From: James Sandford Date: Thu, 7 Aug 2025 14:21:12 +0100 Subject: [PATCH 08/16] Remove client managed duplication mechanism following feedback --- api/TimeAddressableMediaStore.yaml | 72 ++---------------- api/examples/object-storage-post-201.json | 21 ----- api/examples/object-storage-post.json | 3 - api/schemas/object-storage-post.json | 13 ---- api/schemas/object-storage.json | 56 -------------- api/schemas/objects-instances-post.json | 2 +- docs/adr/0038-improved-storage-management.md | 29 +++++++ ...0018-managing-multiple-object-instances.md | 76 +++---------------- 8 files changed, 48 insertions(+), 224 deletions(-) delete mode 100644 api/examples/object-storage-post-201.json delete mode 100644 api/examples/object-storage-post.json delete mode 100644 api/schemas/object-storage-post.json delete mode 100644 api/schemas/object-storage.json diff --git a/api/TimeAddressableMediaStore.yaml b/api/TimeAddressableMediaStore.yaml index 5feb39d4..53015887 100644 --- a/api/TimeAddressableMediaStore.yaml +++ b/api/TimeAddressableMediaStore.yaml @@ -2150,17 +2150,19 @@ paths: post: summary: Register a Media Object instance description: | - Signal that a media object instance is now available on a new storage backend and may be advertised in `get_urls`. Or add a new uncontrolled URL to `get_urls`. + Request the service to create an object instance on a new storage backend. Or add a new uncontrolled URL to `get_urls`. - Where a client has written a new object instance, the client is responsible for ensuring that the object written is complete and correct before registering it with this method. All instances of an object MUST be identical. - - API implementations MAY also support managed copying of media objects using this method. Where implementations support this feature, clients may POST a `storage_id` to this endpoint without previously allocating storage for media object `objectId` on storage backend `storage_id`. The API will then: + To request the duplication of the Object to a new storage backend, clients POST a `storage_id` to this endpoint that does not currently have an instance of the Object. The API will then: - Allocate storage for media object `objectId` on storage backend `storage_id` - Copy the media object from an existing location to the newly allocated storage - Start advertising the new copy in `get_urls` once ready - Where the above mechanism is not supported and the URL being registered is controlled, API implementations SHOULD verify that storage has previously been allocated on the requested storage backend `storage_id` for the media object `objectId`. + The API instances SHOULD be capable of handling the case where the only existant instances are uncontrolled. + + Where a client has written a new uncontrolled object instance, the client is responsible for ensuring that the object written is complete and correct before registering it with this method. + + All instances of an object MUST be identical. operationId: POST_objects-instances tags: - Objects @@ -2204,7 +2206,7 @@ paths: API instances should remove the Media Object instance from the `get_urls` list and then, if the instance is controlled, delete the object instance from storage. - API instances SHOULD prevent clients from deleting all Object instances. Where clients wish to remove all copies of an Object from the store, they should do so by deleting all Flows or Flow Segments which reference the Object. + API instances SHOULD prevent clients from deleting all Object instances. Additionally, API instances MAY prevent clients from deleting all controlled Object instances. Where clients wish to remove all copies of an Object from the store, they should do so by deleting all Flows or Flow Segments which reference the Object. operationId: DELETE_objects-instances tags: - Objects @@ -2234,64 +2236,6 @@ paths: description: Forbidden. You do not have permission to modify this Media Object. "404": description: The requested object ID in the path is invalid. - /objects/{objectId}/storage: - parameters: - - name: objectId - in: path - required: true - schema: - $ref: '#/components/schemas/uuid' - description: The object identifier. - post: - summary: Allocate Additional Object Storage - description: | - Allocate additional storage locations for writing media objects. - - Storage for object `objectId` will be allocated against storage backend `storage_id`. If the object already exists on the specified storage backend, the request will be rejected. - - The media store type, which is indicated in the /service resource, determines the information provided - in the response. The examples and description below are for the "http_object_store" media store type. - This media store type provides HTTP URLs for uploading and downloading media objects in buckets. - - The response will include a PUT URL that a client uses to upload the media object. The client is expected - to register the object instance using the `/objects/{objectId}/instances` endpoint once the upload is complete. - Implementations need to handle situations where objects were uploaded but no Media Object instance was registered - successfully. - - The response may include PUT URLs for creating buckets for the media objects. These PUT URLs should - be used before uploading media objects. The object_id associated with each storage location has the - bucket name as its prefix. - - The response may include PUT URLs for setting the CORS properties for the buckets and media objects. - - When making requests to the provided `put_url`, clients should include credentials if the provided - URL is on the same origin as the API itself, akin to the `same-origin` mode in the - [WhatWG Fetch Standard](https://fetch.spec.whatwg.org/#concept-request-credentials-mode). - operationId: POST_objects-objectId-storage - tags: - - MediaStorage - requestBody: - content: - application/json: - example: - $ref: examples/object-storage-post.json - schema: - $ref: schemas/object-storage-post.json - responses: - "201": - description: Storage locations for writing media objects. - content: - application/json: - schema: - $ref: schemas/object-storage.json - example: - $ref: examples/object-storage-post-201.json - "400": - description: Bad request. Invalid object storage request JSON. The storage backend `storage_id` may not exist. The object may already exist on storage backend. - "403": - description: Forbidden. You do not have permission to modify this media object. - "404": - description: The requested object does not exist. /flow-delete-requests: head: diff --git a/api/examples/object-storage-post-201.json b/api/examples/object-storage-post-201.json deleted file mode 100644 index 15a6b7fb..00000000 --- a/api/examples/object-storage-post-201.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "pre": [ - { - "action": "create_bucket", - "bucket_id": "tams-e2b89b02-21e7-5f9d-aa2d-db38b01453c9", - "put_url": { - "url": "https://example.store.com/tams-e2b89b02-21e7-5f9d-aa2d-db38b01453c9?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=0&X-Amz-Date=20230316T120329Z&X-Amz-Expires=300&X-Amz-SignedHeaders=host&X-Amz-Signature=0", - "body": "\n default\n\n" - } - } - ], - "media_objects": [ - { - "object_id": "tams-e2b89b02-21e7-5f9d-aa2d-db38b01453c9/846023d3-612d-5014-bc47-88f6eb2d04bb", - "put_url": { - "url": "https://example.store.com/tams-e2b89b02-21e7-5f9d-aa2d-db38b01453c9/846023d3-612d-5014-bc47-88f6eb2d04bb?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=0&X-Amz-Date=20230316T120329Z&X-Amz-Expires=300&X-Amz-SignedHeaders=content-type%3Bhost&X-Amz-Signature=0", - "content-type": "video/mp2t" - } - } - ] -} diff --git a/api/examples/object-storage-post.json b/api/examples/object-storage-post.json deleted file mode 100644 index 07f74050..00000000 --- a/api/examples/object-storage-post.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "storage_id": "60af2ab4-e8a5-4c65-a09b-d35983680315" -} diff --git a/api/schemas/object-storage-post.json b/api/schemas/object-storage-post.json deleted file mode 100644 index 0f41f840..00000000 --- a/api/schemas/object-storage-post.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "title": "Object Storage Post", - "description": "Post data for the storage endpoint for working with existing objects", - "type": "object", - "required": ["storage_id"], - "properties": { - "storage_id": { - "description": "The storage backend to allocate storage in. A storage backend identifier as advertised at the `/service` endpoint. If not set the default, as advertised at the `/service` endpoint, will be used if available. An invalid storage backend identifier will result in a 400 error.", - "type": "string", - "pattern": "^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$" - } - } -} diff --git a/api/schemas/object-storage.json b/api/schemas/object-storage.json deleted file mode 100644 index 487bdaad..00000000 --- a/api/schemas/object-storage.json +++ /dev/null @@ -1,56 +0,0 @@ -{ - "type": "object", - "title": "Media Bucket Object Store", - "description": "Gives information on storage for media objects. This schema is for the `http_object_store` media store type which provides URLs for storing media objects in bucket, and is the only store type currently implemented.", - "properties": { - "pre": { - "type": "array", - "description": "Actions that need to be taken before the media object can be written", - "maxItems": 1, - "items": { - "type": "object", - "description": "An action", - "required": [ - "action" - ], - "properties": { - "action": { - "type": "string", - "enum": [ - "create_bucket" - ] - }, - "bucket_id": { - "type": "string", - "description": "The name of the bucket that needs to be created" - }, - "put_url": { "$ref": "http-request.json" }, - "put_cors_url": { "$ref": "http-request.json" } - } - } - }, - "media_objects": { - "type": "array", - "description": "List of information for identifying and uploading media objects", - "minItems": 1, - "maxItems": 1, - "items": { - "type": "object", - "description": "Information for a media object", - "required": [ - "object_id", - "put_url" - ], - "properties": { - "object_id": { - "description": "The object store identifier for the media object.", - "type": "string" - }, - "put_url": { "$ref": "http-request.json" }, - "put_cors_url": { "$ref": "http-request.json" } - } - } - } - }, - "additionalProperties": false -} diff --git a/api/schemas/objects-instances-post.json b/api/schemas/objects-instances-post.json index 57e1c140..311aca6a 100644 --- a/api/schemas/objects-instances-post.json +++ b/api/schemas/objects-instances-post.json @@ -4,7 +4,7 @@ "title": "Media object registration", "oneOf": [ { - "description": "Register a controlled Media Object instance via its `storage_id`.", + "description": "Request the duplication of a Media Object instance to a new Storage Backend, via it's `storage_id`.", "title": "Controlled instance", "type": "object", "required": [ diff --git a/docs/adr/0038-improved-storage-management.md b/docs/adr/0038-improved-storage-management.md index 4a243033..72168f03 100644 --- a/docs/adr/0038-improved-storage-management.md +++ b/docs/adr/0038-improved-storage-management.md @@ -27,6 +27,8 @@ This seperation of Objects and Segments does not require breaking changes, but d * Option 2c: Manage additional Object storage AND initial Object storage via a Objects storage endpoint * Option 3a: Call Object Instance management endpoint `get_urls` * Option 3b: Call Object Instance management endpoint `instances` +* Option 4a: Duplicaion of Object Instances is managed by the Server +* Option 4b: Duplicaion of Object Instances is managed by the Client ## Decision Outcome @@ -35,10 +37,12 @@ Chosen options: * Option 1b: Add `get_urls` management to the Objects endpoint * Option 2b: Manage additional Object storage via a Objects storage endpoint * Option 3b: Call Object Instance management endpoint `instances` +* Option 4a: Duplicaion of Object Instances is managed by the Server These options have been chosen because they provide clearer boundaries between Media Objects and Segments in the data model and its implementation. They should avoid confusion arrising from changes to one Flow impacting another. And they minimise un-needed changes to the API and common workflows. +Option 4a also minamises potential new attack vectors. ### Implementation @@ -121,3 +125,28 @@ Another option is to title the endpoint `instances`. * Good, because it could avoid confusion over the one-to-many relationship of instances and URLs * Good, becuase it more clearly conveys that the client manages the instance, but the service manages the URL * Neutral, because it doesn't match the name of the property it affects + +### Option 4a: Duplicaion of Object Instances is managed by the Server + +This option would see client's request duplication of an Object to a new Storage Backend, and for that duplication to be carried out by the Server. + +* Good, because it requires minimal HTTP requests +* Good, because it ensures the copy is identical to the originating Instance +* Good, because it allows use of efficient copy mechanisms on storage backends +* Neutral, because it requires the server to carry out a task beyond processing metadata + * Given many object stores support duplication via a single request, it is likely to be more simple and efficient to implement and process than creating multiple pre-signed URLs, verifying Objects have been allocated on a given Storage Backend when registering, etc. +* Neutral, because it doesn't follow existing patterns for Object upload + * Though those patterns are for a subtly different purpose + +### Option 4b: Duplicaion of Object Instances is managed by the Client + +This option would see a similar pattern to the existing one for initial creation of objects used for duplication. +Clients would request storage allocation, upload the Media Object to that new location, and then register its availability with the server. + +* Good, because it follows existing patterns +* Neutral, because it only requires the server to carry out metadata management + * Though this may require more a complex implementation in practice +* Bad, because it requires more HTTP requests that Option 4a +* Bad, because it presents a potential attack vector + * A malicious actor could upload a maliciously crafted Object which doesn't match the original for it to be advertised against existing segments +* Bad, because it prevents the use of efficient object duplication methods present on some object stores diff --git a/docs/appnotes/0018-managing-multiple-object-instances.md b/docs/appnotes/0018-managing-multiple-object-instances.md index ded182a9..173887b5 100644 --- a/docs/appnotes/0018-managing-multiple-object-instances.md +++ b/docs/appnotes/0018-managing-multiple-object-instances.md @@ -105,7 +105,7 @@ The Segment above in `flowId` used the default `ts_offset` of `0:0`. As the Segment it was used in in `flowId` started at `20:0`, but in `flowId2` it is placed at `165:0`, we must set a `ts_offset` of `145:0`. For more information on `ts_offset`, see [here](https://bbc.github.io/tams/7.0/index.html#/operations/GET_flows-flowId-segments). -### Duplicating an Existing Object (Client Managed) +### Duplicating an Existing Object There are many reasons a client may want to create a duplicate instance of a Media Object. To create a backup. @@ -113,67 +113,8 @@ To create copies that are physically or logically closer to other systems. To move content to archive storage. All while being able to refer to this collection of duplicates with the same Media Object ID in Flow Segments. -There are two methods of creating duplicate instances of a Media Object. -The first requires the client to manage the duplication process. - -The client first has to allocate additional storage to the Media Object on the required Storage Backend. -This is done via a POST to `/objects/{objectId}/storage` with the required `storage_id`. - -Example POST body to `/objects/{objectId}/storage`: - -```json -{ - "storage_id": "323367fd-21bb-4f2e-ad38-faf048c4ccfc" -} -``` - -The response is identical in form to the `/flows/{flowId}/storage` response above: - -```json -{ - "pre": [ - { - "action": "create_bucket", - "bucket_id": "tams-c6b8e7cc-edd3-5f6d-9d79-4467d06eb8bf", - "put_url": { - "url": "https://example.store.com/tams-c6b8e7cc-edd3-5f6d-9d79-4467d06eb8bf?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=0&X-Amz-Date=20230316T120329Z&X-Amz-Expires=300&X-Amz-SignedHeaders=host&X-Amz-Signature=0", - "body": "\n default\n\n" - } - } - ], - "media_objects": [ - { - "object_id": "tams-c6b8e7cc-edd3-5f6d-9d79-4467d06eb8bf/846023d3-612d-5014-bc47-88f6eb2d04bb", - "put_url": { - "url": "https://example.store.com/tams-c6b8e7cc-edd3-5f6d-9d79-4467d06eb8bf/846023d3-612d-5014-bc47-88f6eb2d04bb?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=0&X-Amz-Date=20230316T120329Z&X-Amz-Expires=300&X-Amz-SignedHeaders=content-type%3Bhost&X-Amz-Signature=0", - "content-type": "video/mp2t" - } - } - ] -} -``` - -Once the media has been uploaded to the new Storage Backend, it must be registered as available. -This is done via a POST to `/objects/{objectId}/instances` with the `storage_id`. - -Example POST body to `/objects/{objectId}/instances`: - -```json -{ - "storage_id": "323367fd-21bb-4f2e-ad38-faf048c4ccfc" -} -``` - -The available instances of the Media Object will now be advertised in `get_urls` on the `/objects/{objectId}` endpoint and on the `/flows/{flowId}/segments` for all Flow Segments which use the Media Object. - -### Duplicating an Existing Object (Server Managed) - -Some TAMS implementations may support a second, server managed, method of duplicating Media Objects. - -For this method, clients POST the required `storage_id` to `/objects/{objectId}/instances`. -This is the same as the final step in the client managed approach. -The TAMS instance will identify that it has not previously allocated storage for the requested Media Object on the requested Storage Backend. -It will allocate storage, and populate it from an existing copy of the Media Object. +To initiate the duplication of a Media Object to a new Storage Backend, clients POST the required `storage_id` to `/objects/{objectId}/instances`. +The TAMS instance will allocate storage, and populate it from an existing copy of the Media Object. It will then begin advertising the copy in `get_urls` lists. Example POST body to `/objects/{objectId}/instances`: @@ -207,15 +148,18 @@ Consider a Flow A with its Media Objects. A malicious actor has read access to Flow A, but not write access. The actor creates a new Flow, Flow B, and re-uses Media Objects from Flow A in Flow B. The write permissions they have on Flow B allows them to add new instances to the Objects. -The malicious actor creates new malicious instances and adds them to the Objects. +The malicious actor creates new malicious uncontrolled instances with content different to the existing instances and adds them to the Objects. Users of Flow A are now presented with the malicious instances, in addition to the original ones, on Flow A's Segments. This attack vector can be mitigated in multiple ways. -The TAMS instance's authorisation logic may be configured to only allow those with write access to the original Flow A to add new instances. -This mitigates the attack vector described, but places more of a burden on the original owner to manage creation/deletion of duplicate instances. +The TAMS instance's authorisation logic may be configured to only allow those with specific permissions, such as write access to the original Flow A, to add new uncontrolled instances. +This mitigates the attack vector described, but places more of a burden on the original owner to manage creation/deletion of duplicate uncontrolled instances. The TAMS instance may be configured to only allow managed duplication. This guarantees all instances will be identical and removes the ability for malicious instances to be uploaded by the actor. -An organisation may, of course, also assess and accept the risk associated with allowing user-managed duplication of Media Objects. +Clients may also wish to exercise extra caution when using uncontrolled instances. +They may wish to favour controlled URLs or check that the URL is on a trusted domain, for example. + +An organisation may, of course, also assess and accept the risk associated with allowing user-managed creation of uncontrolled instances of Media Objects. From 52deaea7fc5bfeae5e98f6b96c48a69a1bab62c0 Mon Sep 17 00:00:00 2001 From: James Sandford Date: Thu, 7 Aug 2025 14:21:42 +0100 Subject: [PATCH 09/16] Update api/TimeAddressableMediaStore.yaml Co-authored-by: Georgina Shippey <9370167+GeorginaShippey@users.noreply.github.com> --- api/TimeAddressableMediaStore.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api/TimeAddressableMediaStore.yaml b/api/TimeAddressableMediaStore.yaml index 53015887..e6eda574 100644 --- a/api/TimeAddressableMediaStore.yaml +++ b/api/TimeAddressableMediaStore.yaml @@ -2217,7 +2217,7 @@ paths: required: true schema: type: string - - name: storge_id + - name: storage_id in: query description: The storage_id identifying the Media Object instance to be deleted. schema: From 5b3d2bfcf1099539180a84cb6d3e3859f966950c Mon Sep 17 00:00:00 2001 From: James Sandford Date: Fri, 15 Aug 2025 14:24:23 +0100 Subject: [PATCH 10/16] Add note about URL encoding special chars in object IDs --- api/TimeAddressableMediaStore.yaml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/api/TimeAddressableMediaStore.yaml b/api/TimeAddressableMediaStore.yaml index e6eda574..d06374e5 100644 --- a/api/TimeAddressableMediaStore.yaml +++ b/api/TimeAddressableMediaStore.yaml @@ -1985,7 +1985,7 @@ paths: parameters: - name: objectId in: path - description: The Media Object identifier. + description: The Media Object identifier. The Object ID may include special characters such as `/` which should be URL encoded. required: true schema: type: string @@ -2071,7 +2071,7 @@ paths: parameters: - name: objectId in: path - description: The Media Object identifier. + description: The Media Object identifier. The Object ID may include special characters such as `/` which should be URL encoded. required: true schema: type: string @@ -2169,7 +2169,7 @@ paths: parameters: - name: objectId in: path - description: The media object identifier. + description: The media object identifier. The Object ID may include special characters such as `/` which should be URL encoded. required: true schema: type: string @@ -2213,7 +2213,7 @@ paths: parameters: - name: objectId in: path - description: The Media Object identifier. + description: The Media Object identifier. The Object ID may include special characters such as `/` which should be URL encoded. required: true schema: type: string From 1064cfe5c6546088793a5d46dee516b3f5cf8ce1 Mon Sep 17 00:00:00 2001 From: James Sandford Date: Wed, 20 Aug 2025 11:33:03 +0100 Subject: [PATCH 11/16] Add `get_urls` to to objects endpoint GET example --- api/examples/objects-get-200.json | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/api/examples/objects-get-200.json b/api/examples/objects-get-200.json index 84b38102..1fe441a1 100644 --- a/api/examples/objects-get-200.json +++ b/api/examples/objects-get-200.json @@ -5,5 +5,10 @@ "4f79cfd1-c057-47f4-8e4d-1b126ca7bf34", "0fde9c11-da9d-434a-a113-d3b20a2cf251" ], - "first_referenced_by_flow": "4f79cfd1-c057-47f4-8e4d-1b126ca7bf34" -} \ No newline at end of file + "first_referenced_by_flow": "4f79cfd1-c057-47f4-8e4d-1b126ca7bf34", + "get_urls": [ + { + "url": "https://a4c52e22f45b4a0fa3ea3b9a42b35808-tams-demo.static.lon1.bbcis.uk/tams-7dffa41c-9e12-4e7d-a269-0aae8351d806.ts" + } + ] +} From 612b76c8d9a7eebc8c4bc94d8d4b87d83dc546c6 Mon Sep 17 00:00:00 2001 From: James Sandford Date: Tue, 23 Sep 2025 11:29:13 +0100 Subject: [PATCH 12/16] Remove pre-action paragraph mistakenly included in rebase --- api/TimeAddressableMediaStore.yaml | 4 ---- 1 file changed, 4 deletions(-) diff --git a/api/TimeAddressableMediaStore.yaml b/api/TimeAddressableMediaStore.yaml index d06374e5..f4089270 100644 --- a/api/TimeAddressableMediaStore.yaml +++ b/api/TimeAddressableMediaStore.yaml @@ -1938,10 +1938,6 @@ paths: The client is expected to register the Flow Segment using the [/flows/{flowId}/segments](#/operations/POST_flows-flowId-segments) endpoint once the upload is complete. Service implementations need to handle situations where Objects were uploaded but no Flow Segment was registered successfully. - The response may include PUT URLs for creating buckets for the Media Objects. - These PUT URLs should be used before uploading Media Objects. - The object_id associated with each storage location has the bucket name as its prefix. - When making requests to the provided `put_url`, clients should include credentials if the provided URL is on the same origin as the API itself, akin to the `same-origin` mode in the [WhatWG Fetch Standard](https://fetch.spec.whatwg.org/#concept-request-credentials-mode). operationId: POST_flows-flowId-storage tags: From 55d7003cba8c03f83fb492a5fd6fd1b4d1e1830f Mon Sep 17 00:00:00 2001 From: James Sandford Date: Tue, 23 Sep 2025 11:29:44 +0100 Subject: [PATCH 13/16] Update formatting to match style used in documentation audit --- api/TimeAddressableMediaStore.yaml | 30 ++++----- api/examples/objects-get-200.json | 2 +- api/schemas/flow-segment.json | 89 ++++++++++++++----------- api/schemas/object-core.json | 13 ++-- api/schemas/object.json | 12 ++-- api/schemas/objects-instances-post.json | 3 +- 6 files changed, 76 insertions(+), 73 deletions(-) diff --git a/api/TimeAddressableMediaStore.yaml b/api/TimeAddressableMediaStore.yaml index f4089270..598b6ff8 100644 --- a/api/TimeAddressableMediaStore.yaml +++ b/api/TimeAddressableMediaStore.yaml @@ -1827,7 +1827,7 @@ paths: The Flow Segment may in that case require the `get_urls` property to provide the information needed by clients to access the Media Object. Clients MAY modify Flow Segments, but this should only be done in exceptional circumstances to correct metadata as such operations will likely break the idempotency of Segments. - Properties of Media Objects, such as `get_urls`, SHOULD be modified via the `/objects` endpoints. + Properties of Media Objects, such as `get_urls`, SHOULD be modified via the [`/objects`](#/operations/GET_objects) endpoints. They SHOULD NOT be modified at this endpoint, and TAMS instences SHOULD reject such requests with a `400` error. If a client needs to modify a Flow Segment, e.g. to correct metadata such as the `key_frame_count` or add additional URLs to `get_urls`, then the client SHOULD first delete the existing Segment and then write a new one. @@ -2004,8 +2004,7 @@ paths: generate a large number of pre-signed URLs for example. Where multiple filter query parameters are provided, the returned `get_urls` will match all filters. schema: - type: string - pattern: ^([^,]+(,[^,]+)*)?$ + $ref: 'schemas/url-label-list.json' - name: accept_storage_ids in: query description: | @@ -2090,8 +2089,7 @@ paths: generate a large number of pre-signed URLs for example. Where multiple filter query parameters are provided, the returned `get_urls` will match all filters. schema: - type: string - pattern: ^([^,]+(,[^,]+)*)?$ + $ref: 'schemas/url-label-list.json' - name: accept_storage_ids in: query description: | @@ -2146,26 +2144,26 @@ paths: post: summary: Register a Media Object instance description: | - Request the service to create an object instance on a new storage backend. Or add a new uncontrolled URL to `get_urls`. + Request the service to create an Object instance on a new Storage Backend. Or add a new uncontrolled URL to `get_urls`. - To request the duplication of the Object to a new storage backend, clients POST a `storage_id` to this endpoint that does not currently have an instance of the Object. The API will then: + To request the duplication of the Object to a new Storage Backend, clients POST a `storage_id` to this endpoint that does not currently have an instance of the Object. The API will then: - - Allocate storage for media object `objectId` on storage backend `storage_id` - - Copy the media object from an existing location to the newly allocated storage + - Allocate storage for Media Object `objectId` on Storage Backend `storage_id` + - Copy the Media Object from an existing location to the newly allocated storage - Start advertising the new copy in `get_urls` once ready The API instances SHOULD be capable of handling the case where the only existant instances are uncontrolled. - Where a client has written a new uncontrolled object instance, the client is responsible for ensuring that the object written is complete and correct before registering it with this method. + Where a client has written a new uncontrolled Object instance, the client is responsible for ensuring that the Object written is complete and correct before registering it with this method. - All instances of an object MUST be identical. + All instances of an Object MUST be identical. operationId: POST_objects-instances tags: - Objects parameters: - name: objectId in: path - description: The media object identifier. The Object ID may include special characters such as `/` which should be URL encoded. + description: The Media Object identifier. The Object ID may include special characters such as `/` which should be URL encoded. required: true schema: type: string @@ -2192,15 +2190,15 @@ 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. delete: summary: Delete a Media Object instance description: | - Delete an instance of a media object. + Delete an instance of a Media Object. One of `storage_id` or `label` MUST be specified in the query parameters. `storage_id` SHOULD be used where `controlled` is `True` for the instance. - API instances should remove the Media Object instance from the `get_urls` list and then, if the instance is controlled, delete the object instance from storage. + API instances should remove the Media Object instance from the `get_urls` list and then, if the instance is controlled, delete the Object instance from storage. API instances SHOULD prevent clients from deleting all Object instances. Additionally, API instances MAY prevent clients from deleting all controlled Object instances. Where clients wish to remove all copies of an Object from the store, they should do so by deleting all Flows or Flow Segments which reference the Object. operationId: DELETE_objects-instances @@ -2231,7 +2229,7 @@ paths: "403": description: Forbidden. You do not have permission to modify this Media Object. "404": - description: The requested object ID in the path is invalid. + description: The requested Object ID in the path is invalid. /flow-delete-requests: head: diff --git a/api/examples/objects-get-200.json b/api/examples/objects-get-200.json index 1fe441a1..3fd00264 100644 --- a/api/examples/objects-get-200.json +++ b/api/examples/objects-get-200.json @@ -8,7 +8,7 @@ "first_referenced_by_flow": "4f79cfd1-c057-47f4-8e4d-1b126ca7bf34", "get_urls": [ { - "url": "https://a4c52e22f45b4a0fa3ea3b9a42b35808-tams-demo.static.lon1.bbcis.uk/tams-7dffa41c-9e12-4e7d-a269-0aae8351d806.ts" + "url": "https://store.example.com/tams-e2b89b02-21e7-5f9d-aa2d-db38b01453c9/846023d3-612d-5014-bc47-88f6eb2d04bb" } ] } diff --git a/api/schemas/flow-segment.json b/api/schemas/flow-segment.json index 683a10df..cc49fb56 100644 --- a/api/schemas/flow-segment.json +++ b/api/schemas/flow-segment.json @@ -1,43 +1,52 @@ { - "type": "object", - "description": "Provides the location and metadata of the media files corresponding to timerange segments of a Flow.", - "title": "Flow Segment", - "allOf":[ - { - "type": "object", - "required": [ - "object_id", - "timerange" - ], - "properties": { - "object_id" : { - "description": "The object store identifier for the media object.", - "type": "string" + "type": "object", + "description": "Provides the location and metadata of the media files corresponding to timerange segments of a Flow.", + "title": "Flow Segment", + "allOf": + [ + { + "type": "object", + "required": + [ + "object_id", + "timerange" + ], + "properties": + { + "object_id": + { + "description": "The object store identifier for the Media Object.", + "type": "string" + }, + "ts_offset": + { + "description": "The timestamp offset between the sample timestamps stored in the media file and the corresponding timestamp in the Segment, ie. ts_offset = segment ts - media object ts. Assumed to be 0:0 if not set. Format as described by the [Timestamp](../schemas/timestamp#top) type", + "$ref": "timestamp.json" + }, + "timerange": + { + "description": "The timerange for the samples contained in the Segment. The timerange start is always inclusive. If samples have a duration then the timerange end is exclusive and covers at least the duration of the last sample. The exclusive timerange end will typically be set to the timestamp of the next sample. If the samples don't have a duration then the timerange end is inclusive. Format is described by the [TimeRange](../schemas/timerange#top) type. Note that where temporal re-ordering is used, the timerange and samples refers to the presentation timeline.", + "$ref": "timerange.json" + }, + "last_duration": + { + "description": "The difference between the exclusive end of the `timerange` and the last sample timestamp. Format as described by the [Timestamp](../schemas/timestamp#top) type, but cannot be negative", + "$ref": "timestamp.json" + }, + "sample_offset": + { + "description": "The start of the Segment represented as a count of samples from the start of the Media Object. Note that a sample is a video frame or audio sample. A (coded) audio frame has multiple audio samples. Assumed to be 0 if not set.", + "type": "integer" + }, + "sample_count": + { + "description": "The count of samples in the Segment (which may be fewer than in the Media Object). The count could be less than expected given the Segment duration and rate if there are gaps. If not set, every sample from sample_offset onwards is used. Note that a sample is a video frame or audio sample. A (coded) audio frame has multiple audio samples", + "type": "integer" + } + } }, - "ts_offset": { - "description": "The timestamp offset between the sample timestamps stored in the media file and the corresponding timestamp in the segment, ie. ts_offset = segment ts - media object ts. Assumed to be 0:0 if not set. Format as described by the [Timestamp](../schemas/timestamp#top) type", - "$ref": "timestamp.json" - }, - "timerange": { - "description": "The timerange for the samples contained in the segment. The timerange start is always inclusive. If samples have a duration then the timerange end is exclusive and covers at least the duration of the last sample. The exclusive timerange end will typically be set to the timestamp of the next sample. If the samples don't have a duration then the timerange end is inclusive. Format is described by the [TimeRange](../schemas/timerange#top) type. Note that where temporal re-ordering is used, the timerange and samples refers to the presentation timeline.", - "$ref": "timerange.json" - }, - "last_duration": { - "description": "The difference between the exclusive end of the `timerange` and the last sample timestamp. Format as described by the [Timestamp](../schemas/timestamp#top) type, but cannot be negative", - "$ref": "timestamp.json" - }, - "sample_offset": { - "description": "The start of the segment represented as a count of samples from the start of the object. Note that a sample is a video frame or audio sample. A (coded) audio frame has multiple audio samples. Assumed to be 0 if not set.", - "type": "integer" - }, - "sample_count": { - "description": "The count of samples in the segment (which may be fewer than in the object). The count could be less than expected given the segment duration and rate if there are gaps. If not set, every sample from sample_offset onwards is used. Note that a sample is a video frame or audio sample. A (coded) audio frame has multiple audio samples", - "type": "integer" + { + "$ref": "object-core.json" } - } - }, - { - "$ref": "object-core.json" - } - ] -} + ] +} \ No newline at end of file diff --git a/api/schemas/object-core.json b/api/schemas/object-core.json index e188b391..77cfb42a 100644 --- a/api/schemas/object-core.json +++ b/api/schemas/object-core.json @@ -1,10 +1,10 @@ { "type": "object", - "description": "Provides the location and metadata of the media files corresponding to a media Object.", + "description": "Provides the location and metadata of the media files corresponding to a Media Object.", "title": "Object", "properties": { "get_urls": { - "description": "A list of URLs to which a GET request can be made to directly retrieve the contents of the media object. This is required by the `http_object_store` media store type, which is the only one currently described. Clients may choose any URL in the list and treat the content returned as identical, however servers may sort the list such that the preferred URL is first. Storage backend metadata for controlled URLs should be populated by the TAMS instance based on the storage backend the object copy resides in.", + "description": "A list of URLs to which a GET request can be made to directly retrieve the contents of the Media Object. This is required by the `http_object_store` Storage Backend type, which is the only one currently described. Clients may choose any URL in the list and treat the content returned as identical, however servers may sort the list such that the preferred URL is first. Storage Backend metadata for controlled URLs should be populated by the TAMS instance based on the Storage Backend the Meda Object instance resides in.", "type": "array", "items": { "type": "object", @@ -20,9 +20,8 @@ ], "properties": { "storage_id": { - "description": "Storage backend identifier", - "type": "string", - "pattern": "^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$" + "description": "Storage Backend identifier", + "$ref": "uuid.json" }, "url": { "description": "A URL to which a GET request can be made to directly retrieve the contents of the media object. Clients should include credentials if the provide URL is on the same origin as the API endpoint", @@ -37,7 +36,7 @@ "type": "string" }, "controlled": { - "description": "If `true`, this URL is on a storage backend controlled by this service instance. If `false`, this URL is uncontrolled and does not have it's lifecycle managed by this instance. If this parameter is unset, assume `true`.", + "description": "If `true`, this URL is on a Storage Backend controlled by this service instance. If `false`, this URL is uncontrolled and does not have it's lifecycle managed by this instance. If this parameter is unset, assume `true`.", "type": "boolean" } } @@ -46,7 +45,7 @@ } }, "key_frame_count": { - "description": "The number of key frames in the media object. This should be set greater than zero when the media object contains key frames that serve as a stream access point", + "description": "The number of key frames in the Media Object. This should be set greater than zero when the Media Object contains key frames that serve as a stream access point", "type": "integer" } } diff --git a/api/schemas/object.json b/api/schemas/object.json index 7726b223..6ee07abb 100644 --- a/api/schemas/object.json +++ b/api/schemas/object.json @@ -9,21 +9,19 @@ ], "properties": { "id": { - "description": "The media object identifier.", + "description": "The Media Object identifier.", "type": "string" }, "referenced_by_flows": { "type": "array", - "description": "List of Flows that reference this media object via Flow Segments in this store.", + "description": "List of Flows that reference this Media Object via Flow Segments in this store instance.", "items": { - "type": "string", - "pattern": "^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$" + "$ref": "uuid.json" } }, "first_referenced_by_flow": { - "description": "The first Flow that had a Flow Segment reference the media object in this store. This Flow is also present in 'referenced_by_flows' if it is still referenced by the Flow. This property is optional and may in some implementations become unset if the Flow no longer references the media object, e.g. because it was deleted.", - "type": "string", - "pattern": "^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$" + "description": "The first Flow that had a Flow Segment reference the Media Object in this store instance. This Flow is also present in 'referenced_by_flows' if it is still referenced by the Flow. This property is optional and may in some implementations become unset if the Flow no longer references the media object, e.g. because it was deleted.", + "$ref": "uuid.json" } } }, diff --git a/api/schemas/objects-instances-post.json b/api/schemas/objects-instances-post.json index 311aca6a..74cc4f46 100644 --- a/api/schemas/objects-instances-post.json +++ b/api/schemas/objects-instances-post.json @@ -13,8 +13,7 @@ "properties": { "storage_id": { "description": "Storage backend identifier", - "type": "string", - "pattern": "^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$" + "$ref": "uuid.json" } } }, From a57c4a195d7f650c5fe4792854c7bee5fb7fe47c Mon Sep 17 00:00:00 2001 From: James Sandford Date: Tue, 23 Sep 2025 11:39:39 +0100 Subject: [PATCH 14/16] Remove mention of pre-actions from appnote 0018 --- ...0018-managing-multiple-object-instances.md | 23 ++----------------- 1 file changed, 2 insertions(+), 21 deletions(-) diff --git a/docs/appnotes/0018-managing-multiple-object-instances.md b/docs/appnotes/0018-managing-multiple-object-instances.md index 173887b5..b7a50850 100644 --- a/docs/appnotes/0018-managing-multiple-object-instances.md +++ b/docs/appnotes/0018-managing-multiple-object-instances.md @@ -30,16 +30,6 @@ Example response: ```json { - "pre": [ - { - "action": "create_bucket", - "bucket_id": "tams-e2b89b02-21e7-5f9d-aa2d-db38b01453c9", - "put_url": { - "url": "https://example.store.com/tams-e2b89b02-21e7-5f9d-aa2d-db38b01453c9?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=0&X-Amz-Date=20230316T120329Z&X-Amz-Expires=300&X-Amz-SignedHeaders=host&X-Amz-Signature=0", - "body": "\n default\n\n" - } - } - ], "media_objects": [ { "object_id": "tams-e2b89b02-21e7-5f9d-aa2d-db38b01453c9/846023d3-612d-5014-bc47-88f6eb2d04bb", @@ -52,19 +42,10 @@ Example response: } ``` -In the response above, you can see a "pre" action alongside the the Media Object. -This may be returned where a service implementation maintains multiple buckets on a storage backend. There is no requirement for a client to use all Media Objects they request. This allows a client to request allocation of Objects in bulk, improving efficiency. -But it also means that a bucket could be created and not used. -This could result in users incurring unnecessary costs. -A service may therefore require the client to trigger its creation. -Note that the request may include pre-actions for multiple buckets. -The bucket a Media Object is associated with is pre-fixed to the `object_id`. -As such, the client should only run the pre-action for a given bucket when it first wishes to populate a Media Object in that bucket. - -In the example above, a client would first trigger the creation of the bucket by a PUT request to the specified pre-signed URL with the specified body. -The client would then PUT the Media Object's file to the `put_url` for one of the Media Object's in the list, with the `content-type` on the request set to the specified value. + +In the example above, a client would PUT the Media Object's file to the `put_url` for one of the Media Object's in the list, with the `content-type` on the request set to the specified value. Once the media Object has been uploaded, it should be registered on the Flow's timeline via a Segment. The appropriate Object ID from the requests above, and the Timerange it covers should be registered via a POST request to the [`/flows/{flowId}/segments`](https://bbc.github.io/tams/7.0/index.html#/operations/POST_flows-flowId-segments) endpoint. From 52a52d494ffb3bede3b57681ec8cf4afdca137fb Mon Sep 17 00:00:00 2001 From: James Sandford Date: Tue, 23 Sep 2025 11:45:15 +0100 Subject: [PATCH 15/16] Mark ADR0038 as accepted --- docs/adr/0038-improved-storage-management.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/adr/0038-improved-storage-management.md b/docs/adr/0038-improved-storage-management.md index 72168f03..792a1fa5 100644 --- a/docs/adr/0038-improved-storage-management.md +++ b/docs/adr/0038-improved-storage-management.md @@ -1,5 +1,5 @@ --- -status: "proposed" +status: "accepted" --- # Improved Storage Management From d3e926fdbadfbd9280be89411f10b78bce9e45fb Mon Sep 17 00:00:00 2001 From: James Sandford Date: Tue, 23 Sep 2025 14:35:21 +0100 Subject: [PATCH 16/16] Update `read_only` property description to make sense with updated object management model --- api/TimeAddressableMediaStore.yaml | 4 ++-- api/schemas/flow-core.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/api/TimeAddressableMediaStore.yaml b/api/TimeAddressableMediaStore.yaml index 598b6ff8..0146237c 100644 --- a/api/TimeAddressableMediaStore.yaml +++ b/api/TimeAddressableMediaStore.yaml @@ -1286,7 +1286,7 @@ paths: $ref: '#/components/responses/trait_resource_info_head_404' get: summary: Flow Read-Only - description: Returns the Flow read_only property. If set to 'true', service implementations SHOULD reject client requests to update Flow metadata (other than the read_only property), Flow Segments and Media Objects. + description: Returns the Flow read_only property. If set to 'true', service implementations SHOULD reject client requests to update Flow metadata (other than the read_only property), and Flow Segments. Service implementations should also reject requests to the [`/flows/{flowId}/storage`](#/operations/POST_flows-flowId-storage) endpoint for the Flow, and requests to delete the Flow. operationId: GET_flows-flowId-read-only tags: - Flows @@ -1302,7 +1302,7 @@ paths: description: The requested Flow does not exist. put: summary: Set Flow Read-Only - description: Set the read-only property. If set to 'true', service implementations SHOULD reject client requests to update Flow metadata (other than the read_only property), Flow Segments and Media Objects. + description: Set the read-only property. If set to 'true', service implementations SHOULD reject client requests to update Flow metadata (other than the read_only property), and Flow Segments. Service implementations should also reject requests to the [`/flows/{flowId}/storage`](#/operations/POST_flows-flowId-storage) endpoint for the Flow, and requests to delete the Flow. operationId: PUT_flows-flowId-read-only tags: - Flows diff --git a/api/schemas/flow-core.json b/api/schemas/flow-core.json index dee6badf..2ad77c37 100644 --- a/api/schemas/flow-core.json +++ b/api/schemas/flow-core.json @@ -75,7 +75,7 @@ }, "read_only": { - "description": "If set to 'true', service implementations SHOULD reject client requests to update Flow metadata (other than the read_only property), Flow Segments and Media Objects", + "description": "If set to 'true', service implementations SHOULD reject client requests to update Flow metadata (other than the read_only property), and Flow Segments. Service implementations should also reject requests to the [`/flows/{flowId}/storage`](#/operations/POST_flows-flowId-storage) endpoint for the Flow, and requests to delete the Flow.", "type": "boolean" }, "codec":