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:

  1. 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.
  2. Origin discrimination: Rails-replicated clients always carry an owning_cell_id; IAM-native dynamic clients (from DCR) never do. The dynamic column cannot serve this purpose because gitlab-rails oauth_applications also have a dynamic column, so clients from both origins would show dynamic=true in 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 BIGINT column to oauth_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) to ManagedClient with no validation (nullable for existing clients).
  • Added owning_cell_id (field 15) to both InternalOAuthClientsServiceCreateClientRequest and ...UpsertClientRequest with 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_id through Client and Registration, registry validation, the repository layer (nullable round-trip via pgtype.Int8 and pkg/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.CreateClient and Registry.UpsertClient are therefore identical with respect to owning_cell_id: the range check in the shared validateRegistration permits 0.
  • Origin invariant: a cell-less client is accepted only when it is dynamic (IAM-native). validateRegistration rejects owning_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_id is overwritten on conflict (not preserved, not insert-only), matching organization_id semantics. 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.

⚠️ Deployment ordering

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 CreateClient examples in docs/iam-auth-local-setup.md and docs/federated-auth-rails-setup-guide.md now include owning_cell_id (required over the RPC); no repo doc enumerates oauth_clients columns.

MR acceptance checklist

Evaluate this MR against the MR acceptance checklist.

🤖 Generated with Claude Code

Edited by Daniele Bracciani

Merge request reports

Loading
Loading