GitLab API: batch repository verifications endpoint (S33 Phase 1)

Why

Role assignments for AR are granted in bulk through a Rails GraphQL wrapper (gitlab-org/gitlab#602144). Before persisting repository-targeted grants, Rails must confirm the target repositories belong to the namespace the caller owns. AR is the only system that knows.

This is not part of the IAM Relationships API (iam!202): its Object is an opaque { id: UUIDv7 } with no containment. It is interim: once GLAZ can walk containment (resource → namespace → org), the resolution moves there and this endpoint is retired. The repository ids are the same UUIDv7s stored as the relationships Object.id.

What

Per ADR-009 Resource Verifications:

POST /api/gitlab/v1/namespaces/:uuid/repositories/verifications
{ "repository_ids": ["<uuid>", ...] }

204  all ids belong to the namespace
400  any failure (opaque; never reveals whether an id exists)
  • Scoped by the namespace UUID, which Rails persists (gitlab#603023); served from a single HASH(namespace_id) partition.
  • Returns no data: AR exposes no organization info, and the caller already knows the owner.
  • Namespace-targeted grants need no registry call: Rails verifies against its persisted mapping.
  • Future assignable resource types follow the per-type pattern POST /api/gitlab/v1/namespaces/:uuid/<resource_type>/verifications.

Service-authenticated; mechanism per ADR-020/ADR-021 (#255). Specified in the merged S33 spec (foundational spec issue #309 (closed), epic &22846) and delivered by Step 8 of the S33 Phase 1 plan.

Decisions

  • Maximum batch size: 1,000 ids, a code constant anchored on ADR-004. Promote to configuration only on demonstrated need.

Rewritten 2026-07-15: formerly "Internal API: resource ancestry lookup endpoint" (slug-scoped POST /api/v1/{slug}/internal/{resource_type}/ancestry_lookups returning org_id/ancestor_ids). Superseded by the merged ADR-009 GitLab API design: UUID-scoped batch verification returning no data. The namespace resource type left scope (Rails verifies those locally), and the old #185 (closed) UUIDv7 dependency is satisfied.

Contract

Step 8 of the S33 Phase 1 plan builds this. Implementation is in progress, so details can still change. Not on main yet.

POST /api/gitlab/v1/namespaces/{namespace_id}/repositories/verifications
Authorization: Bearer <service credential>

{ "repository_ids": ["<uuid>", ...] }

repository_ids: required, 1 to 1,000 UUID strings, no other fields. Duplicates verify once but count toward the 1,000 as sent.

Status Code Meaning
204 every UUID belongs; no body
400 bad_request malformed: bad JSON, unknown field, empty batch, over 1,000, or a non-UUID value
422 unprocessable_entity well formed, but at least one UUID is not a repository in this namespace; the body lists them
401 missing or invalid credential; no body, WWW-Authenticate: Bearer
404 not_found namespace_id names no namespace
413 body over the service limit
500 internal_server_error server failure

Why

  • 400 against 422: the status, not the message text, separates a caller bug from a data mismatch. Provisioning splits the same way.
  • 422 lists the failing UUIDs. Hiding them prevented nothing, since a one-UUID batch is legal and already answers whether that UUID belongs. It only buried the bad id in a batch of a thousand. The body still never says why an id failed, so "unknown" and "owned elsewhere" stay indistinguishable.
  • A soft-deleted repository verifies as belonging, so a deletion that may be undone does not drop a grant.
Edited by João Pereira