Grant the Organization Administrator role when organization owners are created

What does this MR do and why?

Organization owners need the IAM organization_admin role (Organization Administrator) to use the Artifact Registry. Previously only the promotion path (Organizations::OrganizationUsers::UpdateService) enqueued Authz::Organizations::GrantOwnerRoleWorker, so owners created through any other path never got a role tuple. This caused a closed beta blocker, gitlab-org/ops/artifact-registry#1326 (closed): onboarded organizations' owners could not create repositories or grant roles.

This MR used to also include a large EE-to-CE relocation. That relocation has been split out and merged separately as !257221 (merged), and this MR was rebased on top of it, so it now contains only the fix.

The fix adds three hooks in CE services, each gated by Authz::Organizations::OwnerRoleSync.enabled? (true when IAM data access is configured and the edition can sign the token; false on CE, tracked at #630491):

  1. Organizations::CreateService: after the organization is saved, enqueues one job with a nil user id and the creator as actor. The first owner is written through nested attributes on the organization row, so no organization-user service ever sees it. Skipped when there is no current user, since then no owner was added.
  2. Organizations::ActivateService: after the activation transaction commits and the activated event is published, enqueues one job with a nil user id. Onboarding writes the owner set with upsert_all in Organizations::Transfer::OrganizationUsersService, bypassing service hooks entirely; this is the path behind work item 1326. The transfer service itself is untouched because it runs inside the transaction.
  3. Organizations::OrganizationUsers::CreateService: enqueues for the new member when the membership is created directly as owner (for example, organizationUserCreate with OWNER).

The worker itself changes to support this: GrantOwnerRoleWorker#perform(organization_id, user_id = nil, actor_id = nil). A nil user id syncs every current owner of the organization, and a given user id narrows the owner set to that user; both run through the same batch loop of 100, one IAM call per batch, minting the token as the acting owner. If the acting user is not an owner (for example, an instance admin activated the organization), the token is minted as one of the owners in the batch instead. Batching keeps loops out of web requests (suggested by rutgerwessels during review) and gives us the entry point for a future one-time backfill.

There is no new worker class and no feature flag. IAM relationship writes are upserts, so a repeat grant is harmless. The promotion and removal hooks added in !250806 (merged) are unchanged.

Authz::Organizations::GrantOwnerRoleWorker skips an owner row only when the user has the admin flag and the row is their home-organization membership, since that row is written by the flag rather than chosen by anyone. Every other owner row, including an instance admin creating a new organization, is granted like any other owner.

During a rolling deploy, jobs enqueued with a nil user id while old Sidekiq code is still running are skipped with a log line and not retried. We're accepting this because the window only lasts for the deploy of the Sidekiq fleet, on the order of minutes, and the planned one-time backfill re-syncs every organization afterward, repairing any owner missed in that window.

Known gaps and follow-ups

  • Backfill for existing organizations whose owners lack tuples is not included here. The one affected organization (the Artifact Registry test org) was repaired by hand; a one-time migration or task is a follow-up.
  • Non-owner member tuples were never created either; that is out of scope for this MR.
  • Demotion paths that bypass the update hook, the admin area organization user edit page (Users::UpdateService nested attributes) and toggling the admin flag (Organizations::OrganizationUser.update_home_organization_record_for), can leave a stale tuple. This is a security concern and likely needs reconciliation rather than another hook.
  • Owners created through Users::CreateService (the admin new-user form's "Organization administrator" option, and users created with the admin flag) are not synced by this MR. That hook moves to a follow-up (#630676 (closed)) because the owner row there can come from the admin flag, and how to treat it is still being settled.
  • Naming follow-up tracked at #629378 (closed).
  • Reviewers noted the eventual target design is the IAM Outbox pattern; tracked together with the CE propagation follow-up below.

References

Screenshots or screen recordings

Not applicable, this is a backend-only change.

How to set up and validate locally

Configure IAM data access (iam_data_access_service.secret_file in gitlab.yml) on an EE instance with a Cloud Connector key, then:

  1. Create an organization and confirm one GrantOwnerRoleWorker job is enqueued with a nil user id.
  2. Onboard an organization by transferring a top-level group with several owners, and confirm one job is enqueued after activation, and that the worker grants every owner.
  3. Run organizationUserCreate with OWNER and confirm a job is enqueued for that user.
  4. With the secret file unset, repeat the above and confirm nothing is enqueued.

Note: RuboCop is clean on all changed files. RSpec was not run locally because the test database cluster for this checkout is down, so the MR pipeline is the source of truth for spec results.

MR acceptance checklist

Evaluate this MR against the MR acceptance checklist. It helps you analyze changes to reduce risks in quality, performance, reliability, security, and maintainability.

🤖 Generated with Claude Code

Edited by Bojan Marjanovic

Merge request reports

Loading
Loading