chore(iam): add the LookupResources RPC behind a ResourceLookup seam (S09 LookupResources plan: 1/3)

internal/iam gains a one-page LookupResources call behind a new ResourceLookup interface beside Resolver, with its domain request and page types, the proto mapping including LookupFilter, and the bounded rpc metric label. Nothing calls it yet — step 3 wires it into the repository-listing filter — so there is no release-visible surface, hence chore.

Why the seam looks like this

One call is one page. Draining stays with the caller, which is what lets step 3 own the page ceiling and the fail-closed arm instead of burying a loop here. The client echoes next_page_token unchanged and never reads an empty page as the terminator.

NOT_FOUND stays unmapped. IAM answers a subject it cannot resolve with an empty page, so an absence is an empty LookupResourcesPage; mapping NOT_FOUND onto a sentinel would invite a caller to read a reachability failure as an absence. A NOT_FOUND from a pre-fix IAM therefore fails the caller closed, which is the intended posture.

Response rows carry the subject on the proto's identity oneof rather than principal, so relationships.Relationship.Principal is empty on every lookup row. LookupResourcesPage says so, relationships.Relationship's own doc now says which of its two producers that applies to, and a mapping subtest pins it.

Verified against IAM

Re-confirmed against gitlab-org/auth/iam at 88630ea994:

Fact Where
default page size 100, cap 999; page_size <= 0 takes the default, above the cap is clamped down dataaccess/lookup/paginate.go
token fingerprinted over the subject's origin, origin id and local id plus the sorted kinds and roles lookupResourcesCursorFingerprint, same file
direct edges only (edge_type = 1) LookupRelationshipsBySubject in dataaccess/internal/relationships/store/queries.sql
an unresolvable principal answers an empty page, never NOT_FOUND lookupResources in dataaccess/lookup/lookup_resources.go

The vendored proto's Unresolvable subject → NOT_FOUND comment is stale against that handler; raising it upstream is a follow-up. No vendir bump needed.

AuthorizeLookupResources is not only a self-lookup: an org owner may look up any subject in their own organization, and both arms require an organization-origin subject in the caller's org, so a gitlab_federated subject is refused whoever asks. The doc comments state both arms. Step 3's caller must not skip its own subject check on the strength of the narrower rule.

Companions

internal/metrics/cardinality.go's rpc budget and closed set, and docs/dev/observability.md's label set. AuditCardinality checks gathered values against the pinned set, so a metered call fails the audit until the table lists the value.

Testing

Unit tests against an in-process fake LookupService: page mapping, the request reaching the wire verbatim, the four status sentinels plus NOT_FOUND staying unmapped, per-status attempt counts pinning the retry policy the service config grants the whole LookupService, the empty page, an empty page that still carries a token, the zero-value request, the rpc label, and the forwarded-JWT pair mirroring the ReadRelationships tests. An integration test issues one page against the IAM rig.

Local Docker is unavailable, so the integration-tagged file is compile-checked with go vet -tags=integration ./... and linted with golangci-lint run --build-tags=integration.

Size

659 added lines, past the 500 reviewable-LOC guideline. Splitting does not help: 123 are source (resolver.go 42, mapping.go 52, client.go 21, cardinality.go 4, types.go 3, metrics.go 1) and 535 are tests across five internal/iam suites — close to the ratio the plan forecast for this step. A test-only part would move lines between MRs without shrinking the reviewable unit, because the tests are what state what the seam promises.

Known follow-up

internal/iam/context.go's tokenHolder doc names ReadRelationships as the only method whose authorization header requires a JWT; LookupResources needs one too. That block sits far above the 1-line cap for an unexported type, so any edit fails check-comment-caps.sh and would cost the SchemeBearer reachability reasoning it carries. Step 3 changes those reachability facts, so the rewrite belongs there.

S25's spec-side correction is !2405 (merged), which moves LookupResources out of its "this client does not consume" list and into the consumed set; it merged ahead of this MR.

Related to https://gitlab.com/gitlab-org/ops/artifact-registry/-/work_items/969

Merge request reports

Loading
Loading