Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
Diffstat (limited to 'app/services/users')
-rw-r--r--app/services/users/authorized_build_service.rb2
-rw-r--r--app/services/users/destroy_service.rb51
-rw-r--r--app/services/users/email_verification/base_service.rb27
-rw-r--r--app/services/users/email_verification/generate_token_service.rb21
-rw-r--r--app/services/users/email_verification/validate_token_service.rb78
-rw-r--r--app/services/users/migrate_records_to_ghost_user_in_batches_service.rb26
-rw-r--r--app/services/users/migrate_records_to_ghost_user_service.rb111
7 files changed, 299 insertions, 17 deletions
diff --git a/app/services/users/authorized_build_service.rb b/app/services/users/authorized_build_service.rb
index eb2386198d3..5029105b087 100644
--- a/app/services/users/authorized_build_service.rb
+++ b/app/services/users/authorized_build_service.rb
@@ -16,3 +16,5 @@ module Users
end
end
end
+
+Users::AuthorizedBuildService.prepend_mod_with('Users::AuthorizedBuildService')
diff --git a/app/services/users/destroy_service.rb b/app/services/users/destroy_service.rb
index dfa9316889e..a378cb09854 100644
--- a/app/services/users/destroy_service.rb
+++ b/app/services/users/destroy_service.rb
@@ -23,6 +23,11 @@ module Users
# `hard_delete: true` implies `delete_solo_owned_groups: true`. To perform
# a hard deletion without destroying solo-owned groups, pass
# `delete_solo_owned_groups: false, hard_delete: true` in +options+.
+ #
+ # To make the service asynchronous, a new behaviour is being introduced
+ # behind the user_destroy_with_limited_execution_time_worker feature flag.
+ # Migrating the associated user records, and post-migration cleanup is
+ # handled by the Users::MigrateRecordsToGhostUserWorker cron worker.
def execute(user, options = {})
delete_solo_owned_groups = options.fetch(:delete_solo_owned_groups, options[:hard_delete])
@@ -35,12 +40,14 @@ module Users
return user
end
- # Calling all before/after_destroy hooks for the user because
- # there is no dependent: destroy in the relationship. And the removal
- # is done by a foreign_key. Otherwise they won't be called
- user.members.find_each { |member| member.run_callbacks(:destroy) }
+ user.block
+
+ # Load the records. Groups are unavailable after membership is destroyed.
+ solo_owned_groups = user.solo_owned_groups.load
+
+ user.members.each_batch { |batch| batch.destroy_all } # rubocop:disable Style/SymbolProc, Cop/DestroyAll
- user.solo_owned_groups.each do |group|
+ solo_owned_groups.each do |group|
Groups::DestroyService.new(group, current_user).execute
end
@@ -54,22 +61,32 @@ module Users
yield(user) if block_given?
- MigrateToGhostUserService.new(user).execute(hard_delete: options[:hard_delete])
+ hard_delete = options.fetch(:hard_delete, false)
- response = Snippets::BulkDestroyService.new(current_user, user.snippets).execute(options)
- raise DestroyError, response.message if response.error?
+ if Feature.enabled?(:user_destroy_with_limited_execution_time_worker)
+ Users::GhostUserMigration.create!(user: user,
+ initiator_user: current_user,
+ hard_delete: hard_delete)
- # Rails attempts to load all related records into memory before
- # destroying: https://github.com/rails/rails/issues/22510
- # This ensures we delete records in batches.
- user.destroy_dependent_associations_in_batches(exclude: [:snippets])
- user.nullify_dependent_associations_in_batches
+ else
+ MigrateToGhostUserService.new(user).execute(hard_delete: options[:hard_delete])
- # Destroy the namespace after destroying the user since certain methods may depend on the namespace existing
- user_data = user.destroy
- namespace.destroy
+ response = Snippets::BulkDestroyService.new(current_user, user.snippets)
+ .execute(skip_authorization: hard_delete)
+ raise DestroyError, response.message if response.error?
- user_data
+ # Rails attempts to load all related records into memory before
+ # destroying: https://github.com/rails/rails/issues/22510
+ # This ensures we delete records in batches.
+ user.destroy_dependent_associations_in_batches(exclude: [:snippets])
+ user.nullify_dependent_associations_in_batches
+
+ # Destroy the namespace after destroying the user since certain methods may depend on the namespace existing
+ user_data = user.destroy
+ namespace.destroy
+
+ user_data
+ end
end
end
end
diff --git a/app/services/users/email_verification/base_service.rb b/app/services/users/email_verification/base_service.rb
new file mode 100644
index 00000000000..3337beec195
--- /dev/null
+++ b/app/services/users/email_verification/base_service.rb
@@ -0,0 +1,27 @@
+# frozen_string_literal: true
+
+module Users
+ module EmailVerification
+ class BaseService
+ VALID_ATTRS = %i[unlock_token confirmation_token].freeze
+
+ def initialize(attr:)
+ @attr = attr
+
+ validate_attr!
+ end
+
+ protected
+
+ attr_reader :attr, :token
+
+ def validate_attr!
+ raise ArgumentError, 'Invalid attribute' unless attr.in?(VALID_ATTRS)
+ end
+
+ def digest
+ Devise.token_generator.digest(User, attr, token)
+ end
+ end
+ end
+end
diff --git a/app/services/users/email_verification/generate_token_service.rb b/app/services/users/email_verification/generate_token_service.rb
new file mode 100644
index 00000000000..6f0237ce244
--- /dev/null
+++ b/app/services/users/email_verification/generate_token_service.rb
@@ -0,0 +1,21 @@
+# frozen_string_literal: true
+
+module Users
+ module EmailVerification
+ class GenerateTokenService < EmailVerification::BaseService
+ TOKEN_LENGTH = 6
+
+ def execute
+ @token = generate_token
+
+ [token, digest]
+ end
+
+ private
+
+ def generate_token
+ SecureRandom.random_number(10**TOKEN_LENGTH).to_s.rjust(TOKEN_LENGTH, '0')
+ end
+ end
+ end
+end
diff --git a/app/services/users/email_verification/validate_token_service.rb b/app/services/users/email_verification/validate_token_service.rb
new file mode 100644
index 00000000000..b1b34e94f49
--- /dev/null
+++ b/app/services/users/email_verification/validate_token_service.rb
@@ -0,0 +1,78 @@
+# frozen_string_literal: true
+
+module Users
+ module EmailVerification
+ class ValidateTokenService < EmailVerification::BaseService
+ include ActionView::Helpers::DateHelper
+
+ TOKEN_VALID_FOR_MINUTES = 60
+
+ def initialize(attr:, user:, token:)
+ super(attr: attr)
+
+ @user = user
+ @token = token
+ end
+
+ def execute
+ return failure(:rate_limited) if verification_rate_limited?
+ return failure(:invalid) unless valid?
+ return failure(:expired) if expired_token?
+
+ success
+ end
+
+ private
+
+ attr_reader :user
+
+ def verification_rate_limited?
+ Gitlab::ApplicationRateLimiter.throttled?(:email_verification, scope: user[attr])
+ end
+
+ def valid?
+ return false unless token.present?
+
+ Devise.secure_compare(user[attr], digest)
+ end
+
+ def expired_token?
+ generated_at = case attr
+ when :unlock_token then user.locked_at
+ when :confirmation_token then user.confirmation_sent_at
+ end
+
+ generated_at < TOKEN_VALID_FOR_MINUTES.minutes.ago
+ end
+
+ def success
+ { status: :success }
+ end
+
+ def failure(reason)
+ {
+ status: :failure,
+ reason: reason,
+ message: failure_message(reason)
+ }
+ end
+
+ def failure_message(reason)
+ case reason
+ when :rate_limited
+ format(s_("IdentityVerification|You've reached the maximum amount of tries. "\
+ 'Wait %{interval} or send a new code and try again.'), interval: email_verification_interval)
+ when :expired
+ s_('IdentityVerification|The code has expired. Send a new code and try again.')
+ when :invalid
+ s_('IdentityVerification|The code is incorrect. Enter it again, or send a new code.')
+ end
+ end
+
+ def email_verification_interval
+ interval_in_seconds = Gitlab::ApplicationRateLimiter.rate_limits[:email_verification][:interval]
+ distance_of_time_in_words(interval_in_seconds)
+ end
+ end
+ end
+end
diff --git a/app/services/users/migrate_records_to_ghost_user_in_batches_service.rb b/app/services/users/migrate_records_to_ghost_user_in_batches_service.rb
new file mode 100644
index 00000000000..7c4a5698ea9
--- /dev/null
+++ b/app/services/users/migrate_records_to_ghost_user_in_batches_service.rb
@@ -0,0 +1,26 @@
+# frozen_string_literal: true
+
+module Users
+ class MigrateRecordsToGhostUserInBatchesService
+ def initialize
+ @execution_tracker = Gitlab::Utils::ExecutionTracker.new
+ end
+
+ def execute
+ Users::GhostUserMigration.find_each do |user_to_migrate|
+ break if execution_tracker.over_limit?
+
+ service = Users::MigrateRecordsToGhostUserService.new(user_to_migrate.user,
+ user_to_migrate.initiator_user,
+ execution_tracker)
+ service.execute(hard_delete: user_to_migrate.hard_delete)
+ end
+ rescue Gitlab::Utils::ExecutionTracker::ExecutionTimeOutError
+ # no-op
+ end
+
+ private
+
+ attr_reader :execution_tracker
+ end
+end
diff --git a/app/services/users/migrate_records_to_ghost_user_service.rb b/app/services/users/migrate_records_to_ghost_user_service.rb
new file mode 100644
index 00000000000..2d92aaed7da
--- /dev/null
+++ b/app/services/users/migrate_records_to_ghost_user_service.rb
@@ -0,0 +1,111 @@
+# frozen_string_literal: true
+
+# When a user is destroyed, some of their associated records are
+# moved to a "Ghost User", to prevent these associated records from
+# being destroyed.
+#
+# For example, all the issues/MRs a user has created are _not_ destroyed
+# when the user is destroyed.
+module Users
+ class MigrateRecordsToGhostUserService
+ extend ActiveSupport::Concern
+
+ DestroyError = Class.new(StandardError)
+
+ attr_reader :ghost_user, :user, :initiator_user, :hard_delete
+
+ def initialize(user, initiator_user, execution_tracker)
+ @user = user
+ @initiator_user = initiator_user
+ @execution_tracker = execution_tracker
+ @ghost_user = User.ghost
+ end
+
+ def execute(hard_delete: false)
+ @hard_delete = hard_delete
+
+ migrate_records
+ post_migrate_records
+ end
+
+ private
+
+ attr_reader :execution_tracker
+
+ def migrate_records
+ return if hard_delete
+
+ migrate_issues
+ migrate_merge_requests
+ migrate_notes
+ migrate_abuse_reports
+ migrate_award_emoji
+ migrate_snippets
+ migrate_reviews
+ end
+
+ def post_migrate_records
+ delete_snippets
+
+ # Rails attempts to load all related records into memory before
+ # destroying: https://github.com/rails/rails/issues/22510
+ # This ensures we delete records in batches.
+ user.destroy_dependent_associations_in_batches(exclude: [:snippets])
+ user.nullify_dependent_associations_in_batches
+
+ # Destroy the namespace after destroying the user since certain methods may depend on the namespace existing
+ user_data = user.destroy
+ user.namespace.destroy
+
+ user_data
+ end
+
+ def delete_snippets
+ response = Snippets::BulkDestroyService.new(initiator_user, user.snippets).execute(skip_authorization: true)
+ raise DestroyError, response.message if response.error?
+ end
+
+ def migrate_issues
+ batched_migrate(Issue, :author_id)
+ batched_migrate(Issue, :last_edited_by_id)
+ end
+
+ def migrate_merge_requests
+ batched_migrate(MergeRequest, :author_id)
+ batched_migrate(MergeRequest, :merge_user_id)
+ end
+
+ def migrate_notes
+ batched_migrate(Note, :author_id)
+ end
+
+ def migrate_abuse_reports
+ user.reported_abuse_reports.update_all(reporter_id: ghost_user.id)
+ end
+
+ def migrate_award_emoji
+ user.award_emoji.update_all(user_id: ghost_user.id)
+ end
+
+ def migrate_snippets
+ snippets = user.snippets.only_project_snippets
+ snippets.update_all(author_id: ghost_user.id)
+ end
+
+ def migrate_reviews
+ batched_migrate(Review, :author_id)
+ end
+
+ # rubocop:disable CodeReuse/ActiveRecord
+ def batched_migrate(base_scope, column, batch_size: 50)
+ loop do
+ update_count = base_scope.where(column => user.id).limit(batch_size).update_all(column => ghost_user.id)
+ break if update_count == 0
+ raise Gitlab::Utils::ExecutionTracker::ExecutionTimeOutError if execution_tracker.over_limit?
+ end
+ end
+ # rubocop:enable CodeReuse/ActiveRecord
+ end
+end
+
+Users::MigrateRecordsToGhostUserService.prepend_mod_with('Users::MigrateRecordsToGhostUserService')