From 4f0ee7f788f7be061ff118eea2bffbfbb316e51e Mon Sep 17 00:00:00 2001 From: agius Date: Sun, 1 Jun 2025 22:03:56 -0700 Subject: [PATCH 1/8] Migrate admin_bot to per-organization internal user With the change to make internal bot users per-organization, we must begin migrating all calls to Users::Internal to include a reference organization. This change updates all calls to `admin_bot` to follow this process. Changelog: changed --- .../groups/adjourned_deletion_service.rb | 2 +- .../projects/adjourned_deletion_service.rb | 2 +- app/services/users/auto_ban_service.rb | 2 +- .../inactive_projects_deletion_cron_worker.rb | 8 +-- .../inactive_tokens_deletion_cron_worker.rb | 8 +-- .../users/deactivate_dormant_users_worker.rb | 18 ++++--- .../models/compliance_management/pipl_user.rb | 2 +- ee/app/models/ee/user.rb | 2 +- .../pipl/block_pipl_users_worker.rb | 25 +++++----- .../pipl/delete_pipl_users_worker.rb | 25 +++++----- .../update_default_framework_worker.rb | 2 +- .../remove_dormant_members_worker.rb | 2 +- ...security_policy_bot_cleanup_cron_worker.rb | 5 +- .../unconfirmed_users_deletion_cron_worker.rb | 16 +++--- .../mark_admin_bot_runners_as_hosted.rb | 8 +-- ee/spec/models/ee/user_spec.rb | 2 +- .../update_default_framework_worker_spec.rb | 2 +- .../remove_dormant_members_worker_spec.rb | 10 +++- ...ity_policy_bot_cleanup_cron_worker_spec.rb | 50 ++++++++----------- .../delete_orphaned_groups.rb | 12 ++--- ...active_tokens_deletion_cron_worker_spec.rb | 5 +- 21 files changed, 105 insertions(+), 103 deletions(-) diff --git a/app/services/namespaces/groups/adjourned_deletion_service.rb b/app/services/namespaces/groups/adjourned_deletion_service.rb index be12b8973b5183..6dba3e91a72352 100644 --- a/app/services/namespaces/groups/adjourned_deletion_service.rb +++ b/app/services/namespaces/groups/adjourned_deletion_service.rb @@ -29,7 +29,7 @@ def delete_group end def restore_group - admin_bot = ::Users::Internal.admin_bot + admin_bot = ::Users::Internal.for_organization(group.organization).admin_bot Gitlab::Auth::CurrentUserMode.optionally_run_in_admin_mode(admin_bot) do ::Groups::RestoreService.new(group, admin_bot).execute end diff --git a/app/services/projects/adjourned_deletion_service.rb b/app/services/projects/adjourned_deletion_service.rb index 20f57a8252ff6f..2b568bc89950e3 100644 --- a/app/services/projects/adjourned_deletion_service.rb +++ b/app/services/projects/adjourned_deletion_service.rb @@ -30,7 +30,7 @@ def delete_project end def restore_project - admin_bot = ::Users::Internal.admin_bot + admin_bot = ::Users::Internal.for_organization(project.organization).admin_bot Gitlab::Auth::CurrentUserMode.optionally_run_in_admin_mode(admin_bot) do ::Projects::RestoreService.new(project, admin_bot).execute end diff --git a/app/services/users/auto_ban_service.rb b/app/services/users/auto_ban_service.rb index 56bf31a3d7821c..e1500506f6d470 100644 --- a/app/services/users/auto_ban_service.rb +++ b/app/services/users/auto_ban_service.rb @@ -32,7 +32,7 @@ def ban_user end def admin_bot - Users::Internal.admin_bot + Users::Internal.for_organization(user.organization).admin_bot end def ban_duplicate_users diff --git a/app/workers/projects/inactive_projects_deletion_cron_worker.rb b/app/workers/projects/inactive_projects_deletion_cron_worker.rb index 33145083db7006..fe796d929cda8b 100644 --- a/app/workers/projects/inactive_projects_deletion_cron_worker.rb +++ b/app/workers/projects/inactive_projects_deletion_cron_worker.rb @@ -39,15 +39,17 @@ def perform raise TimeoutError end - with_context(project: project, user: admin_bot) do + organization_admin_bot = ::Users::Internal.for_organization(project.organization).admin_bot + + with_context(project: project, user: organization_admin_bot) do deletion_warning_email_sent_on = notified_inactive_projects["project:#{project.id}"] if deletion_warning_email_sent_on.blank? send_notification(project) - log_audit_event(project, admin_bot) + log_audit_event(project, organization_admin_bot) elsif grace_period_is_over?(deletion_warning_email_sent_on) Gitlab::DormantProjectsDeletionWarningTracker.new(project.id).reset - delete_project(project, admin_bot) + delete_project(project, organization_admin_bot) end end end diff --git a/app/workers/resource_access_tokens/inactive_tokens_deletion_cron_worker.rb b/app/workers/resource_access_tokens/inactive_tokens_deletion_cron_worker.rb index a49db473afd820..d97c84e536dffc 100644 --- a/app/workers/resource_access_tokens/inactive_tokens_deletion_cron_worker.rb +++ b/app/workers/resource_access_tokens/inactive_tokens_deletion_cron_worker.rb @@ -24,7 +24,7 @@ def perform(cursor = nil) User.project_bot.where('"users"."id" > ?', cursor || 0).each_batch(of: BATCH_SIZE) do |relation| project_bot_users_whose_all_tokens_became_inactive_before_cut_off_date_or_without_tokens = relation - .select(:id, :username) + .select(:id, :username, :organization_id) .where( 'NOT EXISTS (?)', PersonalAccessToken @@ -57,6 +57,7 @@ def initiate_deletion_for(users) DeleteUserWorker.bulk_perform_async_with_contexts( users, arguments_proc: ->(user) { + admin_bot_id = Users::Internal.for_organization(user.organization).admin_bot.id [ admin_bot_id, user.id, { skip_authorization: true, reason_for_deletion: "No active token assigned" } @@ -65,10 +66,5 @@ def initiate_deletion_for(users) context_proc: ->(user) { { user: user } } ) end - - def admin_bot_id - Users::Internal.admin_bot.id - end - strong_memoize_attr :admin_bot_id end end diff --git a/app/workers/users/deactivate_dormant_users_worker.rb b/app/workers/users/deactivate_dormant_users_worker.rb index 5cd1d2938ee55b..8501d39e6b655f 100644 --- a/app/workers/users/deactivate_dormant_users_worker.rb +++ b/app/workers/users/deactivate_dormant_users_worker.rb @@ -15,25 +15,31 @@ def perform return unless ::Gitlab::CurrentSettings.current_application_settings.deactivate_dormant_users + # Ensure at least one admin bot has been set up admin_bot = Users::Internal.admin_bot return unless admin_bot - Gitlab::Auth::CurrentUserMode.bypass_session!(admin_bot.id) do - deactivate_users(User.dormant, admin_bot) - deactivate_users(User.with_no_activity, admin_bot) - end + deactivate_users(User.dormant) + deactivate_users(User.with_no_activity) end private - def deactivate_users(scope, admin_bot) + def deactivate_users(scope) with_context(caller_id: self.class.name.to_s) do scope.each_batch do |batch| batch.each do |user| - Users::DeactivateService.new(admin_bot).execute(user) + process_user_deactivation(user) end end end end + + def process_user_deactivation(user) + admin_bot = Users::Internal.for_organization(user.organization).admin_bot + Gitlab::Auth::CurrentUserMode.bypass_session!(admin_bot.id) do + Users::DeactivateService.new(admin_bot).execute(user) + end + end end end diff --git a/ee/app/models/compliance_management/pipl_user.rb b/ee/app/models/compliance_management/pipl_user.rb index dafbe74719ea83..98fa74b8c11089 100644 --- a/ee/app/models/compliance_management/pipl_user.rb +++ b/ee/app/models/compliance_management/pipl_user.rb @@ -38,7 +38,7 @@ class PiplUser < ApplicationRecord scope :pipl_deletable, -> do joins(:user) - .includes(:user) + .includes(user: :organization) .left_outer_joins(user: :ghost_user_migration) .where(ghost_user_migrations: { id: nil }) .where.not(state: "deletion_needs_to_be_reviewed") diff --git a/ee/app/models/ee/user.rb b/ee/app/models/ee/user.rb index e7243a29cc498a..d4aee18f289414 100644 --- a/ee/app/models/ee/user.rb +++ b/ee/app/models/ee/user.rb @@ -902,7 +902,7 @@ def audit_lock_access(reason: nil) ::Gitlab::Audit::Auditor.audit( name: 'user_access_locked', - author: ::Users::Internal.admin_bot, + author: ::Users::Internal.for_organization(organization).admin_bot, scope: self, target: self, message: ['User access locked', reason].compact.join(' - ') diff --git a/ee/app/workers/compliance_management/pipl/block_pipl_users_worker.rb b/ee/app/workers/compliance_management/pipl/block_pipl_users_worker.rb index 2c1b2a88a81d94..b7a552b7dceb8b 100644 --- a/ee/app/workers/compliance_management/pipl/block_pipl_users_worker.rb +++ b/ee/app/workers/compliance_management/pipl/block_pipl_users_worker.rb @@ -14,13 +14,11 @@ class BlockPiplUsersWorker queue_namespace :cronjob def perform - Gitlab::Auth::CurrentUserMode.optionally_run_in_admin_mode(admin_bot) do - PiplUser.pipl_blockable.each_batch do |batch| - batch.each do |pipl_user| - with_context(user: pipl_user.user) do - # Ensure that admin mode doesn't break the authorization cycle - block_user(pipl_user) - end + PiplUser.pipl_blockable.each_batch do |batch| + batch.each do |pipl_user| + with_context(user: pipl_user.user) do + # Ensure that admin mode doesn't break the authorization cycle + block_user(pipl_user) end end end @@ -29,6 +27,14 @@ def perform private def block_user(pipl_user) + admin_bot = ::Users::Internal.for_organization(pipl_user.user.organization).admin_bot + + Gitlab::Auth::CurrentUserMode.optionally_run_in_admin_mode(admin_bot) do + process_block_user(pipl_user, admin_bot) + end + end + + def process_block_user(pipl_user, admin_bot) result = BlockNonCompliantUserService.new(pipl_user: pipl_user, current_user: admin_bot).execute return unless result.error? @@ -36,11 +42,6 @@ def block_user(pipl_user) Gitlab::AppLogger .info(message: "Error blocking user: #{pipl_user.user.id} with message #{result.message}", jid: jid) end - - def admin_bot - Users::Internal.admin_bot - end - strong_memoize_attr :admin_bot end end end diff --git a/ee/app/workers/compliance_management/pipl/delete_pipl_users_worker.rb b/ee/app/workers/compliance_management/pipl/delete_pipl_users_worker.rb index ea430f9881b892..3e59b365a106fd 100644 --- a/ee/app/workers/compliance_management/pipl/delete_pipl_users_worker.rb +++ b/ee/app/workers/compliance_management/pipl/delete_pipl_users_worker.rb @@ -14,13 +14,11 @@ class DeletePiplUsersWorker queue_namespace :cronjob def perform - Gitlab::Auth::CurrentUserMode.optionally_run_in_admin_mode(admin_bot) do - PiplUser.pipl_deletable.each_batch do |batch| - batch.each do |pipl_user| - with_context(user: pipl_user.user) do - # Ensure that admin mode doesn't break the authorization cycle - delete_user(pipl_user) - end + PiplUser.pipl_deletable.each_batch do |batch| + batch.each do |pipl_user| + with_context(user: pipl_user.user) do + # Ensure that admin mode doesn't break the authorization cycle + delete_user(pipl_user) end end end @@ -29,6 +27,14 @@ def perform private def delete_user(pipl_user) + admin_bot = ::Users::Internal.for_organization(pipl_user.user.organization).admin_bot + + Gitlab::Auth::CurrentUserMode.optionally_run_in_admin_mode(admin_bot) do + process_delete_user(pipl_user, admin_bot) + end + end + + def process_delete_user(pipl_user, admin_bot) result = DeleteNonCompliantUserService.new(pipl_user: pipl_user, current_user: admin_bot).execute return unless result.error? @@ -36,11 +42,6 @@ def delete_user(pipl_user) Gitlab::AppLogger .info(message: "Error blocking user: #{pipl_user.user.id} with message #{result.message}", jid: jid) end - - def admin_bot - Users::Internal.admin_bot - end - strong_memoize_attr :admin_bot end end end diff --git a/ee/app/workers/compliance_management/update_default_framework_worker.rb b/ee/app/workers/compliance_management/update_default_framework_worker.rb index 7935c5bbd53af9..62b2b2315e8ddc 100644 --- a/ee/app/workers/compliance_management/update_default_framework_worker.rb +++ b/ee/app/workers/compliance_management/update_default_framework_worker.rb @@ -10,8 +10,8 @@ class UpdateDefaultFrameworkWorker feature_category :compliance_management def perform(_user_id, project_id, compliance_framework_id) - admin_bot = ::Users::Internal.admin_bot project = Project.find(project_id) + admin_bot = ::Users::Internal.for_organization(project.organization).admin_bot Gitlab::Auth::CurrentUserMode.bypass_session!(admin_bot.id) do ::ComplianceManagement::Frameworks::UpdateProjectService diff --git a/ee/app/workers/namespaces/remove_dormant_members_worker.rb b/ee/app/workers/namespaces/remove_dormant_members_worker.rb index cb4d32e51e6cbc..cdc24026bf1aee 100644 --- a/ee/app/workers/namespaces/remove_dormant_members_worker.rb +++ b/ee/app/workers/namespaces/remove_dormant_members_worker.rb @@ -57,7 +57,7 @@ def namespaces_requiring_dormant_member_removal(limit = 1) def remove_dormant_members(namespace) dormant_period = namespace.namespace_settings.remove_dormant_members_period.days.ago - admin_bot = ::Users::Internal.admin_bot + admin_bot = ::Users::Internal.for_organization(namespace.organization).admin_bot dormant_count = 0 ::GitlabSubscriptions::SeatAssignment.dormant_in_namespace(namespace, dormant_period).find_each do |assignment| diff --git a/ee/app/workers/users/security_policy_bot_cleanup_cron_worker.rb b/ee/app/workers/users/security_policy_bot_cleanup_cron_worker.rb index 39962bed195f46..afd436ef28f135 100644 --- a/ee/app/workers/users/security_policy_bot_cleanup_cron_worker.rb +++ b/ee/app/workers/users/security_policy_bot_cleanup_cron_worker.rb @@ -15,10 +15,8 @@ class SecurityPolicyBotCleanupCronWorker def perform return unless Feature.enabled?(:security_policy_bot_cleanup_cron_worker, :instance) - admin_user = Users::Internal.admin_bot options = { skip_authorization: true, hard_delete: false, reason_for_deletion: "Security policy bot no longer associated with any project" } - destroy_service = Users::DestroyService.new(admin_user) users_to_ignore = [] MAX_BATCHES.times do @@ -27,6 +25,9 @@ def perform break if users.empty? users.each do |user| + admin_user = Users::Internal.for_organization(user.organization).admin_bot + destroy_service = Users::DestroyService.new(admin_user) + with_context(user: user) do destroy_service.execute(user, options) rescue Gitlab::Access::AccessDeniedError, Users::DestroyService::DestroyError => e diff --git a/ee/app/workers/users/unconfirmed_users_deletion_cron_worker.rb b/ee/app/workers/users/unconfirmed_users_deletion_cron_worker.rb index c26c7ab61dcc66..a87a08b4b3e9fb 100644 --- a/ee/app/workers/users/unconfirmed_users_deletion_cron_worker.rb +++ b/ee/app/workers/users/unconfirmed_users_deletion_cron_worker.rb @@ -18,7 +18,7 @@ def perform return if ::Gitlab::CurrentSettings.email_confirmation_setting_off? return unless ::Gitlab::CurrentSettings.delete_unconfirmed_users? return unless License.feature_available?(:delete_unconfirmed_users) - return unless admin_bot_id + return unless Users::Internal.admin_bot in_lock(self.class.name.underscore, ttl: Gitlab::Utils::ExecutionTracker::MAX_RUNTIME, retries: 0) do order = Gitlab::Pagination::Keyset::Order.build([ @@ -33,7 +33,11 @@ def perform ) ]) - users = User.unconfirmed_and_created_before(cut_off).select(:created_at, :id, :username).order(order) # rubocop: disable CodeReuse/ActiveRecord + # rubocop:disable CodeReuse/ActiveRecord + users = User.unconfirmed_and_created_before(cut_off) + .select(:created_at, :id, :organization_id, :username) + .order(order) + # rubocop:enable CodeReuse/ActiveRecord delete_users(scope: users) end end @@ -51,7 +55,8 @@ def delete_users(scope:) DeleteUserWorker.bulk_perform_async_with_contexts( relation, arguments_proc: ->(user) { - [admin_bot_id, user.id, { skip_authorization: true, reason_for_deletion: reason_for_deletion }] + admin_bot = ::Users::Internal.for_organization(user.organization).admin_bot + [admin_bot.id, user.id, { skip_authorization: true, reason_for_deletion: reason_for_deletion }] }, context_proc: ->(user) { { user: user } } ) @@ -63,11 +68,6 @@ def cut_off end strong_memoize_attr :cut_off - def admin_bot_id - Users::Internal.admin_bot&.id - end - strong_memoize_attr :admin_bot_id - def reason_for_deletion "GitLab automatically deletes unconfirmed users after " \ "#{::Gitlab::CurrentSettings.unconfirmed_users_delete_after_days} days since their creation" diff --git a/ee/lib/ee/gitlab/background_migration/mark_admin_bot_runners_as_hosted.rb b/ee/lib/ee/gitlab/background_migration/mark_admin_bot_runners_as_hosted.rb index 3332ec1e8d7aa9..13ae7870602741 100644 --- a/ee/lib/ee/gitlab/background_migration/mark_admin_bot_runners_as_hosted.rb +++ b/ee/lib/ee/gitlab/background_migration/mark_admin_bot_runners_as_hosted.rb @@ -18,7 +18,7 @@ def perform each_sub_batch do |sub_batch| sub_batch.each do |runner| - next unless runner.creator_id == admin_bot.id + next unless ::User.admin_bot.where(id: runner.creator_id).exists? CiHostedRunner.find_or_create_by!(runner_id: runner.id) end @@ -32,12 +32,6 @@ class ApplicationSetting < ApplicationRecord class CiHostedRunner < ::Ci::ApplicationRecord self.table_name = :ci_hosted_runners end - - private - - def admin_bot - @_admin_bot ||= ::Users::Internal.admin_bot - end end end end diff --git a/ee/spec/models/ee/user_spec.rb b/ee/spec/models/ee/user_spec.rb index e3bec411bc2fad..f14a988b98da93 100644 --- a/ee/spec/models/ee/user_spec.rb +++ b/ee/spec/models/ee/user_spec.rb @@ -3500,8 +3500,8 @@ end describe '#lock_access!' do - let_it_be(:gitlab_admin_bot) { Users::Internal.admin_bot } let_it_be_with_reload(:user) { create(:user) } + let_it_be(:gitlab_admin_bot) { Users::Internal.for_organization(user.organization).admin_bot } subject { user.lock_access! } diff --git a/ee/spec/workers/compliance_management/update_default_framework_worker_spec.rb b/ee/spec/workers/compliance_management/update_default_framework_worker_spec.rb index 3c3bb566b96c39..6c5054765377c4 100644 --- a/ee/spec/workers/compliance_management/update_default_framework_worker_spec.rb +++ b/ee/spec/workers/compliance_management/update_default_framework_worker_spec.rb @@ -5,10 +5,10 @@ RSpec.describe ComplianceManagement::UpdateDefaultFrameworkWorker, feature_category: :compliance_management do let_it_be(:worker) { described_class.new } let_it_be(:user) { create(:user) } - let_it_be(:admin_bot) { create(:user, :admin_bot, :admin) } let_it_be(:group) { create(:group) } let_it_be_with_reload(:project) { create(:project, namespace: group) } let_it_be(:framework) { create(:compliance_framework, namespace: group, name: 'GDPR') } + let_it_be(:admin_bot) { create(:user, :admin_bot, :with_organization, in_organization: project.organization) } let(:job_args) { [user.id, project.id, framework.id] } diff --git a/ee/spec/workers/namespaces/remove_dormant_members_worker_spec.rb b/ee/spec/workers/namespaces/remove_dormant_members_worker_spec.rb index 7e4554e1b693b2..04f06d09181bde 100644 --- a/ee/spec/workers/namespaces/remove_dormant_members_worker_spec.rb +++ b/ee/spec/workers/namespaces/remove_dormant_members_worker_spec.rb @@ -114,7 +114,9 @@ end include_examples 'audit event logging' do - let_it_be(:admin) { create(:admin) } + let_it_be(:admin) do + create(:user, :admin_bot, :with_organization, in_organization: dormant_enterprise_user.organization) + end let(:operation) { perform_work } @@ -142,7 +144,11 @@ end before do - allow(::Users::Internal).to receive(:admin_bot).and_return(admin) + # this has to be stubbed, otherwise the next_found_instance_of stub above + # will apply to admin_bot instead of the expected test user + allow_next_instance_of(::Users::Internal) do |internal| + allow(internal).to receive(:admin_bot).and_return(admin) + end end end end diff --git a/ee/spec/workers/users/security_policy_bot_cleanup_cron_worker_spec.rb b/ee/spec/workers/users/security_policy_bot_cleanup_cron_worker_spec.rb index e38000c86cd3c6..416c317435ea30 100644 --- a/ee/spec/workers/users/security_policy_bot_cleanup_cron_worker_spec.rb +++ b/ee/spec/workers/users/security_policy_bot_cleanup_cron_worker_spec.rb @@ -17,11 +17,15 @@ } end + let(:destroy_service_double) { Users::DestroyService.new(admin_user) } + + before do + allow(Users::DestroyService).to receive(:new).and_return(destroy_service_double) + end + context 'with no security policy bots' do it 'does not delete any users' do - expect_next_instance_of(Users::DestroyService) do |service| - expect(service).not_to receive(:execute) - end + expect(Users::DestroyService).not_to receive(:new) perform end @@ -36,9 +40,7 @@ end it 'does not delete users with project memberships' do - expect_next_instance_of(Users::DestroyService) do |service| - expect(service).not_to receive(:execute) - end + expect(Users::DestroyService).not_to receive(:new) perform end @@ -57,10 +59,8 @@ end it 'deletes security policy bots without project memberships' do - expect_next_instance_of(Users::DestroyService) do |service| - expect(service).to receive(:execute).with(security_policy_bot_1, expected_service_options) - expect(service).to receive(:execute).with(security_policy_bot_2, expected_service_options) - end + expect(destroy_service_double).to receive(:execute).with(security_policy_bot_1, expected_service_options) + expect(destroy_service_double).to receive(:execute).with(security_policy_bot_2, expected_service_options) perform end @@ -76,10 +76,8 @@ end it 'only deletes security policy bots without project memberships' do - expect_next_instance_of(Users::DestroyService) do |service| - expect(service).to receive(:execute).with(security_policy_bot_without_membership, - expected_service_options).and_call_original - end + expect(destroy_service_double).to receive(:execute).with(security_policy_bot_without_membership, + expected_service_options).and_call_original perform end @@ -103,9 +101,9 @@ let_it_be(:ghost_user_migration) { create(:ghost_user_migration, user: ghost_security_policy_bot) } it 'does not attempt to delete security policy bots with ghost user migration' do - expect_next_instance_of(Users::DestroyService) do |service| - expect(service).to receive(:execute).with(security_policy_bot, expected_service_options).and_call_original - end + expect(destroy_service_double).to receive(:execute).with( + security_policy_bot, expected_service_options + ).and_call_original perform end @@ -120,9 +118,7 @@ end it 'processes up to 2 users in a single execution' do - expect_next_instance_of(Users::DestroyService) do |service| - expect(service).to receive(:execute).twice - end + expect(destroy_service_double).to receive(:execute).twice perform end @@ -133,14 +129,12 @@ let_it_be(:security_policy_bot_2) { create(:user, :security_policy_bot) } before do - allow_next_instance_of(Users::DestroyService) do |service| - allow(service).to receive(:execute).and_call_original - allow(service).to( - receive(:execute) - .with(security_policy_bot_1, expected_service_options) - .and_raise(error_class, 'Service error') - ) - end + allow(destroy_service_double).to receive(:execute).and_call_original + allow(destroy_service_double).to( + receive(:execute) + .with(security_policy_bot_1, expected_service_options) + .and_raise(error_class, 'Service error') + ) end shared_examples 'tracking error and continues processing' do diff --git a/lib/gitlab/background_migration/delete_orphaned_groups.rb b/lib/gitlab/background_migration/delete_orphaned_groups.rb index 428867ad400257..3c6f3d44753d81 100644 --- a/lib/gitlab/background_migration/delete_orphaned_groups.rb +++ b/lib/gitlab/background_migration/delete_orphaned_groups.rb @@ -14,16 +14,16 @@ def perform .joins("LEFT JOIN namespaces AS parent ON namespaces.parent_id = parent.id") .where(parent: { id: nil }) .pluck(:id).each do |orphaned_group_id| + organization_id = Group.where(id: orphaned_group_id).pick(:organization_id) + organization = ::Organizations::Organization.find_by_id(organization_id) + + # if organization is nil, returns the default organization admin_bot + admin_bot = Users::Internal.for_organization(organization).admin_bot + ::GroupDestroyWorker.perform_async(orphaned_group_id, admin_bot.id) end end end - - private - - def admin_bot - @_admin_bot ||= Users::Internal.admin_bot - end end end end diff --git a/spec/workers/resource_access_tokens/inactive_tokens_deletion_cron_worker_spec.rb b/spec/workers/resource_access_tokens/inactive_tokens_deletion_cron_worker_spec.rb index 2cfed36a350b63..4eef8e1034817c 100644 --- a/spec/workers/resource_access_tokens/inactive_tokens_deletion_cron_worker_spec.rb +++ b/spec/workers/resource_access_tokens/inactive_tokens_deletion_cron_worker_spec.rb @@ -23,8 +23,7 @@ :freeze_time, :sidekiq_inline ) do - cut_off = cut_off_days.days.ago - admin_bot = Users::Internal.admin_bot + cut_off = Gitlab::CurrentSettings.inactive_resource_access_tokens_delete_after_days.days.ago active_personal_access_token = create(:personal_access_token) @@ -141,6 +140,7 @@ worker.perform users_to_keep.each do |user| + admin_bot = Users::Internal.for_organization(user.organization).admin_bot expect( Users::GhostUserMigration.find_by( user: user, @@ -150,6 +150,7 @@ end users_to_delete.each do |user| + admin_bot = Users::Internal.for_organization(user.organization).admin_bot expect( Users::GhostUserMigration.find_by( user: user, -- GitLab From ae4fabfde7720aa101467dc6d2a9099eddd05725 Mon Sep 17 00:00:00 2001 From: agius Date: Fri, 1 Aug 2025 10:23:38 -0700 Subject: [PATCH 2/8] Fix specs --- .../update_default_framework_worker_spec.rb | 2 +- .../workers/namespaces/remove_dormant_members_worker_spec.rb | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/ee/spec/workers/compliance_management/update_default_framework_worker_spec.rb b/ee/spec/workers/compliance_management/update_default_framework_worker_spec.rb index 6c5054765377c4..ab7610e0d07644 100644 --- a/ee/spec/workers/compliance_management/update_default_framework_worker_spec.rb +++ b/ee/spec/workers/compliance_management/update_default_framework_worker_spec.rb @@ -8,7 +8,7 @@ let_it_be(:group) { create(:group) } let_it_be_with_reload(:project) { create(:project, namespace: group) } let_it_be(:framework) { create(:compliance_framework, namespace: group, name: 'GDPR') } - let_it_be(:admin_bot) { create(:user, :admin_bot, :with_organization, in_organization: project.organization) } + let_it_be(:admin_bot) { create(:user, :admin_bot, organization: project.organization) } let(:job_args) { [user.id, project.id, framework.id] } diff --git a/ee/spec/workers/namespaces/remove_dormant_members_worker_spec.rb b/ee/spec/workers/namespaces/remove_dormant_members_worker_spec.rb index 04f06d09181bde..db6b2c22352d6e 100644 --- a/ee/spec/workers/namespaces/remove_dormant_members_worker_spec.rb +++ b/ee/spec/workers/namespaces/remove_dormant_members_worker_spec.rb @@ -115,7 +115,7 @@ include_examples 'audit event logging' do let_it_be(:admin) do - create(:user, :admin_bot, :with_organization, in_organization: dormant_enterprise_user.organization) + create(:user, :admin_bot, organization: dormant_enterprise_user.organization) end let(:operation) { perform_work } -- GitLab From cb237a5ca824c120c4b223c0956009ee96c444ee Mon Sep 17 00:00:00 2001 From: agius Date: Mon, 11 Aug 2025 15:34:55 -0700 Subject: [PATCH 3/8] Incorporate review feedback --- lib/gitlab/background_migration/delete_orphaned_groups.rb | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/lib/gitlab/background_migration/delete_orphaned_groups.rb b/lib/gitlab/background_migration/delete_orphaned_groups.rb index 3c6f3d44753d81..b1a31fdb8adc61 100644 --- a/lib/gitlab/background_migration/delete_orphaned_groups.rb +++ b/lib/gitlab/background_migration/delete_orphaned_groups.rb @@ -14,8 +14,7 @@ def perform .joins("LEFT JOIN namespaces AS parent ON namespaces.parent_id = parent.id") .where(parent: { id: nil }) .pluck(:id).each do |orphaned_group_id| - organization_id = Group.where(id: orphaned_group_id).pick(:organization_id) - organization = ::Organizations::Organization.find_by_id(organization_id) + organization = Group.where(id: orphaned_group_id).includes(:organization).first&.organization # if organization is nil, returns the default organization admin_bot admin_bot = Users::Internal.for_organization(organization).admin_bot -- GitLab From 3d1e08a12df961e0130303bfb3bf780579758fa3 Mon Sep 17 00:00:00 2001 From: agius Date: Wed, 13 Aug 2025 11:28:31 -0700 Subject: [PATCH 4/8] Fix inactive_projects_deletion_cron_worker_spec --- .../ee/projects/inactive_projects_deletion_cron_worker_spec.rb | 3 ++- .../projects/inactive_projects_deletion_cron_worker_spec.rb | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/ee/spec/workers/ee/projects/inactive_projects_deletion_cron_worker_spec.rb b/ee/spec/workers/ee/projects/inactive_projects_deletion_cron_worker_spec.rb index 99d590b8fb2717..eaa2d7b22f9911 100644 --- a/ee/spec/workers/ee/projects/inactive_projects_deletion_cron_worker_spec.rb +++ b/ee/spec/workers/ee/projects/inactive_projects_deletion_cron_worker_spec.rb @@ -8,7 +8,6 @@ describe "#perform", :clean_gitlab_redis_shared_state, :sidekiq_inline do subject(:worker) { described_class.new } - let_it_be(:admin_bot) { ::Users::Internal.admin_bot } let_it_be(:non_admin_user) { create(:user) } let_it_be(:group) { create(:group) } let_it_be(:new_blank_project) do @@ -33,6 +32,8 @@ .tap { |project| project.update!(last_activity_at: 1.month.ago) } end + let_it_be(:admin_bot) { ::Users::Internal.for_organization(new_blank_project.organization).admin_bot } + let_it_be(:delay) { anything } before do diff --git a/spec/workers/projects/inactive_projects_deletion_cron_worker_spec.rb b/spec/workers/projects/inactive_projects_deletion_cron_worker_spec.rb index 20eb9fb036869a..b54ae1714f4cf7 100644 --- a/spec/workers/projects/inactive_projects_deletion_cron_worker_spec.rb +++ b/spec/workers/projects/inactive_projects_deletion_cron_worker_spec.rb @@ -36,7 +36,6 @@ describe "#perform" do subject(:worker) { described_class.new } - let_it_be(:admin_bot) { ::Users::Internal.admin_bot } let_it_be(:non_admin_user) { create(:user) } let_it_be(:new_blank_project) do create_project_with_statistics.tap do |project| @@ -44,6 +43,8 @@ end end + let_it_be(:admin_bot) { ::Users::Internal.for_organization(new_blank_project.organization).admin_bot } + let_it_be(:inactive_blank_project) do create_project_with_statistics.tap do |project| project.update!(last_activity_at: 13.months.ago) -- GitLab From 87d4049025faf24b0fb6af827dfd09e195a11add Mon Sep 17 00:00:00 2001 From: agius Date: Tue, 19 Aug 2025 11:11:23 -0700 Subject: [PATCH 5/8] Allow Users::Internal to accept organization_id This allows passing just the organization_id to Users::Internal, so we can avoid loading the full model in most cases --- lib/users/internal.rb | 37 ++++++++++++++++++++++----------- spec/lib/users/internal_spec.rb | 24 +++++++++++++++++++++ 2 files changed, 49 insertions(+), 12 deletions(-) diff --git a/lib/users/internal.rb b/lib/users/internal.rb index bee09537f86597..0a89b62315ce64 100644 --- a/lib/users/internal.rb +++ b/lib/users/internal.rb @@ -32,7 +32,13 @@ def support_bot_id # rubocop:disable CodeReuse/ActiveRecord -- Need to instantiate a record here def initialize(organization: nil) - @organization = organization + case organization + when ::Organizations::Organization + @organization = organization + @organization_id = organization.id + else + @organization_id = organization + end end # Return (create if necessary) the ghost user. The ghost user @@ -153,28 +159,35 @@ def bot_avatar(image:) private + def organization + return @organization if @organization + + Organizations::Organization.find_by_id(@organization_id) + end + strong_memoize_attr :organization + # NOTE: This method is patched in spec/spec_helper.rb to allow use of exclusive lease in RSpec's # :before_all scope to keep the specs DRY. def unique_internal(scope, username, email_pattern, &block) - if @organization && organization_users_internal_enabled? - scope = scope.joins(:organization_users).where(organization_users: { organization: @organization }) + if @organization_id && organization_users_internal_enabled? # rubocop:disable Style/IfUnlessModifier -- exceeds line length + scope = scope.where(organization_id: @organization_id) end scope.first || create_unique_internal(scope, username, email_pattern, &block) end def username_with_organization_suffix(username) - return username if @organization.nil? || @organization == first_organization + return username if organization.nil? || organization == first_organization return username unless organization_users_internal_enabled? - [username, @organization.path].join('_') + [username, organization.path].join('_') end def display_name_with_organization_suffix(display_name) - return display_name if @organization.nil? || @organization == first_organization + return display_name if organization.nil? || organization == first_organization return display_name unless organization_users_internal_enabled? - "#{display_name} (#{@organization.name})" + "#{display_name} (#{organization.name})" end def first_organization @@ -183,15 +196,15 @@ def first_organization strong_memoize_attr :first_organization def organization_users_internal_enabled? - Feature.enabled?(:organization_users_internal, @organization) + Feature.enabled?(:organization_users_internal, ::Organizations::Organization.actor_from_id(@organization_id)) end strong_memoize_attr :organization_users_internal_enabled? def create_unique_internal(scope, username, email_pattern, &creation_block) # Since we only want a single one of these in an instance, we use an # exclusive lease to ensure than this block is never run concurrently. - lease_key = if @organization - "user:unique_internal:#{@organization.id}:#{username}" + lease_key = if organization + "user:unique_internal:#{@organization_id}:#{username}" else "user:unique_internal:#{username}" end @@ -227,8 +240,8 @@ def create_unique_internal(scope, username, email_pattern, &creation_block) &creation_block ) - user_organization = if @organization && organization_users_internal_enabled? - @organization + user_organization = if organization && organization_users_internal_enabled? + organization else Organizations::Organization.first end diff --git a/spec/lib/users/internal_spec.rb b/spec/lib/users/internal_spec.rb index 84d5e274799003..62d5b734cbadf9 100644 --- a/spec/lib/users/internal_spec.rb +++ b/spec/lib/users/internal_spec.rb @@ -250,6 +250,30 @@ end end + context 'when organization_id is passed instead of organization' do + let_it_be(:organization_id) { organization.id } + + subject(:get_admin_bot) { described_class.for_organization(organization_id).admin_bot } + + context 'and bot exists' do + let_it_be(:admin_bot) { create(:user, :admin_bot, organization: organization) } + + it 'does not load full organization' do + expect(::Organizations::Organization).not_to receive(:find_by_id) + + expect(get_admin_bot).to eq(admin_bot) + end + end + + context 'and bot does not yet exist' do + it 'loads full organization to create new user' do + expect(::Organizations::Organization).to receive(:find_by_id).and_call_original + + expect(get_admin_bot).to be_admin_bot + end + end + end + context 'when organization is not used' do it 'creates user in first organization' do bot = described_class.support_bot -- GitLab From 5dfd23a9f79035b39a86eb17be271fccea17571c Mon Sep 17 00:00:00 2001 From: agius Date: Tue, 19 Aug 2025 11:48:05 -0700 Subject: [PATCH 6/8] Reduce N+1 queries across call-sites This adds memoization for admin_bot lookups in cron workers, and changes methods that passed a full Organization model to pass organization_id where practical. Should reduce db queries and lookups --- .../namespaces/groups/adjourned_deletion_service.rb | 2 +- app/services/projects/adjourned_deletion_service.rb | 2 +- app/services/users/auto_ban_service.rb | 2 +- .../inactive_projects_deletion_cron_worker.rb | 10 ++++++---- .../inactive_tokens_deletion_cron_worker.rb | 7 ++++++- app/workers/users/deactivate_dormant_users_worker.rb | 11 ++++++----- ee/app/models/ee/user.rb | 2 +- .../pipl/block_pipl_users_worker.rb | 7 ++++++- .../pipl/delete_pipl_users_worker.rb | 7 ++++++- .../update_default_framework_worker.rb | 9 ++++++++- .../namespaces/remove_dormant_members_worker.rb | 7 ++++++- .../users/security_policy_bot_cleanup_cron_worker.rb | 9 ++++++++- .../users/unconfirmed_users_deletion_cron_worker.rb | 8 ++++++-- 13 files changed, 62 insertions(+), 21 deletions(-) diff --git a/app/services/namespaces/groups/adjourned_deletion_service.rb b/app/services/namespaces/groups/adjourned_deletion_service.rb index 6dba3e91a72352..3379953d0b2b54 100644 --- a/app/services/namespaces/groups/adjourned_deletion_service.rb +++ b/app/services/namespaces/groups/adjourned_deletion_service.rb @@ -29,7 +29,7 @@ def delete_group end def restore_group - admin_bot = ::Users::Internal.for_organization(group.organization).admin_bot + admin_bot = ::Users::Internal.for_organization(group.organization_id).admin_bot Gitlab::Auth::CurrentUserMode.optionally_run_in_admin_mode(admin_bot) do ::Groups::RestoreService.new(group, admin_bot).execute end diff --git a/app/services/projects/adjourned_deletion_service.rb b/app/services/projects/adjourned_deletion_service.rb index 2b568bc89950e3..51509a294cdcb6 100644 --- a/app/services/projects/adjourned_deletion_service.rb +++ b/app/services/projects/adjourned_deletion_service.rb @@ -30,7 +30,7 @@ def delete_project end def restore_project - admin_bot = ::Users::Internal.for_organization(project.organization).admin_bot + admin_bot = ::Users::Internal.for_organization(project.organization_id).admin_bot Gitlab::Auth::CurrentUserMode.optionally_run_in_admin_mode(admin_bot) do ::Projects::RestoreService.new(project, admin_bot).execute end diff --git a/app/services/users/auto_ban_service.rb b/app/services/users/auto_ban_service.rb index e1500506f6d470..6d3dd5b07d0ece 100644 --- a/app/services/users/auto_ban_service.rb +++ b/app/services/users/auto_ban_service.rb @@ -32,7 +32,7 @@ def ban_user end def admin_bot - Users::Internal.for_organization(user.organization).admin_bot + Users::Internal.for_organization(user.organization_id).admin_bot end def ban_duplicate_users diff --git a/app/workers/projects/inactive_projects_deletion_cron_worker.rb b/app/workers/projects/inactive_projects_deletion_cron_worker.rb index fe796d929cda8b..802a7873ad4c30 100644 --- a/app/workers/projects/inactive_projects_deletion_cron_worker.rb +++ b/app/workers/projects/inactive_projects_deletion_cron_worker.rb @@ -22,9 +22,6 @@ def perform return unless ::Gitlab::CurrentSettings.delete_inactive_projects? @start_time ||= ::Gitlab::Metrics::System.monotonic_time - admin_bot = ::Users::Internal.admin_bot - - return unless admin_bot notified_inactive_projects = Gitlab::DormantProjectsDeletionWarningTracker.notified_projects @@ -39,7 +36,7 @@ def perform raise TimeoutError end - organization_admin_bot = ::Users::Internal.for_organization(project.organization).admin_bot + organization_admin_bot = admin_bot_for_organization_id(project.organization_id) with_context(project: project, user: organization_admin_bot) do deletion_warning_email_sent_on = notified_inactive_projects["project:#{project.id}"] @@ -113,6 +110,11 @@ def reset_last_processed_project_id def with_redis(&block) Gitlab::Redis::Cache.with(&block) # rubocop:disable CodeReuse/ActiveRecord end + + def admin_bot_for_organization_id(organization_id) + @admin_bots ||= {} + @admin_bots[organization_id] ||= Users::Internal.for_organization(organization_id).admin_bot + end end end diff --git a/app/workers/resource_access_tokens/inactive_tokens_deletion_cron_worker.rb b/app/workers/resource_access_tokens/inactive_tokens_deletion_cron_worker.rb index d97c84e536dffc..2d93c1cf0c3bb7 100644 --- a/app/workers/resource_access_tokens/inactive_tokens_deletion_cron_worker.rb +++ b/app/workers/resource_access_tokens/inactive_tokens_deletion_cron_worker.rb @@ -57,7 +57,7 @@ def initiate_deletion_for(users) DeleteUserWorker.bulk_perform_async_with_contexts( users, arguments_proc: ->(user) { - admin_bot_id = Users::Internal.for_organization(user.organization).admin_bot.id + admin_bot_id = admin_bot_id_for_organization_id(user.organization_id) [ admin_bot_id, user.id, { skip_authorization: true, reason_for_deletion: "No active token assigned" } @@ -66,5 +66,10 @@ def initiate_deletion_for(users) context_proc: ->(user) { { user: user } } ) end + + def admin_bot_id_for_organization_id(organization_id) + @admin_bots ||= {} + @admin_bots[organization_id] ||= Users::Internal.for_organization(organization_id).admin_bot.id + end end end diff --git a/app/workers/users/deactivate_dormant_users_worker.rb b/app/workers/users/deactivate_dormant_users_worker.rb index 8501d39e6b655f..c0cc7992cdb685 100644 --- a/app/workers/users/deactivate_dormant_users_worker.rb +++ b/app/workers/users/deactivate_dormant_users_worker.rb @@ -15,10 +15,6 @@ def perform return unless ::Gitlab::CurrentSettings.current_application_settings.deactivate_dormant_users - # Ensure at least one admin bot has been set up - admin_bot = Users::Internal.admin_bot - return unless admin_bot - deactivate_users(User.dormant) deactivate_users(User.with_no_activity) end @@ -36,10 +32,15 @@ def deactivate_users(scope) end def process_user_deactivation(user) - admin_bot = Users::Internal.for_organization(user.organization).admin_bot + admin_bot = admin_bot_for_organization_id(user.organization_id) Gitlab::Auth::CurrentUserMode.bypass_session!(admin_bot.id) do Users::DeactivateService.new(admin_bot).execute(user) end end + + def admin_bot_for_organization_id(organization_id) + @admin_bots ||= {} + @admin_bots[organization_id] ||= Users::Internal.for_organization(organization_id).admin_bot + end end end diff --git a/ee/app/models/ee/user.rb b/ee/app/models/ee/user.rb index d4aee18f289414..696eaf87cacff3 100644 --- a/ee/app/models/ee/user.rb +++ b/ee/app/models/ee/user.rb @@ -902,7 +902,7 @@ def audit_lock_access(reason: nil) ::Gitlab::Audit::Auditor.audit( name: 'user_access_locked', - author: ::Users::Internal.for_organization(organization).admin_bot, + author: ::Users::Internal.for_organization(organization_id).admin_bot, scope: self, target: self, message: ['User access locked', reason].compact.join(' - ') diff --git a/ee/app/workers/compliance_management/pipl/block_pipl_users_worker.rb b/ee/app/workers/compliance_management/pipl/block_pipl_users_worker.rb index b7a552b7dceb8b..0693b1d90f2605 100644 --- a/ee/app/workers/compliance_management/pipl/block_pipl_users_worker.rb +++ b/ee/app/workers/compliance_management/pipl/block_pipl_users_worker.rb @@ -27,7 +27,7 @@ def perform private def block_user(pipl_user) - admin_bot = ::Users::Internal.for_organization(pipl_user.user.organization).admin_bot + admin_bot = admin_bot_for_organization_id(pipl_user.user.organization_id) Gitlab::Auth::CurrentUserMode.optionally_run_in_admin_mode(admin_bot) do process_block_user(pipl_user, admin_bot) @@ -42,6 +42,11 @@ def process_block_user(pipl_user, admin_bot) Gitlab::AppLogger .info(message: "Error blocking user: #{pipl_user.user.id} with message #{result.message}", jid: jid) end + + def admin_bot_for_organization_id(organization_id) + @admin_bots ||= {} + @admin_bots[organization_id] ||= Users::Internal.for_organization(organization_id).admin_bot + end end end end diff --git a/ee/app/workers/compliance_management/pipl/delete_pipl_users_worker.rb b/ee/app/workers/compliance_management/pipl/delete_pipl_users_worker.rb index 3e59b365a106fd..2280996b2e3531 100644 --- a/ee/app/workers/compliance_management/pipl/delete_pipl_users_worker.rb +++ b/ee/app/workers/compliance_management/pipl/delete_pipl_users_worker.rb @@ -27,7 +27,7 @@ def perform private def delete_user(pipl_user) - admin_bot = ::Users::Internal.for_organization(pipl_user.user.organization).admin_bot + admin_bot = admin_bot_for_organization_id(pipl_user.user.organization_id) Gitlab::Auth::CurrentUserMode.optionally_run_in_admin_mode(admin_bot) do process_delete_user(pipl_user, admin_bot) @@ -42,6 +42,11 @@ def process_delete_user(pipl_user, admin_bot) Gitlab::AppLogger .info(message: "Error blocking user: #{pipl_user.user.id} with message #{result.message}", jid: jid) end + + def admin_bot_for_organization_id(organization_id) + @admin_bots ||= {} + @admin_bots[organization_id] ||= Users::Internal.for_organization(organization_id).admin_bot + end end end end diff --git a/ee/app/workers/compliance_management/update_default_framework_worker.rb b/ee/app/workers/compliance_management/update_default_framework_worker.rb index 62b2b2315e8ddc..91f546b03f1d43 100644 --- a/ee/app/workers/compliance_management/update_default_framework_worker.rb +++ b/ee/app/workers/compliance_management/update_default_framework_worker.rb @@ -11,7 +11,7 @@ class UpdateDefaultFrameworkWorker def perform(_user_id, project_id, compliance_framework_id) project = Project.find(project_id) - admin_bot = ::Users::Internal.for_organization(project.organization).admin_bot + admin_bot = admin_bot_for_organization_id(project.organization_id) Gitlab::Auth::CurrentUserMode.bypass_session!(admin_bot.id) do ::ComplianceManagement::Frameworks::UpdateProjectService @@ -21,5 +21,12 @@ def perform(_user_id, project_id, compliance_framework_id) rescue ActiveRecord::RecordNotFound => e Gitlab::ErrorTracking.log_exception(e) end + + private + + def admin_bot_for_organization_id(organization_id) + @admin_bots ||= {} + @admin_bots[organization_id] ||= Users::Internal.for_organization(organization_id).admin_bot + end end end diff --git a/ee/app/workers/namespaces/remove_dormant_members_worker.rb b/ee/app/workers/namespaces/remove_dormant_members_worker.rb index cdc24026bf1aee..70417ed72e2335 100644 --- a/ee/app/workers/namespaces/remove_dormant_members_worker.rb +++ b/ee/app/workers/namespaces/remove_dormant_members_worker.rb @@ -57,7 +57,7 @@ def namespaces_requiring_dormant_member_removal(limit = 1) def remove_dormant_members(namespace) dormant_period = namespace.namespace_settings.remove_dormant_members_period.days.ago - admin_bot = ::Users::Internal.for_organization(namespace.organization).admin_bot + admin_bot = admin_bot_for_organization_id(namespace.organization) dormant_count = 0 ::GitlabSubscriptions::SeatAssignment.dormant_in_namespace(namespace, dormant_period).find_each do |assignment| @@ -88,5 +88,10 @@ def log_monitoring_data(namespace_id, dormant_count) dormant_count: dormant_count ) end + + def admin_bot_for_organization_id(organization_id) + @admin_bots ||= {} + @admin_bots[organization_id] ||= Users::Internal.for_organization(organization_id).admin_bot + end end end diff --git a/ee/app/workers/users/security_policy_bot_cleanup_cron_worker.rb b/ee/app/workers/users/security_policy_bot_cleanup_cron_worker.rb index afd436ef28f135..96b495a0639aa8 100644 --- a/ee/app/workers/users/security_policy_bot_cleanup_cron_worker.rb +++ b/ee/app/workers/users/security_policy_bot_cleanup_cron_worker.rb @@ -25,7 +25,7 @@ def perform break if users.empty? users.each do |user| - admin_user = Users::Internal.for_organization(user.organization).admin_bot + admin_user = admin_bot_for_organization_id(user.organization_id) destroy_service = Users::DestroyService.new(admin_user) with_context(user: user) do @@ -37,5 +37,12 @@ def perform end end end + + private + + def admin_bot_for_organization_id(organization_id) + @admin_bots ||= {} + @admin_bots[organization_id] ||= Users::Internal.for_organization(organization_id).admin_bot + end end end diff --git a/ee/app/workers/users/unconfirmed_users_deletion_cron_worker.rb b/ee/app/workers/users/unconfirmed_users_deletion_cron_worker.rb index a87a08b4b3e9fb..5a81e1ec926964 100644 --- a/ee/app/workers/users/unconfirmed_users_deletion_cron_worker.rb +++ b/ee/app/workers/users/unconfirmed_users_deletion_cron_worker.rb @@ -18,7 +18,6 @@ def perform return if ::Gitlab::CurrentSettings.email_confirmation_setting_off? return unless ::Gitlab::CurrentSettings.delete_unconfirmed_users? return unless License.feature_available?(:delete_unconfirmed_users) - return unless Users::Internal.admin_bot in_lock(self.class.name.underscore, ttl: Gitlab::Utils::ExecutionTracker::MAX_RUNTIME, retries: 0) do order = Gitlab::Pagination::Keyset::Order.build([ @@ -55,7 +54,7 @@ def delete_users(scope:) DeleteUserWorker.bulk_perform_async_with_contexts( relation, arguments_proc: ->(user) { - admin_bot = ::Users::Internal.for_organization(user.organization).admin_bot + admin_bot = admin_bot_for_organization_id(user.organization_id) [admin_bot.id, user.id, { skip_authorization: true, reason_for_deletion: reason_for_deletion }] }, context_proc: ->(user) { { user: user } } @@ -73,5 +72,10 @@ def reason_for_deletion "#{::Gitlab::CurrentSettings.unconfirmed_users_delete_after_days} days since their creation" end strong_memoize_attr :reason_for_deletion + + def admin_bot_for_organization_id(organization_id) + @admin_bots ||= {} + @admin_bots[organization_id] ||= Users::Internal.for_organization(organization_id).admin_bot + end end end -- GitLab From c600a47bf72fd6e27890223f90204a9fdda7a101 Mon Sep 17 00:00:00 2001 From: agius Date: Tue, 19 Aug 2025 12:40:02 -0700 Subject: [PATCH 7/8] Remove all remaining Users::Internal.admin_bot This removes all the calls to Users::Internal.admin_bot, and ensures an Organization is used in all call-sites including in specs --- .../mark_admin_bot_runners_as_hosted_spec.rb | 2 +- ee/spec/models/ee/ci/runner_spec.rb | 5 +++-- .../policies/compliance_management/pipl_user_policy_spec.rb | 2 +- .../pipl/block_non_compliant_user_service_spec.rb | 2 +- .../inactive_tokens_deletion_cron_worker_spec.rb | 2 +- .../users/unconfirmed_users_deletion_cron_worker_spec.rb | 3 +-- lib/tasks/ci/allowlist_migration.rake | 2 +- spec/factories/ci/runners.rb | 2 +- .../background_migration/delete_orphaned_groups_spec.rb | 2 +- spec/models/ci/job_token/allowlist_migration_task_spec.rb | 2 +- spec/policies/base_policy_spec.rb | 2 +- spec/services/members/schedule_deletion_service_spec.rb | 2 +- .../projects/import_export/relation_import_service_spec.rb | 2 +- spec/tasks/ci/allowlist_migration_rake_spec.rb | 2 +- 14 files changed, 16 insertions(+), 16 deletions(-) diff --git a/ee/spec/lib/ee/gitlab/background_migration/mark_admin_bot_runners_as_hosted_spec.rb b/ee/spec/lib/ee/gitlab/background_migration/mark_admin_bot_runners_as_hosted_spec.rb index 4dd7a2113e5f9a..2d4ad0c71b3a87 100644 --- a/ee/spec/lib/ee/gitlab/background_migration/mark_admin_bot_runners_as_hosted_spec.rb +++ b/ee/spec/lib/ee/gitlab/background_migration/mark_admin_bot_runners_as_hosted_spec.rb @@ -4,7 +4,7 @@ RSpec.describe Gitlab::BackgroundMigration::MarkAdminBotRunnersAsHosted, feature_category: :hosted_runners do - let!(:admin_bot) { Users::Internal.admin_bot } + let!(:admin_bot) { ::Users::Internal.for_organization(::Organizations::Organization.first).admin_bot } let!(:admin_bot_runner) do table(:ci_runners, database: :ci, primary_key: :id) diff --git a/ee/spec/models/ee/ci/runner_spec.rb b/ee/spec/models/ee/ci/runner_spec.rb index 5a6550419d6876..a234217c814b18 100644 --- a/ee/spec/models/ee/ci/runner_spec.rb +++ b/ee/spec/models/ee/ci/runner_spec.rb @@ -5,6 +5,7 @@ RSpec.describe Ci::Runner, feature_category: :hosted_runners do let_it_be(:namespace) { create(:group, created_at: Date.new(2021, 7, 16)) } let_it_be(:project) { create(:project) } + let_it_be(:admin_bot) { create(:user, :admin_bot) } let(:shared_runners_minutes) { 400 } @@ -42,7 +43,7 @@ end context 'with an admin bot created runner' do - let_it_be(:runner) { create(:ci_runner, creator: Users::Internal.admin_bot) } + let_it_be(:runner) { create(:ci_runner, creator: admin_bot) } it 'returns true' do expect(runner.dedicated_gitlab_hosted?).to be_truthy @@ -67,7 +68,7 @@ end context 'when not on dedicated installation' do - let_it_be(:runner) { create(:ci_runner, creator: Users::Internal.admin_bot) } + let_it_be(:runner) { create(:ci_runner, creator: admin_bot) } before do allow(Gitlab::CurrentSettings).to receive(:gitlab_dedicated_instance?).and_return(false) diff --git a/ee/spec/policies/compliance_management/pipl_user_policy_spec.rb b/ee/spec/policies/compliance_management/pipl_user_policy_spec.rb index 75007bd62fcaa3..c101a9ced4e62c 100644 --- a/ee/spec/policies/compliance_management/pipl_user_policy_spec.rb +++ b/ee/spec/policies/compliance_management/pipl_user_policy_spec.rb @@ -6,7 +6,7 @@ let_it_be(:pipl_user) { create(:pipl_user) } let_it_be(:simple_user) { create(:user) } - let_it_be(:admin_user) { Users::Internal.admin_bot } + let_it_be(:admin_user) { create(:user, :admin_bot) } let(:current_user) { admin_user } subject { described_class.new(current_user, pipl_user) } diff --git a/ee/spec/services/compliance_management/pipl/block_non_compliant_user_service_spec.rb b/ee/spec/services/compliance_management/pipl/block_non_compliant_user_service_spec.rb index e10ead806cfa87..95f584bbfb4f29 100644 --- a/ee/spec/services/compliance_management/pipl/block_non_compliant_user_service_spec.rb +++ b/ee/spec/services/compliance_management/pipl/block_non_compliant_user_service_spec.rb @@ -94,7 +94,7 @@ end context 'when the block operation fails' do - let(:pipl_user) { create(:pipl_user, user: Users::Internal.admin_bot, initial_email_sent_at: 60.days.ago) } + let(:pipl_user) { create(:pipl_user, user: create(:user, :admin_bot), initial_email_sent_at: 60.days.ago) } it_behaves_like 'does not block the user' it_behaves_like 'has a validation error', diff --git a/ee/spec/workers/resource_access_tokens/inactive_tokens_deletion_cron_worker_spec.rb b/ee/spec/workers/resource_access_tokens/inactive_tokens_deletion_cron_worker_spec.rb index 6143a17c269735..821e0d017f3d6c 100644 --- a/ee/spec/workers/resource_access_tokens/inactive_tokens_deletion_cron_worker_spec.rb +++ b/ee/spec/workers/resource_access_tokens/inactive_tokens_deletion_cron_worker_spec.rb @@ -26,7 +26,7 @@ audit_event = AuditEvent.where("details LIKE ?", "%user_destroyed%").last - author = Users::Internal.admin_bot + author = Users::Internal.for_organization(resource.organization).admin_bot expect(audit_event.author_id).to eq(author.id) expect(audit_event.entity).to eq(resource) expect(audit_event.details).to eq({ diff --git a/ee/spec/workers/users/unconfirmed_users_deletion_cron_worker_spec.rb b/ee/spec/workers/users/unconfirmed_users_deletion_cron_worker_spec.rb index d2e7d8ba700842..a13b23b919f3e9 100644 --- a/ee/spec/workers/users/unconfirmed_users_deletion_cron_worker_spec.rb +++ b/ee/spec/workers/users/unconfirmed_users_deletion_cron_worker_spec.rb @@ -10,7 +10,7 @@ let_it_be(:confirmed_user) { create(:user, created_at: cut_off_datetime - 1.day) } let_it_be(:banned_user) { create(:user, :banned, :unconfirmed, created_at: cut_off_datetime - 1.day) } let_it_be(:bot_user) { create(:user, :bot, :unconfirmed, created_at: cut_off_datetime - 1.day) } - let_it_be(:admin_bot) { ::Users::Internal.admin_bot } + let_it_be(:admin_bot) { create(:user, :admin_bot) } let_it_be(:unconfirmed_user_created_after_cut_off) do create(:user, :unconfirmed, created_at: cut_off_datetime + 1.day) @@ -30,7 +30,6 @@ end it 'destroys unconfirmed users who never signed in & signed up before unconfirmed_users_delete_after_days days' do - admin_bot = ::Users::Internal.admin_bot users_to_keep = [ unconfirmed_user_created_after_cut_off, unconfirmed_user_who_signed_in, diff --git a/lib/tasks/ci/allowlist_migration.rake b/lib/tasks/ci/allowlist_migration.rake index 5bd7ad540090c9..98ab03075f33e0 100644 --- a/lib/tasks/ci/allowlist_migration.rake +++ b/lib/tasks/ci/allowlist_migration.rake @@ -15,7 +15,7 @@ namespace :ci do task = ::Ci::JobToken::AllowlistMigrationTask.new(only_ids: only_ids, exclude_ids: exclude_ids, preview: preview, - user: ::Users::Internal.admin_bot, + user: ::Users::Internal.for_organization(::Organizations::Organization.first).admin_bot, concurrency: concurrency.to_i) task.execute diff --git a/spec/factories/ci/runners.rb b/spec/factories/ci/runners.rb index 35ccb1b2f6ed2f..bf6223f60ba82d 100644 --- a/spec/factories/ci/runners.rb +++ b/spec/factories/ci/runners.rb @@ -161,7 +161,7 @@ end trait :hosted_runner do - creator { Users::Internal.admin_bot } + creator { Users::Internal.for_organization(organization_id).admin_bot } end end end diff --git a/spec/lib/gitlab/background_migration/delete_orphaned_groups_spec.rb b/spec/lib/gitlab/background_migration/delete_orphaned_groups_spec.rb index 4d34cb55602792..f0f39a87acaedb 100644 --- a/spec/lib/gitlab/background_migration/delete_orphaned_groups_spec.rb +++ b/spec/lib/gitlab/background_migration/delete_orphaned_groups_spec.rb @@ -8,7 +8,7 @@ let(:organization) { organizations.create!(name: 'Foobar', path: 'path1') } let!(:parent) { namespaces.create!(name: 'Group', type: 'Group', path: 'space1', organization_id: organization.id) } let!(:group) { namespaces.create!(name: 'GitLab', type: 'Group', path: 'group1', organization_id: organization.id) } - let!(:admin_bot) { ::Users::Internal.admin_bot } + let!(:admin_bot) { ::Users::Internal.for_organization(organization).admin_bot } let!(:orphaned_groups) do (1..4).map do |i| namespaces.create!( diff --git a/spec/models/ci/job_token/allowlist_migration_task_spec.rb b/spec/models/ci/job_token/allowlist_migration_task_spec.rb index b49302016cfac1..995c6619bdcb00 100644 --- a/spec/models/ci/job_token/allowlist_migration_task_spec.rb +++ b/spec/models/ci/job_token/allowlist_migration_task_spec.rb @@ -8,7 +8,7 @@ let(:preview) { nil } let(:output_stream) { StringIO.new } let(:path) { Rails.root.join('tmp/tests/doc/ci/jobs') } - let(:user) { ::Users::Internal.admin_bot } + let(:user) { create(:user, :admin_bot) } let(:concurrency) { 4 } let(:task) do diff --git a/spec/policies/base_policy_spec.rb b/spec/policies/base_policy_spec.rb index 018ec0a80e3956..060ab3dcf21b97 100644 --- a/spec/policies/base_policy_spec.rb +++ b/spec/policies/base_policy_spec.rb @@ -66,7 +66,7 @@ def policy end context 'with the admin bot user' do - let(:current_user) { ::Users::Internal.admin_bot } + let(:current_user) { create(:user, :admin_bot) } it { is_expected.to be_allowed(ability) } end diff --git a/spec/services/members/schedule_deletion_service_spec.rb b/spec/services/members/schedule_deletion_service_spec.rb index b84ef2c15ab2a6..d254d1ddc73fc1 100644 --- a/spec/services/members/schedule_deletion_service_spec.rb +++ b/spec/services/members/schedule_deletion_service_spec.rb @@ -81,7 +81,7 @@ end context 'when the user is an admin bot', :enable_admin_mode do - let_it_be(:scheduled_by) { Users::Internal.admin_bot } + let_it_be(:scheduled_by) { create(:user, :admin_bot) } it 'creates a deletion schedule' do result = service.execute diff --git a/spec/services/projects/import_export/relation_import_service_spec.rb b/spec/services/projects/import_export/relation_import_service_spec.rb index a115daa9c0bbee..f9cec9cb04b164 100644 --- a/spec/services/projects/import_export/relation_import_service_spec.rb +++ b/spec/services/projects/import_export/relation_import_service_spec.rb @@ -71,7 +71,7 @@ end context 'and the user is an admin bot' do - let_it_be(:user) { Users::Internal.admin_bot } + let_it_be(:user) { create(:user, :admin_bot) } include_examples 'returns success response' end diff --git a/spec/tasks/ci/allowlist_migration_rake_spec.rb b/spec/tasks/ci/allowlist_migration_rake_spec.rb index 00bb8f4c477fa2..09b8fdd02c9949 100644 --- a/spec/tasks/ci/allowlist_migration_rake_spec.rb +++ b/spec/tasks/ci/allowlist_migration_rake_spec.rb @@ -4,7 +4,7 @@ RSpec.describe 'ci:job_tokens:allowlist rake tasks', feature_category: :secrets_management do let(:task_class) { ::Ci::JobToken::AllowlistMigrationTask } - let(:user) { ::Users::Internal.admin_bot } + let(:user) { create(:user, :admin_bot) } before do Rake.application.rake_require('tasks/ci/allowlist_migration') -- GitLab From c1ed107e3f41a1be5f55c4f803827b4f0ba98a17 Mon Sep 17 00:00:00 2001 From: agius Date: Wed, 20 Aug 2025 09:49:10 -0700 Subject: [PATCH 8/8] Incorporate review feedback --- ee/app/workers/namespaces/remove_dormant_members_worker.rb | 2 +- lib/gitlab/background_migration/delete_orphaned_groups.rb | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/ee/app/workers/namespaces/remove_dormant_members_worker.rb b/ee/app/workers/namespaces/remove_dormant_members_worker.rb index 70417ed72e2335..37b31ee55ef1bf 100644 --- a/ee/app/workers/namespaces/remove_dormant_members_worker.rb +++ b/ee/app/workers/namespaces/remove_dormant_members_worker.rb @@ -57,7 +57,7 @@ def namespaces_requiring_dormant_member_removal(limit = 1) def remove_dormant_members(namespace) dormant_period = namespace.namespace_settings.remove_dormant_members_period.days.ago - admin_bot = admin_bot_for_organization_id(namespace.organization) + admin_bot = admin_bot_for_organization_id(namespace.organization_id) dormant_count = 0 ::GitlabSubscriptions::SeatAssignment.dormant_in_namespace(namespace, dormant_period).find_each do |assignment| diff --git a/lib/gitlab/background_migration/delete_orphaned_groups.rb b/lib/gitlab/background_migration/delete_orphaned_groups.rb index b1a31fdb8adc61..e712551510db9e 100644 --- a/lib/gitlab/background_migration/delete_orphaned_groups.rb +++ b/lib/gitlab/background_migration/delete_orphaned_groups.rb @@ -14,10 +14,10 @@ def perform .joins("LEFT JOIN namespaces AS parent ON namespaces.parent_id = parent.id") .where(parent: { id: nil }) .pluck(:id).each do |orphaned_group_id| - organization = Group.where(id: orphaned_group_id).includes(:organization).first&.organization + organization_id = Group.where(id: orphaned_group_id).includes(:organization).first&.organization_id # if organization is nil, returns the default organization admin_bot - admin_bot = Users::Internal.for_organization(organization).admin_bot + admin_bot = Users::Internal.for_organization(organization_id).admin_bot ::GroupDestroyWorker.perform_async(orphaned_group_id, admin_bot.id) end -- GitLab