162 lines
5.0 KiB
Ruby
162 lines
5.0 KiB
Ruby
# frozen_string_literal: true
|
|
|
|
module ProjectAuthorizations
|
|
# How to use this class
|
|
# authorizations_to_add:
|
|
# Rows to insert in the form `[{ user_id: user_id, project_id: project_id, access_level: access_level}, ...]
|
|
#
|
|
# ProjectAuthorizations::Changes.new do |changes|
|
|
# changes.add(authorizations_to_add)
|
|
# changes.remove_users_in_project(project, user_ids)
|
|
# changes.remove_projects_for_user(user, project_ids)
|
|
# end.apply!
|
|
class Changes
|
|
attr_reader :projects_to_remove, :users_to_remove_in_project, :authorizations_to_add
|
|
|
|
BATCH_SIZE = 1000
|
|
EVENT_USER_BATCH_SIZE = 100
|
|
SLEEP_DELAY = 0.1
|
|
|
|
def initialize
|
|
@authorizations_to_add = []
|
|
@affected_project_ids = Set.new
|
|
@removed_user_ids = Set.new
|
|
yield self
|
|
end
|
|
|
|
def add(authorizations_to_add)
|
|
@authorizations_to_add += authorizations_to_add
|
|
end
|
|
|
|
def remove_users_in_project(project, user_ids)
|
|
@users_to_remove_in_project = { user_ids: user_ids, scope: project }
|
|
end
|
|
|
|
def remove_projects_for_user(user, project_ids)
|
|
@projects_to_remove = { project_ids: project_ids, scope: user }
|
|
end
|
|
|
|
def apply!
|
|
delete_authorizations_for_user if should_delete_authorizations_for_user?
|
|
delete_authorizations_for_project if should_delete_authorizations_for_project?
|
|
add_authorizations if should_add_authorization?
|
|
|
|
publish_events
|
|
end
|
|
|
|
private
|
|
|
|
def should_add_authorization?
|
|
authorizations_to_add.present?
|
|
end
|
|
|
|
def should_delete_authorizations_for_user?
|
|
user && project_ids.present?
|
|
end
|
|
|
|
def should_delete_authorizations_for_project?
|
|
project && user_ids.present?
|
|
end
|
|
|
|
def add_authorizations
|
|
insert_all_in_batches(authorizations_to_add)
|
|
@affected_project_ids += authorizations_to_add.pluck(:project_id)
|
|
end
|
|
|
|
def delete_authorizations_for_user
|
|
delete_all_in_batches(resource: user,
|
|
ids_to_remove: project_ids,
|
|
column_name_of_ids_to_remove: :project_id)
|
|
@affected_project_ids += project_ids
|
|
@removed_user_ids.add(user.id)
|
|
end
|
|
|
|
def delete_authorizations_for_project
|
|
delete_all_in_batches(resource: project,
|
|
ids_to_remove: user_ids,
|
|
column_name_of_ids_to_remove: :user_id)
|
|
@affected_project_ids << project.id
|
|
@removed_user_ids += user_ids
|
|
end
|
|
|
|
def delete_all_in_batches(resource:, ids_to_remove:, column_name_of_ids_to_remove:)
|
|
add_delay = add_delay_between_batches?(entire_size: ids_to_remove.size, batch_size: BATCH_SIZE)
|
|
log_details(entire_size: ids_to_remove.size, batch_size: BATCH_SIZE) if add_delay
|
|
|
|
ids_to_remove.each_slice(BATCH_SIZE) do |ids_batch|
|
|
resource.project_authorizations.where(column_name_of_ids_to_remove => ids_batch).delete_all
|
|
perform_delay if add_delay
|
|
end
|
|
end
|
|
|
|
def insert_all_in_batches(attributes)
|
|
add_delay = add_delay_between_batches?(entire_size: attributes.size, batch_size: BATCH_SIZE)
|
|
log_details(entire_size: attributes.size, batch_size: BATCH_SIZE) if add_delay
|
|
|
|
attributes.each_slice(BATCH_SIZE) do |attributes_batch|
|
|
attributes_batch.each { |attrs| attrs[:is_unique] = true }
|
|
|
|
ProjectAuthorization.insert_all(attributes_batch)
|
|
perform_delay if add_delay
|
|
end
|
|
end
|
|
|
|
def add_delay_between_batches?(entire_size:, batch_size:)
|
|
# The reason for adding a delay is to give the replica database enough time to
|
|
# catch up with the primary when large batches of records are being added/removed.
|
|
# Hence, we add a delay only if the GitLab installation has a replica database configured.
|
|
entire_size > batch_size &&
|
|
!::Gitlab::Database::LoadBalancing.primary_only?
|
|
end
|
|
|
|
def log_details(entire_size:, batch_size:)
|
|
Gitlab::AppLogger.info(
|
|
entire_size: entire_size,
|
|
total_delay: (entire_size / batch_size.to_f).ceil * SLEEP_DELAY,
|
|
message: 'Project authorizations refresh performed with delay',
|
|
**Gitlab::ApplicationContext.current
|
|
)
|
|
end
|
|
|
|
def perform_delay
|
|
sleep(SLEEP_DELAY)
|
|
end
|
|
|
|
def user
|
|
projects_to_remove&.[](:scope)
|
|
end
|
|
|
|
def project_ids
|
|
projects_to_remove&.[](:project_ids)
|
|
end
|
|
|
|
def project
|
|
users_to_remove_in_project&.[](:scope)
|
|
end
|
|
|
|
def user_ids
|
|
users_to_remove_in_project&.[](:user_ids)
|
|
end
|
|
|
|
def publish_events
|
|
@affected_project_ids.each do |project_id|
|
|
::Gitlab::EventStore.publish(
|
|
::ProjectAuthorizations::AuthorizationsChangedEvent.new(data: { project_id: project_id })
|
|
)
|
|
end
|
|
return if ::Feature.disabled?(:user_approval_rules_removal) || @removed_user_ids.blank?
|
|
|
|
@affected_project_ids.each do |project_id|
|
|
@removed_user_ids.to_a.each_slice(EVENT_USER_BATCH_SIZE).each do |user_ids_batch|
|
|
::Gitlab::EventStore.publish(
|
|
::ProjectAuthorizations::AuthorizationsRemovedEvent.new(data: {
|
|
project_id: project_id,
|
|
user_ids: user_ids_batch
|
|
})
|
|
)
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|