94 lines
3.7 KiB
Ruby
94 lines
3.7 KiB
Ruby
# frozen_string_literal: true
|
|
|
|
module Users
|
|
# Service for refreshing the authorized projects of a user.
|
|
#
|
|
# This particular service class can not be used to update data for the same
|
|
# user concurrently. Doing so could lead to an incorrect state. To ensure this
|
|
# doesn't happen a caller must synchronize access (e.g. using
|
|
# `Gitlab::ExclusiveLease`).
|
|
#
|
|
# Usage:
|
|
#
|
|
# user = User.find_by(username: 'alice')
|
|
# service = Users::RefreshAuthorizedProjectsService.new(some_user)
|
|
# service.execute
|
|
class RefreshAuthorizedProjectsService
|
|
attr_reader :user, :source
|
|
|
|
LEASE_TIMEOUT = 1.minute.to_i
|
|
|
|
# user - The User for which to refresh the authorized projects.
|
|
def initialize(user, source: nil, incorrect_auth_found_callback: nil, missing_auth_found_callback: nil)
|
|
@user = user
|
|
@source = source
|
|
@incorrect_auth_found_callback = incorrect_auth_found_callback
|
|
@missing_auth_found_callback = missing_auth_found_callback
|
|
end
|
|
|
|
def execute
|
|
lease_key = "refresh_authorized_projects:#{user.id}"
|
|
lease = Gitlab::ExclusiveLease.new(lease_key, timeout: LEASE_TIMEOUT)
|
|
|
|
until uuid = lease.try_obtain
|
|
# Keep trying until we obtain the lease. If we don't do so we may end up
|
|
# not updating the list of authorized projects properly. To prevent
|
|
# hammering Redis too much we'll wait for a bit between retries.
|
|
sleep(0.1)
|
|
end
|
|
|
|
begin
|
|
# We need an up to date User object that has access to all relations that
|
|
# may have been created earlier. The only way to ensure this is to reload
|
|
# the User object.
|
|
user.reset
|
|
execute_without_lease
|
|
ensure
|
|
Gitlab::ExclusiveLease.cancel(lease_key, uuid)
|
|
end
|
|
end
|
|
|
|
# This method returns the updated User object.
|
|
def execute_without_lease
|
|
remove, add = AuthorizedProjectUpdate::FindRecordsDueForRefreshService.new(
|
|
user,
|
|
source: source,
|
|
incorrect_auth_found_callback: incorrect_auth_found_callback,
|
|
missing_auth_found_callback: missing_auth_found_callback
|
|
).execute
|
|
|
|
update_authorizations(remove, add)
|
|
end
|
|
|
|
# Updates the list of authorizations for the current user.
|
|
#
|
|
# remove - The IDs of the authorization rows to remove.
|
|
# add - Rows to insert in the form `[{ user_id: user_id, project_id: project_id, access_level: access_level}, ...]`
|
|
def update_authorizations(remove = [], add = [])
|
|
log_refresh_details(remove, add)
|
|
|
|
user.remove_project_authorizations(remove) if remove.any?
|
|
ProjectAuthorization.insert_all_in_batches(add) if add.any?
|
|
|
|
# Since we batch insert authorization rows, Rails' associations may get
|
|
# out of sync. As such we force a reload of the User object.
|
|
user.reset
|
|
end
|
|
|
|
private
|
|
|
|
attr_reader :incorrect_auth_found_callback, :missing_auth_found_callback
|
|
|
|
def log_refresh_details(remove, add)
|
|
Gitlab::AppJsonLogger.info(event: 'authorized_projects_refresh',
|
|
user_id: user.id,
|
|
'authorized_projects_refresh.source': source,
|
|
'authorized_projects_refresh.rows_deleted_count': remove.length,
|
|
'authorized_projects_refresh.rows_added_count': add.length,
|
|
# most often there's only a few entries in remove and add, but limit it to the first 5
|
|
# entries to avoid flooding the logs
|
|
'authorized_projects_refresh.rows_deleted_slice': remove.first(5),
|
|
'authorized_projects_refresh.rows_added_slice': add.first(5).map(&:values))
|
|
end
|
|
end
|
|
end
|