docs(plans): close the gaps in the per-user token-exchange plan

What

Stacked on !1969 (merged), targeting its branch. Lands the spec amendments that plan names but does not carry, and fills the gaps a reviewer would otherwise raise against the plan document.

Merge !1969 (merged) first; this then merges into it.

Spec amendments

!1969 (merged)'s Spec Reference and MR description both say it amends the specs the change falsifies. Its diff is two files, the plan and the plans index. This MR is those amendments.

monolith/S02, across nine sections:

  • Credential acquisition gains the target contract for the per-user default: the seam mints a short-lived RS256 JWT whose aud carries both gitlab-artifact-registry and gitlab-iam-data-access, and yields nothing for a principal that cannot hold one. The paragraph states the shipped state and the target separately, so it reads correctly either side of the step landing.
  • The ADR-014 OQ#2 dependency row moves from Open (deferred) to Approved. ADR-020 R1-R3 settles the credential format. What stays open is the seam that calls it, not the contract.
  • Nine sites attributed the per-user seam's interim provider to monolith/S03. No such wiring was ever built: the client's one production construction site, CachesClient#artifact_registry_client, passes current_user: and no providers, and S03's own spec answers "None, that is monolith/S02" on the credential question. All nine are corrected. Seven named S03 outright; two said only "the consuming slice", which is why a grep keyed on S03 near the credential text found neither.
  • The per-user plaintext Follow-up reasoned that the residual stays latent until the first functional exchange, without saying what closes it. It now records that extending guard_service_transport! is not the closure.

monolith/S03 and monolith/S04 each deferred the real token exchange to "the S02 credential seam and ADR-014 open question 2". Both now name the seam's own default as the acquirer, name ADR-020 as the settled format, and record that neither slice injects a provider.

Carried in the same amendment, and not this plan's subject. S02's base-URL criterion, error case, and security section named ArgumentError where the merged client raises ArtifactRegistry::Client::ConfigurationError, landed by gitlab-org/gitlab!250785 (merged). That plan never claimed the spec amendment, so the reconcile was unowned. It rides here because the per-user amendment edits the same sections, and it changes nothing about the step. Say the word and I will split it into its own MR.

Plan document

  • Per-step fields. Step 1 gains Shared seams, Feature flag, and i18n / changelog, which the plan template requires. Its Type becomes type::feature: the monolith labels by type:: rather than by a Conventional Commits prefix.
  • Plan token. Four plans under docs/plans/monolith/ declare monolith/S02 as their Spec Reference, so a bare S02 names none of them. Naming Conventions now carries the qualified token, and the step MR title ends (S02 per-user token exchange plan: 1/1).
  • Three research findings. The two gates the seam already sits behind (ArtifactRegistry::AcquiresClient on artifact_registry_ui, and read_artifact_registry on the types and the mutation base); the deliberate per-user HTTPS exemption; and the five request specs that stub past the null default.
  • Three Approach positions the document did not state: the cleartext in-cluster hop as an accepted residual, a signing-key failure escaping rather than being rescued, and the ADR-020 R1 enablement check staying out of the seam.
  • The entitlement finding drops access_artifact_registry_service. git grep finds it nowhere in origin/master, so naming it sends the next reader looking for a symbol that is not there. The negative claim stands on its own.

The finding worth the most review time

The plan's Dependencies listed the per-user plaintext residual as an issue to file, and named guard_service_transport! as refusing plaintext on the service path only. What that leaves out is why the obvious closure is the wrong one: gitlab-org/gitlab!249808 (merged) introduced the per-user exemption on 2026-08-18 and records that GitLab.com addresses AR over plaintext in-cluster HTTP. Extending the guard to the per-user path would therefore refuse every per-user AR call on .com the moment it merged.

Two facts also bound the residual the other way, and neither was stated: the minted token names gitlab-iam-data-access alongside the AR audience, so a captured one is not scoped to AR; and ADR-020 records no revocation before exp. The residual is now written up as accepted for a flag-off rollout, owned by S02's Follow-ups and ADR-014 open question 1, with no new tracking item.

Two smaller corrections

  • Five request specs, not zero, stub past the null default with allow_next_instance_of(ArtifactRegistry::TokenExchange). All five stay green through the change, so nothing breaks. The cost is coverage: no test proves a real minted header reaches AR. One suite gets an unstubbed example.
  • JWKS rotation. AR fetches the issuer JWKS once at startup with no refresh and no stale-key retention, which is behind ADR-020 R2's own description. Recorded in Dependencies so a signing-key rotation is sequenced against an AR restart.

Related to gitlab-org/gitlab#623370 (closed)

Edited by Rahul Chanila

Merge request reports

Loading
Loading