Add owning_cell_id to oauth_clients
Adds owning_cell_id to oauth_clients to distinguish replicated clients from IAM-native clients and enable per-cell reconciliation.
Why
IAM replicates clients from multiple independent gitlab-rails cells but has no way to record which cell each replicated client originated from. Two downstream consumers need this signal:
- Layer 4 orphan reconciliation (gitlab-org/gitlab#602679 (closed), gitlab-org/gitlab#628424): A cell must only reconcile and delete clients it owns; otherwise it could delete another cell's client as an orphan.
- Origin discrimination: Rails-replicated clients always carry an
owning_cell_id; IAM-native dynamic clients (from DCR) never do. Thedynamiccolumn cannot serve this purpose becausegitlab-railsoauth_applications also have adynamiccolumn, so clients from both origins would showdynamic=truein IAM.
Follows the pattern from !633 (merged), which added organization_id the same way.
Related to gitlab-org/gitlab#629574 (closed). IAM side only. The gitlab-rails stamping half is a separate MR. Correcting owning_cell_id after a cross-cell organization move is out of scope (gitlab-org/gitlab#629579).
What changed
Migration (Postgres and YugabyteDB):
- Added nullable
owning_cell_id BIGINTcolumn tooauth_clients. - The supporting
(owning_cell_id, id)index is intentionally deferred to the MR that adds the querying endpoint (Layer 4 reconciliation /ListClients), so the index is introduced alongside, and validated by, the real query rather than committed ahead of any consumer.
Proto (proto/auth/auth.proto):
- Added
owning_cell_id(field 14) toManagedClientwith no validation (nullable for existing clients). - Added
owning_cell_id(field 15) to bothInternalOAuthClientsServiceCreateClientRequestand...UpsertClientRequestwith the same rule:owning_cell_id >= 1(lte 1000000). The requirement is a property of the external gRPC boundary, not of one RPC: every gRPC caller is a gitlab-rails cell replicating a client it owns, so both create and upsert require a cell. IAM-native DCR clients (which have no cell) never use these RPCs — they are created through the in-process registry (/oauth/register), where a cell-less client is accepted.
Go (clients package):
- Threaded
owning_cell_idthroughClientandRegistration, registry validation, the repository layer (nullable round-trip viapgtype.Int8andpkg/storage.Int8OrNull), and gRPC handler mapping in both directions. - Presence (
owning_cell_id >= 1) is enforced only at the external gRPC/CEL boundary (the protovalidate interceptor), not inside the registry.Registry.CreateClientandRegistry.UpsertClientare therefore identical with respect toowning_cell_id: the range check in the sharedvalidateRegistrationpermits0. - Origin invariant: a cell-less client is accepted only when it is
dynamic(IAM-native).validateRegistrationrejectsowning_cell_id = 0 && !dynamic, so a non-dynamic client can never be persisted without a cell and then misclassified as IAM-native (and skipped) by Layer 4 reconciliation. - Upsert behavior:
owning_cell_idis overwritten on conflict (not preserved, not insert-only), matchingorganization_idsemantics. Rationale: a client re-replicated by a new cell must take that cell's id; preserving the old value would leave a stale source-cell id after a cross-cell replication event.
Testing
Unit and integration tests (Postgres and YugabyteDB) for validation (create and upsert share one owning_cell_id expectation table), the origin invariant (a cell-less client is rejected unless dynamic), upsert overwrite behavior, and the internal registry accepting a cell-less dynamic client. The round-trip test asserts owning_cell_id IS NULL at the DB level, since 0 must be stored as SQL NULL (the property Layer 4 keys on) rather than a literal 0. Manually verified end-to-end with grpcurl against a running iam-auth instance.
gitlab-rails does not yet send owning_cell_id in replication upserts (the replicator at lib/authn/iam_replication/oauth_application_replicator.rb does not set it in upsert_attributes). Because UpsertClient requires owning_cell_id >= 1, every replication upsert will fail with InvalidArgument until the paired gitlab-rails MR (which stamps it from Gitlab.config.cell.id) is merged and deployed. That Rails MR must follow closely behind this one. Sandbox runs with iam_data_replication enabled and will hit this gap between the two deployments. (CreateClient carries the same rule, but it has no live caller — rails replicates via UpsertClient only, and DCR uses the in-process registry — so it adds no new deployment coupling.)
Out of scope
- Config-proto changes: OAuth client messages are not in
proto/config/. - Doc updates beyond the affected examples: the grpcurl
CreateClientexamples indocs/iam-auth-local-setup.mdanddocs/federated-auth-rails-setup-guide.mdnow includeowning_cell_id(required over the RPC); no repo doc enumeratesoauth_clientscolumns.
MR acceptance checklist
Evaluate this MR against the MR acceptance checklist.