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
400against422: the status, not the message text, separates a caller bug from a data mismatch. Provisioning splits the same way.422lists 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.