Sync Artifact Registry owner role on organization owner changes

What does this MR do and why?

Implements the "ongoing sync" trigger from work item #601665 (closed): keeps an Artifact Registry organization-owner role assignment (organization_admin, via the IAM Relationships API) in sync as an organization's owner set changes.

When Organizations::OrganizationUsers::UpdateService successfully promotes or demotes a user across the owner boundary, EE::Organizations::OrganizationUsers::UpdateService enqueues GrantOwnerRoleWorker/RevokeOwnerRoleWorker, which call the IAM Relationships API (Authn::IamService::UpdateRelationshipsClient) directly rather than through the existing Authz::ArtifactRegistry::Grant/RevokeRoleAssignmentsService: that service exists to validate live GraphQL request input, none of which applies to this system-triggered write, and reusing it would keep organization_admin reachable through the public grant mutations. The organization_admin role (platform role id 019ed9d7-920d-72eb-a0ff-117122219c2a, which inherits artifact_admin's full permission set) is resolved dynamically via the gitlab-glaz gem, not hardcoded.

Ships behind the existing artifact_registry_role_assignment feature flag (default-off), so this has no effect until explicitly enabled.

Scope

This MR covers only the promote/demote path through OrganizationUsers::UpdateService. Explicitly out of scope, tracked as follow-ups:

  • The "AR activation" bootstrap trigger named in the issue - no backend activation mutation exists yet to attach it to.
  • Organizations::Transfer::OrganizationUsersService's owner-upsert path.
  • The initial owner assigned at organization creation.
  • The "Leave organization" self-removal case (!250819 (merged), not merged yet) and the actor-goes-stale race - see the design notes below and #627181 (closed).

Design notes

  • resource_id is organization.uuid, not an Artifact Registry namespace id. Per ADR-021 (gitlab-org/ops/artifact-registry), the Organization Administrator role is bootstrapped via a tuple binding the owner to the organization itself, which AR's authorization middleware already queries IAM for as a third ancestor object alongside the namespace and repository. This works independent of, and before, AR activation for that organization.
  • IAM's AuthorizeWrite only lets an owner's token write the organization's own resource. For grants the newly-promoted owner is still an owner when the worker runs, so minting as them works. For revokes the person losing the role no longer holds that claim by the time the worker runs, so their own token would be refused (PERMISSION_DENIED) on every demotion. EE::Organizations::OrganizationUsers::UpdateService passes current_user.id through as a third perform_async argument (the actor who made the change), and both workers mint the token as that actor, re-verified as a current owner at execution time since it can go stale across the async gap.
    • GrantOwnerRoleWorker falls back to the newly-promoted owner themselves if the actor is unavailable - a genuine party to their own grant, not a borrowed identity.
    • RevokeOwnerRoleWorker has no fallback. Borrowing another owner's identity would make IAM's record of who performed the write false, and !250819 (merged) (Leave organization) has no acting owner at all - the only person involved is the one leaving - so a fallback would always misattribute that case to an uninvolved owner. When the actor isn't available, the revoke does not happen and the user keeps the role; this is tracked via Gitlab::ErrorTracking.track_exception (reaches Sentry, unlike a plain skip) rather than silently guessed around. Follow-up: #627181 (closed).
    • This "borrow an identity to authorize a system-triggered write" shape is itself interim - it goes away once outbound IAM writes move to the IAM outbox approach.
    • Known gap, intentionally not addressed here: an instance admin can demote an organization owner via rule { admin } without being an owner themselves, so their token would also lack the owner claim. That needs its own authorization path on the IAM side (instance admins authorizing based on admin access, not organization membership), not something this worker can route around.
  • Each worker re-verifies current ownership via the existing User#owns_organization? at execution time rather than trusting the state at enqueue time, since Sidekiq does not guarantee job ordering across retries.
  • Removed the dedicated sync_artifact_registry_owner_role_assignments flag and now use artifact_registry_role_assignment to gate both manual role mutations and automatic owner role sync. Consolidating the flags reduces coordination overhead for Artifact Registry's GA launch, since both remain unreleased (type: wip).

Database review

No new query: currently_owner? reuses the existing, already-reviewed User#owns_organization? instance method.

References

Related to #601665 (closed) Follow-up: #627181 (closed)

How to set up and validate locally

bin/rspec \
  spec/models/organizations/organization_user_spec.rb \
  spec/services/organizations/organization_users/update_service_spec.rb \
  ee/spec/services/ee/organizations/organization_users/update_service_spec.rb \
  ee/spec/workers/authz/organizations/grant_owner_role_worker_spec.rb \
  ee/spec/workers/authz/organizations/revoke_owner_role_worker_spec.rb

Enable the flag in a Rails console (Feature.enable(:artifact_registry_role_assignment)), then promote/demote an organization member via Organizations::OrganizationUsers::UpdateService and confirm the appropriate worker runs.

Edited by Bojan Marjanovic

Merge request reports

Loading
Loading