Allow token authentication for design management images
What does this MR do and why?
Design management images (/-/design_management/designs/:id/(:sha/)raw_image and …/resized_image/:size) are currently only accessible to session-authenticated (browser) requests. API clients can read all design metadata — including the image URLs — through GraphQL (DesignManagement::Design.image / imageV432x230), but then have no way to actually fetch those images on non-public projects:
- the routes are not part of the REST or GraphQL API (the REST
Designentity'simage_urlpoints back at this same web route), and - web requests only accept PAT/OAuth tokens for an allowlist of request formats — RSS/ICS,
/api, repository archives, release downloads (Gitlab::Auth::AuthFinders#valid_web_access_format?) — which design images are not part of.
So a token that can read everything about a design cannot download it: clients (mobile apps, integrations, exporters) get a sign-in redirect instead of the image.
This MR adds a :design sessionless authentication format, valid only on design management image paths, and authenticates it in Projects::DesignManagement::DesignsController show actions — the same mechanism (and motivation) previously used for repository archives (#28978 (closed)) and release downloads.
Notes for reviewers:
- Authorization is unchanged.
authorize_read_design!(can?(current_user, :read_design, design)) still runs on every request; this only adds an authentication transport for a read-only endpoint. - Legacy PAT/OAuth tokens must carry the
apiorread_apiscope: the dedicated:designbranch inRequestAuthenticator#find_sessionless_usercallsfind_user_from_web_access_token(:design, scopes: [:api, :read_api])directly and deliberately bypasses the broaderfind_user_from_any_authentication_methodchain, so feed, static object, and CI job tokens can never authenticate design images. Insufficient scopes keep behaving as unauthenticated. - Granular (fine-grained) PATs skip the coarse scope check by design; they are gated by
authorize_granular_token!on theread_designpermission instead, which is bundled into Work Item: Read. On public projects, granular tokens without an explicit scope keep working throughAuthz::BoundaryPolicy's anonymous fallback (anonymous_can_read_design). - Tokens are accepted from headers only (
PRIVATE-TOKENorAuthorization: Bearer). Requests carrying a token in the query string (private_token,access_token, orbearer_token) fail closed, per the access token guardrails. - Project/group access tokens and service accounts (bot users) are intentionally not added to
RequestAuthenticator#can_sign_in_bot?, matching release downloads. Enabling them would deliberately widen bot sessionless sign-in, so it is planned as a separate follow-up with its own authentication and AppSec review.
What changed since the first review round
- The
:designformat got a dedicated branch infind_sessionless_user("Narrow design request auth to web access tokens"), so only web access tokens are ever consulted. - The temporary outright denial of granular tokens was replaced by real granular support, folded in from !246945 (merged) (merged into this branch rather than retargeted to
master): it defines theread_designraw permission, grants it through the existing Work Item: Read assignable permission — matching how design metadata and image URLs are already exposed to granular tokens in GraphQL — and mapsdesign: :read_designinGRANULAR_FORMAT_PERMISSIONS. - Query string tokens are rejected ("Reject query string tokens on design image endpoints"): the endpoints are header-only from day one, so the parameter never has to be deprecated later.
- Documentation was added: a task section on the design management page and a row in the fine-grained token operations inventory.
How to set up and validate locally
-
On a private project, add a design to an issue (issue → Designs → upload), and create a personal access token with the
read_apiscope. -
Get the design's image URL via GraphQL:
{ project(fullPath: "<path>") { issue(iid: "<iid>") { designCollection { designs(first: 1) { nodes { image } } } } } } -
Without this change:
curl -i -H "PRIVATE-TOKEN: <token>" "<image url>"redirects to/users/sign_in. -
With this change: the same request returns the image (
200,Content-Type: image/…). A token with an insufficient scope (for exampleread_user) still gets the sign-in redirect, a token whose user cannot read the design still gets a 404, and passing the token as?private_token=instead of a header gets the sign-in redirect.
MR acceptance checklist
Please evaluate this MR against the MR acceptance checklist. It helps you analyze changes to reduce risks in quality, performance, reliability, security, and maintainability.