Authorization duplicates route parsing and repository resolution on every request

Follow-up to the authorization stack starting at !983 (merged). Once the stack completes (format RouteParser implementations plus dispatcher wiring), every request to a format mount performs two pieces of work twice. This is accepted technical architecture debt: it should be solved once authorization is fully in place for all formats, not while the seams are still landing.

🔁 1. The URL path is parsed twice

  • The authorization middleware's RouteParser parses the path to recover the target repository and ADR-021 action (internal/authz/middleware.go).
  • The format dispatcher then parses the same path again to route the request. For OCI, ParseRoute in internal/format/oci/handler.go already implements the full /v2/<slug>/container/<repository>/<image>/<action-suffix> grammar (right-edge action peel, multi-segment image names).

Two implementations of one grammar can drift, and drift is exactly the failure class the authorization layer fails closed on: a route the dispatcher serves but the authorization parser misses is denied with 404, while the inverse passes authorization and then 404s at dispatch. Every additional format doubles its grammar the same way. The parallel structure is already visible in the types: authz.ParsedRoute{Slug, RepositoryName, Action} next to oci.ParsedRoute{Slug, RepositoryName, ImageName, Action, Reference}.

🔁 2. The repository is resolved twice

  • Assembly.resolveTarget calls namespace.Resolver.ResolveRepository and keeps only the ids (internal/authz/middleware.go).
  • The format handler then resolves the same <slug>/<repository_name> pair again (for example internal/format/oci/manifest.go and internal/format/oci/upload.go).

That duplicates the resolution's DB round trips on every authorized request and opens a small window where the two lookups observe different rows.

🔍 Root cause

Authorization runs as a chain middleware before the dispatcher, and nothing it computes (parsed route, repository resolution) is published for downstream layers — so the dispatcher and handlers recompute both.

💡 Candidate directions

  • Parse once and attach the parsed route to the request context as first-class route identity, consumed by both the authorization layer and the format handlers (the pattern the GitLab container registry follows).
  • Publish the authorization layer's Resolution into the request context for handlers to reuse.
  • Alternatively, run the authorization decision inside the dispatcher after its single parse, instead of as a pre-dispatch middleware.

Interim mitigation available today, without solving the debt: each format's RouteParser implementation should delegate to the format's existing parser (for OCI, ParseRoute) and only add the action mapping, so the grammar exists once even while it is invoked twice.

Related to !983 (merged)