Summary
get_signed_artifact_url issues GCS signed URLs with expiration=datetime.timedelta(days=7) — the maximum GCS allows. A signed URL is the only read in this API that is a detached bearer credential: it works with no Tangle authentication, from any network, for anyone who holds the string, and it cannot be revoked before it expires.
cloud_pipelines_backend/api_server_sql.py (at ac5599b):
signed_url = blob.generate_signed_url(
# Expiration is required. Max expiration value is 7 days.
expiration=datetime.timedelta(days=7)
)
The comment states the ceiling; the code then uses it as the value. A week is far longer than any consumer needs.
Why the lifetime is the part that matters
This came out of a security review that also flagged the endpoint for having no ownership check. That part is worth stating precisely, because it is not an anomaly in this endpoint:
router.get(
"/api/artifacts/{id}/signed_artifact_url", tags=["artifacts"], **default_config
)(inject_session_dependency(artifact_service.get_signed_artifact_url))
No GET route in api_router.py passes dependencies=, and none filter by created_by — not /api/artifacts/{id}, /api/pipeline_runs/, /api/pipeline_runs/{id}, /api/executions/{id}/details, nor the container-log routes. ensure_user_can_write gates mutating routes and ensure_admin_user gates admin ones; reads are open to any authenticated user by design. Adding an ownership check to this one endpoint would be inconsistent with the rest of the read surface and would not reduce exposure much, since the same caller can already read the artifact's metadata and its producing execution's logs through routes that are equally open.
What is unique here is that every other read requires passing the deployment's authentication on each request, whereas a signed URL keeps working for 7 days once it leaves that channel — pasted into Slack, attached to a ticket, captured in browser history, or logged by an intermediary. The function validates only existence, a non-empty uri, is_dir, and a gs:// prefix, so the URL is minted for any artifact id the caller can name.
Proposed change
Reduce the expiration to 1 hour, and make it a named module-level constant rather than an inline literal.
That is comfortably within what consumers need:
- The URL is fetched per view via
getArtifactSignedUrl in tangle-ui's src/services/executionService.ts, keyed in react-query on the artifact id, so a new view gets a new URL rather than reusing a cached one.
TextVisualizer and CsvVisualizer fetch the object immediately on receiving the URL.
ParquetVisualizer / openParquet issue HTTP range reads against the URL across a viewing session, so the TTL has to outlast a session rather than a single request. An hour covers that with a wide margin.
Options considered
- A few minutes. Tighter, but risks expiring mid-session for the Parquet viewer's ranged reads, and would need the UI to refresh the URL on a 403. Worth doing later together with that refresh, not as a first step.
- An ownership check on this endpoint. Rejected for now, for the reason above: it would be the only ownership-filtered read in the API and would not meaningfully narrow what the caller can already reach.
- Whether the whole read surface should be ownership- or team-scoped. A real question, but a platform-level decision affecting every
GET route and both the UI and SDK. Out of scope here; worth its own issue if there is appetite.
Scope
Pre-existing and identical in every deployment — it is not specific to any one environment, and predates the data-warehouse environments whose review surfaced it. Signing goes through IAM signBlob with the runtime service account, so the URL conveys exactly that account's read access on the object.
🤖 Filed with AI assistance. Code references pinned to ac5599b on master.
Summary
get_signed_artifact_urlissues GCS signed URLs withexpiration=datetime.timedelta(days=7)— the maximum GCS allows. A signed URL is the only read in this API that is a detached bearer credential: it works with no Tangle authentication, from any network, for anyone who holds the string, and it cannot be revoked before it expires.cloud_pipelines_backend/api_server_sql.py(atac5599b):The comment states the ceiling; the code then uses it as the value. A week is far longer than any consumer needs.
Why the lifetime is the part that matters
This came out of a security review that also flagged the endpoint for having no ownership check. That part is worth stating precisely, because it is not an anomaly in this endpoint:
No
GETroute inapi_router.pypassesdependencies=, and none filter bycreated_by— not/api/artifacts/{id},/api/pipeline_runs/,/api/pipeline_runs/{id},/api/executions/{id}/details, nor the container-log routes.ensure_user_can_writegates mutating routes andensure_admin_usergates admin ones; reads are open to any authenticated user by design. Adding an ownership check to this one endpoint would be inconsistent with the rest of the read surface and would not reduce exposure much, since the same caller can already read the artifact's metadata and its producing execution's logs through routes that are equally open.What is unique here is that every other read requires passing the deployment's authentication on each request, whereas a signed URL keeps working for 7 days once it leaves that channel — pasted into Slack, attached to a ticket, captured in browser history, or logged by an intermediary. The function validates only existence, a non-empty
uri,is_dir, and ags://prefix, so the URL is minted for any artifact id the caller can name.Proposed change
Reduce the expiration to 1 hour, and make it a named module-level constant rather than an inline literal.
That is comfortably within what consumers need:
getArtifactSignedUrlintangle-ui'ssrc/services/executionService.ts, keyed in react-query on the artifact id, so a new view gets a new URL rather than reusing a cached one.TextVisualizerandCsvVisualizerfetch the object immediately on receiving the URL.ParquetVisualizer/openParquetissue HTTP range reads against the URL across a viewing session, so the TTL has to outlast a session rather than a single request. An hour covers that with a wide margin.Options considered
GETroute and both the UI and SDK. Out of scope here; worth its own issue if there is appetite.Scope
Pre-existing and identical in every deployment — it is not specific to any one environment, and predates the data-warehouse environments whose review surfaced it. Signing goes through IAM
signBlobwith the runtime service account, so the URL conveys exactly that account's read access on the object.🤖 Filed with AI assistance. Code references pinned to
ac5599bonmaster.