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_idisorganization.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
AuthorizeWriteonly 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::UpdateServicepassescurrent_user.idthrough as a thirdperform_asyncargument (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.GrantOwnerRoleWorkerfalls back to the newly-promoted owner themselves if the actor is unavailable - a genuine party to their own grant, not a borrowed identity.RevokeOwnerRoleWorkerhas 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 viaGitlab::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_assignmentsflag and now useartifact_registry_role_assignmentto 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.rbEnable 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.