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
audcarries bothgitlab-artifact-registryandgitlab-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)toApproved. 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, passescurrent_user:and no providers, and S03's own spec answers "None, that ismonolith/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 onS03near 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 bytype::rather than by a Conventional Commits prefix. - Plan token. Four plans under
docs/plans/monolith/declaremonolith/S02as their Spec Reference, so a bareS02names 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::AcquiresClientonartifact_registry_ui, andread_artifact_registryon 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 grepfinds it nowhere inorigin/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)