Skip to content

API Server - Artifacts - Shorten signed artifact URL lifetime from the 7-day GCS maximum #357

Description

@morgan-wowk

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.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions