Amend AR ADR-021 and ADR-009: verdicts embedded in a sub-resource list

💡 Why is this change being made?

ADR-021's Permission checks for UI gating section governs verdicts embedded in domain responses. It describes three carriers, and every one of them answers for the resource its route addresses. artifact-registry#1174 adds a fourth shape — include_permissions on listUpstreamRepositories, embedding each upstream repository's verdicts in the row — and that shape is contradicted by the current text in three places, not merely unmentioned.

❌ The three contradictions

  1. The enumeration is closed at three responses. The text reads "the repository detail response carries that repository's verdicts, and the repository list response carries an envelope with the namespace verdicts and each row's", plus the namespace details response in the following paragraph. An upstream list is a fourth surface, and its rows answer for a resource the route does not address.
  2. The stated justification for embedding does not transfer. The ADR says "The list envelope's verdicts are a by-product of serving it: for a caller without a namespace role, filtering the rows they may see already evaluates every repository." That is true of the repository list and false of an upstream list: the route gates on read_repository against the virtual repository alone and evaluates none of the upstreams its rows name. Every verdict there is net-new BatchCheck work on a route that currently does none.
  3. The existence-hiding rule makes no claim about per-sibling verdicts. The rule rests on "a caller with no access learns nothing the gated endpoints would not reveal." Whether per-sibling verdicts stay inside that model is an ADR-level claim, and it was made nowhere.

📝 What changed

Four edits inside ### Permission checks for UI gating, plus one correctness fix in ### List operations.

Edit Paragraph Change
1 The enumeration paragraph Adds the general rule: an embedded element can carry verdicts of its own, answering for the resource that element represents.
2 The by-product paragraph Scoped explicitly to the repository list, then followed by a new paragraph supplying the replacement rationale for a sub-resource list.
3 The existence-hiding bullet Adds that verdicts never change a row set.
4 The sequence-diagram note Names the repository list, and adds that a sub-resource list carries each embedded element's, for the resource that element represents.
5 The omission sentence in ### List operations Narrows it to name which lists omit rows: a list of a namespace's repositories omits the resources the principal cannot read; a sub-resource list whose rows the addressed resource already names keeps them, with deny on every action. Added on an operator decision during review, and worded to name the list rather than its level on a second operator decision.
6 009_api_design.md, the verdict paragraph Drops its three-carrier enumeration and delegates the carrier question to ADR-021, keeping its own claim on the route and the parameter. Added on an operator decision during review.

Edit 1 is written as a general rule rather than an upstream-list exception, so later sub-resource lists need no further amendment to this ADR.

🎯 Edit 1 stays at the decision's altitude

The ADR gains one sentence and no example. Anything more dates the decision to one sub-resource list and has to be restated for the next one, which is the opposite of what this amendment is for.

The upstream specifics that motivate it belong here instead. A listUpstreamRepositories row returns two resources — the repository_upstream association itself (its own id and position) and the nested upstream_repository it points at — and only the second carries verdicts, so one row's menu is gated from two carriers:

Row control Acts on Verdict source
Clear cache (delete_artifact) the upstream repository this row
Edit repository (update_repository) the upstream repository this row
Reorder (update_repository_upstream) the virtual repository its detail response
Remove (delete_repository_upstream) the virtual repository its detail response

The rule is phrased around the embedded element rather than the route, because a route-based phrasing would discriminate nothing: the repository list route addresses the namespace while its rows are repositories, so "the row is not what the route addresses" was already true of every existing carrier.

💰 Edit 2 is the load-bearing one

Removing the by-product argument for this surface leaves the embedding unjustified, so the amendment supplies a different argument: the cost is bounded and opt-in. The bound is already in the decision set — ADR-004 caps upstream sources per virtual repository at 20 — so an upstream list's verdicts are one BatchCheck per page, and a consumer rendering no per-upstream control asks for none.

Edits 1 and 2 overlapped one existing sentence, so it was merged rather than stacked. The old "Visibility filtering is itself a verdict, so every list request already depends on the relationships service and the policy engine" gave a repository-list-specific reason for a claim the section needs to hold generally. An upstream list does no visibility filtering, but its own gate is a point check, so the dependency is still pre-existing there. The sentence now states the claim first and names both routes to it, keeping the conclusion — "opting in adds per-row evaluation cost, not the dependency" — true for every list.

🛡️ Edit 3 is the sentence AppSec will read

The claim is made explicit rather than left implied: a resource the principal cannot read stays listed, with deny on every action, wherever the addressed resource's response already names it. The upstream list is exactly that case — it names the sibling repositories a virtual repository resolves through with or without verdicts — so embedding their verdicts discloses no resource the list did not already. The bullet now carries that reconciliation in the ADR text itself: this does not loosen the omission rule under List operations, which still governs every list whose rows are not already named.

🚫 What deliberately did not change

  • The action-set paragraph. "Each surface's action set is fixed by its resource level" already settles that an upstream row takes the full repository action set, because an upstream is a repository. 1174 introduces no new action set and no new schema.
  • The other three rules — advisory only, the caller is the end user, embedded and opt-in — hold as written.
  • ADR-021's resource-level tables. The reviewer's analysis surfaced a pre-existing disagreement — the table under #### Namespace-level resources files "Virtual repository upstream listing" as namespace-level, while ### List operations calls the same list repository-scoped. Edit 5 sidesteps it by naming the list rather than its level; reclassifying a table row is its own decision and is left for a follow-up.
  • Nothing else outside ### Permission checks for UI gating. Within ### List operations only the one omission sentence changes: the namespace-scoped listing bullets and the repository-scoped paragraph above it, and the landing-experience sentences after it, are untouched. Within 009_api_design.md only the verdict paragraph changes; no route, parameter or schema is added there.

An earlier revision of this description declined an ADR-009 amendment on the grounds that the route already exists there. That answered the route and left the parameter unaddressed, and the paragraph at 009_api_design.md:147 claims both — it named three carriers and then said "this ADR fixes the route and the parameter". A fourth carrier therefore contradicted it outright.

The fix is not to make it say four. Appending the upstream list only moves the contradiction to the fifth carrier, which is precisely the failure edit 1 exists to prevent. So the enumeration comes out: by the paragraph's own next clause, enumerating carriers is semantics it had already delegated to ADR-021. The route and the parameter stay with ADR-009, untouched.

This crosses the file boundary the MR otherwise holds to, on an explicit operator decision during review.

♻️ Mirror

Do not edit docs/adr/ in the Artifact Registry repository — the handbook is the authority, and the daily sync:adrs job mirrors this change and opens an MR there automatically.

🧪 Validation

Check Result
markdownlint-cli2 0.23.2, both changed files "Linting: 2 files", 0 issues
vale 3.13.0 at --minAlertLevel warning (the level scripts/vale.sh enforces for changed files) 0 errors, 13 warnings, every one on a pre-existing line — 4 handbook.Spelling in ADR-021 (135, 177, 260, 287) and 9 gitlab_base.LatinTerms/handbook.Spelling in ADR-009. The via flagged at 009:147 sits in the clause the amendment preserves verbatim. Vale exits 0 at this level; only errors fail.
hugo 0.166.0-extended per .tool-versions, full render exit 0, 4660 pages, no new warnings
ADR-009's preserved cross-reference Renders as /handbook/.../021_authorization/#permission-checks-for-ui-gating; ADR-021's page emits that id
New anchored link round-trip Renders as /handbook/.../004_data_and_application_limits/#entity-count-limits; ADR-004's page emits id="entity-count-limits"
ADR-004 bound re-verified in handbook source ### Entity Count Limits table still reads "Upstream sources per virtual repository | 20"
Sibling-link form Matches this directory's convention (004_data_and_application_limits.md#entity-count-limits), not the absolute URLs the mirror rewrites to
Mermaid note Present in rendered output
Working tree Only 021_authorization.md and 009_api_design.md modified; no build output left behind

👤 Review

.gitlab/CODEOWNERS maps /content/handbook/engineering/architecture/ to @gitlab-org/architecture/coaches under an optional approval section, so no reviewer is auto-assigned. No maintainer files are touched and nothing is deleted, so there is no potentially breaking change to flag.

Author and Reviewer Checklist

  • Provided a concise title for this Merge Request (MR)
  • Added a description explaining the reasons for the change, per say why, not just what
    • No confidential data is added; the content is SAFE.
  • Assign reviewers for this MR to the correct
  • For transparency, share this MR with the audience that will be impacted.

Commits

%{all_commits}


Edited by David Fernandez

Merge request reports

Loading
Loading