user.rb 40.3 KB
Newer Older
S
Steven Thonus 已提交
1 2
require 'carrierwave/orm/activerecord'

G
gitlabhq 已提交
3
class User < ActiveRecord::Base
4
  extend Gitlab::ConfigHelper
5 6

  include Gitlab::ConfigHelper
H
Hiroyuki Sato 已提交
7
  include Gitlab::SQL::Pattern
D
Douwe Maan 已提交
8
  include AfterCommitQueue
9
  include Avatarable
10 11
  include Referable
  include Sortable
12
  include CaseSensitivity
13
  include TokenAuthenticatable
14
  include IgnorableColumn
15
  include FeatureGate
16
  include CreatedAtFilterable
17
  include IgnorableColumn
18
  include BulkMemberAccessLoad
19
  include BlocksJsonSerialization
20

21 22
  DEFAULT_NOTIFICATION_LEVEL = :participating

23 24
  ignore_column :external_email
  ignore_column :email_provider
25
  ignore_column :authentication_token
26

27
  add_authentication_token_field :incoming_email_token
28
  add_authentication_token_field :rss_token
29

30
  default_value_for :admin, false
31
  default_value_for(:external) { Gitlab::CurrentSettings.user_default_external }
32
  default_value_for :can_create_group, gitlab_config.default_can_create_group
33 34
  default_value_for :can_create_team, false
  default_value_for :hide_no_ssh_key, false
35
  default_value_for :hide_no_password, false
36
  default_value_for :project_view, :files
37
  default_value_for :notified_of_own_activity, false
38
  default_value_for :preferred_language, I18n.default_locale
39
  default_value_for :theme_id, gitlab_config.default_theme
40

41
  attr_encrypted :otp_secret,
42
    key:       Gitlab::Application.secrets.otp_key_base,
43
    mode:      :per_attribute_iv_and_salt,
44
    insecure_mode: true,
45 46
    algorithm: 'aes-256-cbc'

47
  devise :two_factor_authenticatable,
48
         otp_secret_encryption_key: Gitlab::Application.secrets.otp_key_base
R
Robert Speicher 已提交
49

50
  devise :two_factor_backupable, otp_number_of_backup_codes: 10
51
  serialize :otp_backup_codes, JSON # rubocop:disable Cop/ActiveRecordSerialize
R
Robert Speicher 已提交
52

53
  devise :lockable, :recoverable, :rememberable, :trackable,
54 55 56 57
         :validatable, :omniauthable, :confirmable, :registerable

  BLOCKED_MESSAGE = "Your account has been blocked. Please contact your GitLab " \
                    "administrator if you think this is an error.".freeze
G
gitlabhq 已提交
58

59 60
  # Override Devise::Models::Trackable#update_tracked_fields!
  # to limit database writes to at most once every hour
61
  def update_tracked_fields!(request)
62 63
    update_tracked_fields(request)

64 65 66
    lease = Gitlab::ExclusiveLease.new("user_update_tracked_fields:#{id}", timeout: 1.hour.to_i)
    return unless lease.try_obtain

J
James Lopez 已提交
67
    Users::UpdateService.new(self, user: self).execute(validate: false)
68 69
  end

70
  attr_accessor :force_random_password
G
gitlabhq 已提交
71

72 73 74
  # Virtual attribute for authenticating by either username or email
  attr_accessor :login

75 76 77 78
  #
  # Relations
  #

79
  # Namespace for personal projects
80
  has_one :namespace, -> { where(type: nil) }, dependent: :destroy, foreign_key: :owner_id, inverse_of: :owner, autosave: true # rubocop:disable Cop/ActiveRecordDependent
81 82

  # Profile
83 84 85
  has_many :keys, -> do
    type = Key.arel_table[:type]
    where(type.not_eq('DeployKey').or(type.eq(nil)))
86 87
  end, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
  has_many :deploy_keys, -> { where(type: 'DeployKey') }, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
88
  has_many :gpg_keys
89

90 91 92 93 94
  has_many :emails, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
  has_many :personal_access_tokens, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
  has_many :identities, dependent: :destroy, autosave: true # rubocop:disable Cop/ActiveRecordDependent
  has_many :u2f_registrations, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
  has_many :chat_names, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
95
  has_one :user_synced_attributes_metadata, autosave: true
96 97

  # Groups
98 99
  has_many :members
  has_many :group_members, -> { where(requested_at: nil) }, source: 'GroupMember'
100
  has_many :groups, through: :group_members
101 102
  has_many :owned_groups, -> { where members: { access_level: Gitlab::Access::OWNER } }, through: :group_members, source: :group
  has_many :masters_groups, -> { where members: { access_level: Gitlab::Access::MASTER } }, through: :group_members, source: :group
103

104
  # Projects
105 106
  has_many :groups_projects,          through: :groups, source: :projects
  has_many :personal_projects,        through: :namespace, source: :projects
107
  has_many :project_members, -> { where(requested_at: nil) }
108
  has_many :projects,                 through: :project_members
109
  has_many :created_projects,         foreign_key: :creator_id, class_name: 'Project'
110
  has_many :users_star_projects, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
C
Ciro Santilli 已提交
111
  has_many :starred_projects, through: :users_star_projects, source: :project
112
  has_many :project_authorizations
113
  has_many :authorized_projects, through: :project_authorizations, source: :project
114

115 116 117 118 119 120
  has_many :snippets,                 dependent: :destroy, foreign_key: :author_id # rubocop:disable Cop/ActiveRecordDependent
  has_many :notes,                    dependent: :destroy, foreign_key: :author_id # rubocop:disable Cop/ActiveRecordDependent
  has_many :issues,                   dependent: :destroy, foreign_key: :author_id # rubocop:disable Cop/ActiveRecordDependent
  has_many :merge_requests,           dependent: :destroy, foreign_key: :author_id # rubocop:disable Cop/ActiveRecordDependent
  has_many :events,                   dependent: :destroy, foreign_key: :author_id # rubocop:disable Cop/ActiveRecordDependent
  has_many :subscriptions,            dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
121
  has_many :recent_events, -> { order "id DESC" }, foreign_key: :author_id,   class_name: "Event"
122 123 124 125 126 127
  has_many :oauth_applications, class_name: 'Doorkeeper::Application', as: :owner, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
  has_one  :abuse_report,             dependent: :destroy, foreign_key: :user_id # rubocop:disable Cop/ActiveRecordDependent
  has_many :reported_abuse_reports,   dependent: :destroy, foreign_key: :reporter_id, class_name: "AbuseReport" # rubocop:disable Cop/ActiveRecordDependent
  has_many :spam_logs,                dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
  has_many :builds,                   dependent: :nullify, class_name: 'Ci::Build' # rubocop:disable Cop/ActiveRecordDependent
  has_many :pipelines,                dependent: :nullify, class_name: 'Ci::Pipeline' # rubocop:disable Cop/ActiveRecordDependent
128
  has_many :todos
129 130 131
  has_many :notification_settings,    dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
  has_many :award_emoji,              dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
  has_many :triggers,                 dependent: :destroy, class_name: 'Ci::Trigger', foreign_key: :owner_id # rubocop:disable Cop/ActiveRecordDependent
132

133 134
  has_many :issue_assignees
  has_many :assigned_issues, class_name: "Issue", through: :issue_assignees, source: :issue
135
  has_many :assigned_merge_requests,  dependent: :nullify, foreign_key: :assignee_id, class_name: "MergeRequest" # rubocop:disable Cop/ActiveRecordDependent
136

137
  has_many :custom_attributes, class_name: 'UserCustomAttribute'
M
Matija Čupić 已提交
138
  has_many :callouts, class_name: 'UserCallout'
139
  has_many :uploads, as: :model, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
140

141 142 143
  #
  # Validations
  #
144
  # Note: devise :validatable above adds validations for :email and :password
C
Cyril 已提交
145
  validates :name, presence: true
D
Douwe Maan 已提交
146
  validates :email, confirmation: true
147 148
  validates :notification_email, presence: true
  validates :notification_email, email: true, if: ->(user) { user.notification_email != user.email }
149
  validates :public_email, presence: true, uniqueness: true, email: true, allow_blank: true
150
  validates :bio, length: { maximum: 255 }, allow_blank: true
151 152 153
  validates :projects_limit,
    presence: true,
    numericality: { greater_than_or_equal_to: 0, less_than_or_equal_to: Gitlab::Database::MAX_INT_VALUE }
154
  validates :username,
155
    user_path: true,
156
    presence: true
157

158
  validates :namespace, presence: true
159 160
  validate :namespace_move_dir_allowed, if: :username_changed?

T
Tiago Botelho 已提交
161 162 163
  validate :unique_email, if: :email_changed?
  validate :owns_notification_email, if: :notification_email_changed?
  validate :owns_public_email, if: :public_email_changed?
164
  validate :signup_domain_valid?, on: :create, if: ->(user) { !user.created_by_id }
165

166
  before_validation :sanitize_attrs
T
Tiago Botelho 已提交
167 168
  before_validation :set_notification_email, if: :email_changed?
  before_validation :set_public_email, if: :public_email_changed?
D
Douwe Maan 已提交
169
  before_save :ensure_incoming_email_token
170
  before_save :ensure_user_rights_and_limits, if: ->(user) { user.new_record? || user.external_changed? }
171
  before_save :skip_reconfirmation!, if: ->(user) { user.email_changed? && user.read_only_attribute?(:email) }
A
Alexandra 已提交
172
  before_save :check_for_verified_email, if: ->(user) { user.email_changed? && !user.new_record? }
173
  before_validation :ensure_namespace_correct
174
  after_validation :set_username_errors
175
  after_update :username_changed_hook, if: :username_changed?
176
  after_destroy :post_destroy_hook
Y
Yorick Peterse 已提交
177
  after_destroy :remove_key_cache
178
  after_commit :update_emails_with_primary_email, on: :update, if: -> { previous_changes.key?('email') }
179
  after_commit :update_invalid_gpg_signatures, on: :update, if: -> { previous_changes.key?('email') }
180

181
  after_initialize :set_projects_limit
D
Dmitriy Zaporozhets 已提交
182

183
  # User's Layout preference
184
  enum layout: [:fixed, :fluid]
185

186 187
  # User's Dashboard preference
  # Note: When adding an option, it MUST go on the end of the array.
188
  enum dashboard: [:projects, :stars, :project_activity, :starred_project_activity, :groups, :todos]
189

190
  # User's Project preference
191 192 193
  # Note: When adding an option, it MUST go on the end of the array.
  enum project_view: [:readme, :activity, :files]

194
  delegate :path, to: :namespace, allow_nil: true, prefix: true
195

196 197 198
  state_machine :state, initial: :active do
    event :block do
      transition active: :blocked
199
      transition ldap_blocked: :blocked
200 201
    end

202 203 204 205
    event :ldap_block do
      transition active: :ldap_blocked
    end

206 207
    event :activate do
      transition blocked: :active
208
      transition ldap_blocked: :active
209
    end
210 211 212 213 214

    state :blocked, :ldap_blocked do
      def blocked?
        true
      end
215 216 217 218 219 220

      def active_for_authentication?
        false
      end

      def inactive_message
221
        BLOCKED_MESSAGE
222
      end
223
    end
224 225
  end

A
Andrey Kumanyaev 已提交
226
  # Scopes
227
  scope :admins, -> { where(admin: true) }
228
  scope :blocked, -> { with_states(:blocked, :ldap_blocked) }
229
  scope :external, -> { where(external: true) }
J
James Lopez 已提交
230
  scope :active, -> { with_state(:active).non_internal }
B
Ben Bodenmiller 已提交
231
  scope :without_projects, -> { where('id NOT IN (SELECT DISTINCT(user_id) FROM members WHERE user_id IS NOT NULL AND requested_at IS NULL)') }
V
Valery Sizov 已提交
232
  scope :todo_authors, ->(user_id, state) { where(id: Todo.where(user_id: user_id, state: state).select(:author_id)) }
233 234
  scope :order_recent_sign_in, -> { reorder(Gitlab::Database.nulls_last_order('current_sign_in_at', 'DESC')) }
  scope :order_oldest_sign_in, -> { reorder(Gitlab::Database.nulls_last_order('current_sign_in_at', 'ASC')) }
235 236

  def self.with_two_factor
237 238
    joins("LEFT OUTER JOIN u2f_registrations AS u2f ON u2f.user_id = users.id")
      .where("u2f.id IS NOT NULL OR otp_required_for_login = ?", true).distinct(arel_table[:id])
239 240 241
  end

  def self.without_two_factor
242 243
    joins("LEFT OUTER JOIN u2f_registrations AS u2f ON u2f.user_id = users.id")
      .where("u2f.id IS NULL AND otp_required_for_login = ?", false)
244
  end
A
Andrey Kumanyaev 已提交
245

246 247 248
  #
  # Class methods
  #
A
Andrey Kumanyaev 已提交
249
  class << self
250
    # Devise method overridden to allow sign in with email or username
251 252 253
    def find_for_database_authentication(warden_conditions)
      conditions = warden_conditions.dup
      if login = conditions.delete(:login)
G
Gabriel Mazetto 已提交
254
        where(conditions).find_by("lower(username) = :value OR lower(email) = :value", value: login.downcase)
255
      else
G
Gabriel Mazetto 已提交
256
        find_by(conditions)
257 258
      end
    end
259

V
Valery Sizov 已提交
260
    def sort(method)
261 262 263
      order_method = method || 'id_desc'

      case order_method.to_s
264 265
      when 'recent_sign_in' then order_recent_sign_in
      when 'oldest_sign_in' then order_oldest_sign_in
266
      else
267
        order_by(order_method)
V
Valery Sizov 已提交
268 269 270
      end
    end

271
    def for_github_id(id)
272
      joins(:identities).merge(Identity.with_extern_uid(:github, id))
273 274
    end

275 276
    # Find a User by their primary email or any associated secondary email
    def find_by_any_email(email)
Y
Yorick Peterse 已提交
277 278 279 280 281 282 283 284 285 286
      by_any_email(email).take
    end

    # Returns a relation containing all the users for the given Email address
    def by_any_email(email)
      users = where(email: email)
      emails = joins(:emails).where(emails: { email: email })
      union = Gitlab::SQL::Union.new([users, emails])

      from("(#{union.to_sql}) #{table_name}")
287
    end
288

289
    def filter(filter_name)
A
Andrey Kumanyaev 已提交
290
      case filter_name
291
      when 'admins'
292
        admins
293
      when 'blocked'
294
        blocked
295
      when 'two_factor_disabled'
296
        without_two_factor
297
      when 'two_factor_enabled'
298
        with_two_factor
299
      when 'wop'
300
        without_projects
301
      when 'external'
302
        external
A
Andrey Kumanyaev 已提交
303
      else
304
        active
A
Andrey Kumanyaev 已提交
305
      end
306 307
    end

308 309 310 311 312 313 314
    # Searches users matching the given query.
    #
    # This method uses ILIKE on PostgreSQL and LIKE on MySQL.
    #
    # query - The search query as a String
    #
    # Returns an ActiveRecord::Relation.
315
    def search(query)
316 317
      return none if query.blank?

318 319
      query = query.downcase

320 321 322 323 324 325 326 327 328
      order = <<~SQL
        CASE
          WHEN users.name = %{query} THEN 0
          WHEN users.username = %{query} THEN 1
          WHEN users.email = %{query} THEN 2
          ELSE 3
        END
      SQL

329
      where(
330 331 332
        fuzzy_arel_match(:name, query)
          .or(fuzzy_arel_match(:username, query))
          .or(arel_table[:email].eq(query))
333
      ).reorder(order % { query: ActiveRecord::Base.connection.quote(query) }, :name)
A
Andrey Kumanyaev 已提交
334
    end
335

336 337 338 339 340
    # searches user by given pattern
    # it compares name, email, username fields and user's secondary emails with given pattern
    # This method uses ILIKE on PostgreSQL and LIKE on MySQL.

    def search_with_secondary_emails(query)
341 342
      return none if query.blank?

343 344
      query = query.downcase

345
      email_table = Email.arel_table
346 347
      matched_by_emails_user_ids = email_table
        .project(email_table[:user_id])
348
        .where(email_table[:email].eq(query))
349 350

      where(
351 352
        fuzzy_arel_match(:name, query)
          .or(fuzzy_arel_match(:username, query))
353
          .or(arel_table[:email].eq(query))
354
          .or(arel_table[:id].in(matched_by_emails_user_ids))
355 356 357
      )
    end

358
    def by_login(login)
359 360 361 362 363 364 365
      return nil unless login

      if login.include?('@'.freeze)
        unscoped.iwhere(email: login).take
      else
        unscoped.iwhere(username: login).take
      end
366 367
    end

368 369 370 371
    def find_by_username(username)
      iwhere(username: username).take
    end

R
Robert Speicher 已提交
372
    def find_by_username!(username)
373
      iwhere(username: username).take!
R
Robert Speicher 已提交
374 375
    end

T
Timothy Andrew 已提交
376
    def find_by_personal_access_token(token_string)
377 378
      return unless token_string

379
      PersonalAccessTokensFinder.new(state: 'active').find_by(token: token_string)&.user
T
Timothy Andrew 已提交
380 381
    end

Y
Yorick Peterse 已提交
382 383
    # Returns a user for the given SSH key.
    def find_by_ssh_key_id(key_id)
384
      Key.find_by(id: key_id)&.user
Y
Yorick Peterse 已提交
385 386
    end

387
    def find_by_full_path(path, follow_redirects: false)
388 389
      namespace = Namespace.for_user.find_by_full_path(path, follow_redirects: follow_redirects)
      namespace&.owner
390 391
    end

392 393 394
    def reference_prefix
      '@'
    end
395 396 397 398

    # Pattern used to extract `@user` user references from text
    def reference_pattern
      %r{
399
        (?<!\w)
400
        #{Regexp.escape(reference_prefix)}
401
        (?<user>#{Gitlab::PathRegex::FULL_NAMESPACE_FORMAT_REGEX})
402 403
      }x
    end
404 405 406 407

    # Return (create if necessary) the ghost user. The ghost user
    # owns records previously belonging to deleted users.
    def ghost
408 409
      email = 'ghost%s@example.com'
      unique_internal(where(ghost: true), 'ghost', email) do |u|
410 411
        u.bio = 'This is a "Ghost User", created to hold all issues authored by users that have since been deleted. This user cannot be removed.'
        u.name = 'Ghost User'
412
        u.notification_email = email
413
      end
414
    end
V
vsizov 已提交
415
  end
R
randx 已提交
416

M
Michael Kozono 已提交
417 418 419 420
  def full_path
    username
  end

421 422 423 424
  def self.internal_attributes
    [:ghost]
  end

425
  def internal?
426 427 428 429 430 431 432 433
    self.class.internal_attributes.any? { |a| self[a] }
  end

  def self.internal
    where(Hash[internal_attributes.zip([true] * internal_attributes.size)])
  end

  def self.non_internal
434
    where(Hash[internal_attributes.zip([[false, nil]] * internal_attributes.size)])
435 436
  end

437 438 439
  #
  # Instance methods
  #
440 441 442 443 444

  def to_param
    username
  end

J
Jarka Kadlecova 已提交
445
  def to_reference(_from = nil, target_project: nil, full: nil)
446 447 448
    "#{self.class.reference_prefix}#{username}"
  end

449 450
  def skip_confirmation=(bool)
    skip_confirmation! if bool
D
Daniel Juarez 已提交
451 452 453 454
  end

  def skip_reconfirmation=(bool)
    skip_reconfirmation! if bool
R
randx 已提交
455
  end
456

457
  def generate_reset_token
M
Marin Jankovski 已提交
458
    @reset_token, enc = Devise.token_generator.generate(self.class, :reset_password_token)
459 460 461 462

    self.reset_password_token   = enc
    self.reset_password_sent_at = Time.now.utc

M
Marin Jankovski 已提交
463
    @reset_token
464 465
  end

466 467 468 469
  def recently_sent_password_reset?
    reset_password_sent_at.present? && reset_password_sent_at >= 1.minute.ago
  end

T
Toon Claes 已提交
470 471 472 473 474 475 476 477
  def remember_me!
    super if ::Gitlab::Database.read_write?
  end

  def forget_me!
    super if ::Gitlab::Database.read_write?
  end

R
Robert Speicher 已提交
478
  def disable_two_factor!
479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496
    transaction do
      update_attributes(
        otp_required_for_login:      false,
        encrypted_otp_secret:        nil,
        encrypted_otp_secret_iv:     nil,
        encrypted_otp_secret_salt:   nil,
        otp_grace_period_started_at: nil,
        otp_backup_codes:            nil
      )
      self.u2f_registrations.destroy_all
    end
  end

  def two_factor_enabled?
    two_factor_otp_enabled? || two_factor_u2f_enabled?
  end

  def two_factor_otp_enabled?
497
    otp_required_for_login?
498 499 500
  end

  def two_factor_u2f_enabled?
501 502 503 504 505
    if u2f_registrations.loaded?
      u2f_registrations.any?
    else
      u2f_registrations.exists?
    end
R
Robert Speicher 已提交
506 507
  end

508 509 510 511 512 513
  def namespace_move_dir_allowed
    if namespace&.any_project_has_container_registry_tags?
      errors.add(:username, 'cannot be changed if a personal project has container registry tags.')
    end
  end

514
  def unique_email
515 516
    if !emails.exists?(email: email) && Email.exists?(email: email)
      errors.add(:email, 'has already been taken')
517
    end
518 519
  end

520
  def owns_notification_email
521
    return if temp_oauth_email?
522

523
    errors.add(:notification_email, "is not an email you own") unless all_emails.include?(notification_email)
524 525
  end

526
  def owns_public_email
527
    return if public_email.blank?
528

529
    errors.add(:public_email, "is not an email you own") unless all_emails.include?(public_email)
530 531
  end

A
Alexandra 已提交
532 533 534 535 536
  # see if the new email is already a verified secondary email
  def check_for_verified_email
    skip_reconfirmation! if emails.confirmed.where(email: self.email).any?
  end

537
  # Note: the use of the Emails services will cause `saves` on the user object, running
538
  # through the callbacks again and can have side effects, such as the `previous_changes`
539 540 541
  # hash and `_was` variables getting munged.
  # By using an `after_commit` instead of `after_update`, we avoid the recursive callback
  # scenario, though it then requires us to use the `previous_changes` hash
542
  def update_emails_with_primary_email
543
    previous_email = previous_changes[:email][0]  # grab this before the DestroyService is called
544
    primary_email_record = emails.find_by(email: email)
545
    Emails::DestroyService.new(self, user: self).execute(primary_email_record) if primary_email_record
546

547 548
    # the original primary email was confirmed, and we want that to carry over.  We don't
    # have access to the original confirmation values at this point, so just set confirmed_at
549
    Emails::CreateService.new(self, user: self, email: previous_email).execute(confirmed_at: confirmed_at)
550 551
  end

552
  def update_invalid_gpg_signatures
553
    gpg_keys.each(&:update_invalid_gpg_signatures)
554 555
  end

556 557
  # Returns the groups a user has access to
  def authorized_groups
558 559
    union = Gitlab::SQL::Union
      .new([groups.select(:id), authorized_projects.select(:namespace_id)])
560

561
    Group.where("namespaces.id IN (#{union.to_sql})") # rubocop:disable GitlabSecurity/SqlInjection
562 563
  end

564 565
  # Returns a relation of groups the user has access to, including their parent
  # and child groups (recursively).
566
  def all_expanded_groups
567
    Gitlab::GroupHierarchy.new(groups).all_groups
568 569 570 571 572 573
  end

  def expanded_groups_requiring_two_factor_authentication
    all_expanded_groups.where(require_two_factor_authentication: true)
  end

574
  def refresh_authorized_projects
575 576 577 578
    Users::RefreshAuthorizedProjectsService.new(self).execute
  end

  def remove_project_authorizations(project_ids)
579
    project_authorizations.where(project_id: project_ids).delete_all
580 581
  end

582
  def authorized_projects(min_access_level = nil)
583 584
    # We're overriding an association, so explicitly call super with no
    # arguments or it would be passed as `force_reload` to the association
585
    projects = super()
586 587

    if min_access_level
588 589
      projects = projects
        .where('project_authorizations.access_level >= ?', min_access_level)
590
    end
591 592 593 594 595 596

    projects
  end

  def authorized_project?(project, min_access_level = nil)
    authorized_projects(min_access_level).exists?({ id: project.id })
597 598
  end

599 600 601 602 603 604 605 606 607 608
  # Returns the projects this user has reporter (or greater) access to, limited
  # to at most the given projects.
  #
  # This method is useful when you have a list of projects and want to
  # efficiently check to which of these projects the user has at least reporter
  # access.
  def projects_with_reporter_access_limited_to(projects)
    authorized_projects(Gitlab::Access::REPORTER).where(id: projects)
  end

609
  def owned_projects
610
    @owned_projects ||=
611 612
      Project.where('namespace_id IN (?) OR namespace_id = ?',
                    owned_groups.select(:id), namespace.id).joins(:namespace)
613 614
  end

615 616 617 618
  # Returns projects which user can admin issues on (for example to move an issue to that project).
  #
  # This logic is duplicated from `Ability#project_abilities` into a SQL form.
  def projects_where_can_admin_issues
F
Felipe Artur 已提交
619
    authorized_projects(Gitlab::Access::REPORTER).non_archived.with_issues_enabled
620 621
  end

D
Dmitriy Zaporozhets 已提交
622
  def require_ssh_key?
Y
Yorick Peterse 已提交
623 624 625
    count = Users::KeysCountService.new(self).count

    count.zero? && Gitlab::ProtocolAccess.allowed?('ssh')
D
Dmitriy Zaporozhets 已提交
626 627
  end

628 629 630 631 632 633
  def require_password_creation_for_web?
    allow_password_authentication_for_web? && password_automatically_set?
  end

  def require_password_creation_for_git?
    allow_password_authentication_for_git? && password_automatically_set?
634 635
  end

636
  def require_personal_access_token_creation_for_git_auth?
637
    return false if allow_password_authentication_for_git? || ldap_user?
638 639

    PersonalAccessTokensFinder.new(user: self, impersonation: false, state: 'active').execute.none?
640 641
  end

642 643 644 645
  def require_extra_setup_for_git_auth?
    require_password_creation_for_git? || require_personal_access_token_creation_for_git_auth?
  end

646
  def allow_password_authentication?
647 648 649 650
    allow_password_authentication_for_web? || allow_password_authentication_for_git?
  end

  def allow_password_authentication_for_web?
651
    Gitlab::CurrentSettings.password_authentication_enabled_for_web? && !ldap_user?
652 653 654
  end

  def allow_password_authentication_for_git?
655
    Gitlab::CurrentSettings.password_authentication_enabled_for_git? && !ldap_user?
656 657
  end

658
  def can_change_username?
659
    gitlab_config.username_changing_enabled
660 661
  end

D
Dmitriy Zaporozhets 已提交
662
  def can_create_project?
663
    projects_limit_left > 0
D
Dmitriy Zaporozhets 已提交
664 665 666
  end

  def can_create_group?
667
    can?(:create_group)
D
Dmitriy Zaporozhets 已提交
668 669
  end

670 671 672 673
  def can_select_namespace?
    several_namespaces? || admin
  end

674
  def can?(action, subject = :global)
H
http://jneen.net/ 已提交
675
    Ability.allowed?(self, action, subject)
D
Dmitriy Zaporozhets 已提交
676 677
  end

678 679 680 681
  def confirm_deletion_with_password?
    !password_automatically_set? && allow_password_authentication?
  end

D
Dmitriy Zaporozhets 已提交
682 683 684 685
  def first_name
    name.split.first unless name.blank?
  end

686
  def projects_limit_left
687 688 689 690 691
    projects_limit - personal_projects_count
  end

  def personal_projects_count
    @personal_projects_count ||= personal_projects.count
692 693
  end

694 695
  def recent_push(project = nil)
    service = Users::LastPushEventService.new(self)
D
Dmitriy Zaporozhets 已提交
696

697 698 699 700
    if project
      service.last_event_for_project(project)
    else
      service.last_event_for_user
701
    end
D
Dmitriy Zaporozhets 已提交
702 703 704
  end

  def several_namespaces?
705
    owned_groups.any? || masters_groups.any?
D
Dmitriy Zaporozhets 已提交
706 707 708 709 710
  end

  def namespace_id
    namespace.try :id
  end
711

712 713 714
  def name_with_username
    "#{name} (#{username})"
  end
D
Dmitriy Zaporozhets 已提交
715

716
  def already_forked?(project)
717 718 719
    !!fork_of(project)
  end

720
  def fork_of(project)
721
    namespace.find_fork_of(project)
722
  end
723 724

  def ldap_user?
725
    if identities.loaded?
726
      identities.find { |identity| Gitlab::OAuth::Provider.ldap_provider?(identity.provider) && !identity.extern_uid.nil? }
727 728 729
    else
      identities.exists?(["provider LIKE ? AND extern_uid IS NOT NULL", "ldap%"])
    end
730 731 732 733
  end

  def ldap_identity
    @ldap_identity ||= identities.find_by(["provider LIKE ?", "ldap%"])
734
  end
735

736
  def project_deploy_keys
737
    DeployKey.unscoped.in_projects(authorized_projects.pluck(:id)).distinct(:id)
738 739
  end

740
  def accessible_deploy_keys
741 742 743 744 745
    @accessible_deploy_keys ||= begin
      key_ids = project_deploy_keys.pluck(:id)
      key_ids.push(*DeployKey.are_public.pluck(:id))
      DeployKey.where(id: key_ids)
    end
746
  end
747 748

  def created_by
S
skv 已提交
749
    User.find_by(id: created_by_id) if created_by_id
750
  end
751 752

  def sanitize_attrs
753 754 755
    %i[skype linkedin twitter].each do |attr|
      value = self[attr]
      self[attr] = Sanitize.clean(value) if value.present?
756 757
    end
  end
758

759
  def set_notification_email
760 761
    if notification_email.blank? || !all_emails.include?(notification_email)
      self.notification_email = email
762 763 764
    end
  end

765
  def set_public_email
766
    if public_email.blank? || !all_emails.include?(public_email)
767
      self.public_email = ''
768 769 770
    end
  end

771
  def update_secondary_emails!
772 773 774
    set_notification_email
    set_public_email
    save if notification_email_changed? || public_email_changed?
775 776
  end

777
  def set_projects_limit
778 779 780
    # `User.select(:id)` raises
    # `ActiveModel::MissingAttributeError: missing attribute: projects_limit`
    # without this safeguard!
781
    return unless has_attribute?(:projects_limit) && projects_limit.nil?
782

783
    self.projects_limit = Gitlab::CurrentSettings.default_projects_limit
784 785
  end

786
  def requires_ldap_check?
787 788 789
    if !Gitlab.config.ldap.enabled
      false
    elsif ldap_user?
790 791 792 793 794 795
      !last_credential_check_at || (last_credential_check_at + 1.hour) < Time.now
    else
      false
    end
  end

J
Jacob Vosmaer 已提交
796 797 798 799 800 801 802
  def try_obtain_ldap_lease
    # After obtaining this lease LDAP checks will be blocked for 600 seconds
    # (10 minutes) for this user.
    lease = Gitlab::ExclusiveLease.new("user_ldap_check:#{id}", timeout: 600)
    lease.try_obtain
  end

803 804 805 806 807
  def solo_owned_groups
    @solo_owned_groups ||= owned_groups.select do |group|
      group.owners == [self]
    end
  end
808 809

  def with_defaults
810
    User.defaults.each do |k, v|
811
      public_send("#{k}=", v) # rubocop:disable GitlabSecurity/PublicSend
812
    end
813 814

    self
815
  end
816

817 818 819 820
  def can_leave_project?(project)
    project.namespace != namespace &&
      project.project_member(self)
  end
821

J
Jerome Dalbert 已提交
822
  def full_website_url
823
    return "http://#{website_url}" if website_url !~ %r{\Ahttps?://}
J
Jerome Dalbert 已提交
824 825 826 827 828

    website_url
  end

  def short_website_url
829
    website_url.sub(%r{\Ahttps?://}, '')
J
Jerome Dalbert 已提交
830
  end
G
GitLab 已提交
831

832
  def all_ssh_keys
833
    keys.map(&:publishable_key)
834
  end
835 836

  def temp_oauth_email?
837
    email.start_with?('temp-email-for-oauth')
838 839
  end

840
  def avatar_url(size: nil, scale: 2, **args)
841
    GravatarService.new.execute(email, size, scale, username: username)
842
  end
D
Dmitriy Zaporozhets 已提交
843

844 845 846 847
  def primary_email_verified?
    confirmed? && !temp_oauth_email?
  end

848
  def all_emails
849
    all_emails = []
850 851
    all_emails << email unless temp_oauth_email?
    all_emails.concat(emails.map(&:email))
852
    all_emails
853 854
  end

855
  def verified_emails
856
    verified_emails = []
857
    verified_emails << email if primary_email_verified?
858
    verified_emails.concat(emails.confirmed.pluck(:email))
859 860 861
    verified_emails
  end

862
  def verified_email?(check_email)
863
    downcased = check_email.downcase
A
Alexandra 已提交
864
    email == downcased ? primary_email_verified? : emails.confirmed.where(email: downcased).exists?
865 866
  end

K
Kirill Zaitsev 已提交
867 868 869 870
  def hook_attrs
    {
      name: name,
      username: username,
871
      avatar_url: avatar_url(only_path: false)
K
Kirill Zaitsev 已提交
872 873 874
    }
  end

D
Dmitriy Zaporozhets 已提交
875
  def ensure_namespace_correct
876 877 878 879
    if namespace
      namespace.path = namespace.name = username if username_changed?
    else
      build_namespace(path: username, name: username)
D
Dmitriy Zaporozhets 已提交
880 881 882
    end
  end

883 884 885 886 887
  def set_username_errors
    namespace_path_errors = self.errors.delete(:"namespace.path")
    self.errors[:username].concat(namespace_path_errors) if namespace_path_errors
  end

888 889 890 891
  def username_changed_hook
    system_hook_service.execute_hooks_for(self, :rename)
  end

D
Dmitriy Zaporozhets 已提交
892
  def post_destroy_hook
893
    log_info("User \"#{name}\" (#{email})  was removed")
D
Douwe Maan 已提交
894

D
Dmitriy Zaporozhets 已提交
895 896 897
    system_hook_service.execute_hooks_for(self, :destroy)
  end

Y
Yorick Peterse 已提交
898 899 900 901
  def remove_key_cache
    Users::KeysCountService.new(self).delete_cache
  end

N
Nick Thomas 已提交
902 903 904 905 906
  def delete_async(deleted_by:, params: {})
    block if params[:hard_delete]
    DeleteUserWorker.perform_async(deleted_by.id, id, params)
  end

D
Dmitriy Zaporozhets 已提交
907
  def notification_service
D
Dmitriy Zaporozhets 已提交
908 909 910
    NotificationService.new
  end

911
  def log_info(message)
D
Dmitriy Zaporozhets 已提交
912 913 914 915 916 917
    Gitlab::AppLogger.info message
  end

  def system_hook_service
    SystemHooksService.new
  end
C
Ciro Santilli 已提交
918 919

  def starred?(project)
V
Valery Sizov 已提交
920
    starred_projects.exists?(project.id)
C
Ciro Santilli 已提交
921 922 923
  end

  def toggle_star(project)
924
    UsersStarProject.transaction do
925 926
      user_star_project = users_star_projects
          .where(project: project, user: self).lock(true).first
927 928 929 930 931 932

      if user_star_project
        user_star_project.destroy
      else
        UsersStarProject.create!(project: project, user: self)
      end
C
Ciro Santilli 已提交
933 934
    end
  end
935 936

  def manageable_namespaces
937 938 939 940 941 942 943 944 945 946
    @manageable_namespaces ||= [namespace] + manageable_groups
  end

  def manageable_groups
    union = Gitlab::SQL::Union.new([owned_groups.select(:id),
                                    masters_groups.select(:id)])
    arel_union = Arel::Nodes::SqlLiteral.new(union.to_sql)
    owned_and_master_groups = Group.where(Group.arel_table[:id].in(arel_union))

    Gitlab::GroupHierarchy.new(owned_and_master_groups).base_and_descendants
947
  end
D
Dmitriy Zaporozhets 已提交
948

949 950 951 952 953 954
  def namespaces
    namespace_ids = groups.pluck(:id)
    namespace_ids.push(namespace.id)
    Namespace.where(id: namespace_ids)
  end

D
Dmitriy Zaporozhets 已提交
955
  def oauth_authorized_tokens
956
    Doorkeeper::AccessToken.where(resource_owner_id: id, revoked_at: nil)
D
Dmitriy Zaporozhets 已提交
957
  end
958

959 960 961 962 963 964 965 966 967
  # Returns the projects a user contributed to in the last year.
  #
  # This method relies on a subquery as this performs significantly better
  # compared to a JOIN when coupled with, for example,
  # `Project.visible_to_user`. That is, consider the following code:
  #
  #     some_user.contributed_projects.visible_to_user(other_user)
  #
  # If this method were to use a JOIN the resulting query would take roughly 200
968
  # ms on a database with a similar size to GitLab.com's database. On the other
969 970
  # hand, using a subquery means we can get the exact same data in about 40 ms.
  def contributed_projects
971 972 973 974 975
    events = Event.select(:project_id)
      .contributions.where(author_id: self)
      .where("created_at > ?", Time.now - 1.year)
      .uniq
      .reorder(nil)
976 977

    Project.where(id: events)
978
  end
979

980 981 982
  def can_be_removed?
    !solo_owned_groups.present?
  end
983 984

  def ci_authorized_runners
K
Kamil Trzcinski 已提交
985
    @ci_authorized_runners ||= begin
986
      runner_ids = Ci::RunnerProject
987
        .where("ci_runner_projects.project_id IN (#{ci_projects_union.to_sql})") # rubocop:disable GitlabSecurity/SqlInjection
988
        .select(:runner_id)
K
Kamil Trzcinski 已提交
989 990
      Ci::Runner.specific.where(id: runner_ids)
    end
991
  end
992

993
  def notification_settings_for(source)
994 995 996 997 998
    if notification_settings.loaded?
      notification_settings.find { |notification| notification.source == source }
    else
      notification_settings.find_or_initialize_by(source: source)
    end
999 1000
  end

1001 1002 1003
  # Lazy load global notification setting
  # Initializes User setting with Participating level if setting not persisted
  def global_notification_setting
1004 1005 1006 1007 1008 1009
    return @global_notification_setting if defined?(@global_notification_setting)

    @global_notification_setting = notification_settings.find_or_initialize_by(source: nil)
    @global_notification_setting.update_attributes(level: NotificationSetting.levels[DEFAULT_NOTIFICATION_LEVEL]) unless @global_notification_setting.persisted?

    @global_notification_setting
1010 1011
  end

1012
  def assigned_open_merge_requests_count(force: false)
1013
    Rails.cache.fetch(['users', id, 'assigned_open_merge_requests_count'], force: force, expires_in: 20.minutes) do
1014
      MergeRequestsFinder.new(self, assignee_id: self.id, state: 'opened').execute.count
1015 1016 1017
    end
  end

J
Josh Frye 已提交
1018
  def assigned_open_issues_count(force: false)
1019
    Rails.cache.fetch(['users', id, 'assigned_open_issues_count'], force: force, expires_in: 20.minutes) do
1020
      IssuesFinder.new(self, assignee_id: self.id, state: 'opened').execute.count
1021
    end
1022 1023
  end

J
Josh Frye 已提交
1024
  def update_cache_counts
1025
    assigned_open_merge_requests_count(force: true)
J
Josh Frye 已提交
1026 1027 1028
    assigned_open_issues_count(force: true)
  end

1029
  def invalidate_cache_counts
1030 1031 1032 1033 1034
    invalidate_issue_cache_counts
    invalidate_merge_request_cache_counts
  end

  def invalidate_issue_cache_counts
1035 1036 1037
    Rails.cache.delete(['users', id, 'assigned_open_issues_count'])
  end

1038 1039 1040 1041
  def invalidate_merge_request_cache_counts
    Rails.cache.delete(['users', id, 'assigned_open_merge_requests_count'])
  end

P
Paco Guzman 已提交
1042
  def todos_done_count(force: false)
1043
    Rails.cache.fetch(['users', id, 'todos_done_count'], force: force, expires_in: 20.minutes) do
1044
      TodosFinder.new(self, state: :done).execute.count
P
Paco Guzman 已提交
1045 1046 1047 1048
    end
  end

  def todos_pending_count(force: false)
1049
    Rails.cache.fetch(['users', id, 'todos_pending_count'], force: force, expires_in: 20.minutes) do
1050
      TodosFinder.new(self, state: :pending).execute.count
P
Paco Guzman 已提交
1051 1052 1053 1054 1055 1056 1057 1058
    end
  end

  def update_todos_count_cache
    todos_done_count(force: true)
    todos_pending_count(force: true)
  end

1059 1060 1061 1062 1063 1064 1065 1066 1067 1068 1069 1070
  # This is copied from Devise::Models::Lockable#valid_for_authentication?, as our auth
  # flow means we don't call that automatically (and can't conveniently do so).
  #
  # See:
  #   <https://github.com/plataformatec/devise/blob/v4.0.0/lib/devise/models/lockable.rb#L92>
  #
  def increment_failed_attempts!
    self.failed_attempts ||= 0
    self.failed_attempts += 1
    if attempts_exceeded?
      lock_access! unless access_locked?
    else
J
James Lopez 已提交
1071
      Users::UpdateService.new(self, user: self).execute(validate: false)
1072 1073 1074
    end
  end

1075 1076 1077 1078 1079 1080 1081 1082 1083
  def access_level
    if admin?
      :admin
    else
      :regular
    end
  end

  def access_level=(new_level)
D
Douwe Maan 已提交
1084 1085
    new_level = new_level.to_s
    return unless %w(admin regular).include?(new_level)
1086

D
Douwe Maan 已提交
1087 1088
    self.admin = (new_level == 'admin')
  end
1089

1090 1091 1092 1093 1094 1095
  # Does the user have access to all private groups & projects?
  # Overridden in EE to also check auditor?
  def full_private_access?
    admin?
  end

1096
  def update_two_factor_requirement
1097
    periods = expanded_groups_requiring_two_factor_authentication.pluck(:two_factor_grace_period)
1098

1099
    self.require_two_factor_authentication_from_group = periods.any?
1100 1101 1102 1103 1104
    self.two_factor_grace_period = periods.min || User.column_defaults['two_factor_grace_period']

    save
  end

A
Alexis Reigel 已提交
1105 1106 1107 1108 1109 1110 1111
  # each existing user needs to have an `rss_token`.
  # we do this on read since migrating all existing users is not a feasible
  # solution.
  def rss_token
    ensure_rss_token!
  end

1112 1113 1114 1115 1116 1117 1118 1119 1120 1121 1122 1123 1124 1125 1126 1127
  def sync_attribute?(attribute)
    return true if ldap_user? && attribute == :email

    attributes = Gitlab.config.omniauth.sync_profile_attributes

    if attributes.is_a?(Array)
      attributes.include?(attribute.to_s)
    else
      attributes
    end
  end

  def read_only_attribute?(attribute)
    user_synced_attributes_metadata&.read_only?(attribute)
  end

B
Brian Neel 已提交
1128 1129
  # override, from Devise
  def lock_access!
1130
    Gitlab::AppLogger.info("Account Locked: username=#{username}")
B
Brian Neel 已提交
1131 1132 1133
    super
  end

1134 1135 1136 1137 1138 1139 1140 1141 1142 1143 1144 1145 1146 1147 1148 1149 1150 1151 1152 1153 1154 1155 1156 1157 1158 1159 1160 1161
  # Determine the maximum access level for a group of projects in bulk.
  #
  # Returns a Hash mapping project ID -> maximum access level.
  def max_member_access_for_project_ids(project_ids)
    max_member_access_for_resource_ids(Project, project_ids) do |project_ids|
      project_authorizations.where(project: project_ids)
                            .group(:project_id)
                            .maximum(:access_level)
    end
  end

  def max_member_access_for_project(project_id)
    max_member_access_for_project_ids([project_id])[project_id]
  end

  # Determine the maximum access level for a group of groups in bulk.
  #
  # Returns a Hash mapping project ID -> maximum access level.
  def max_member_access_for_group_ids(group_ids)
    max_member_access_for_resource_ids(Group, group_ids) do |group_ids|
      group_members.where(source: group_ids).group(:source_id).maximum(:access_level)
    end
  end

  def max_member_access_for_group(group_id)
    max_member_access_for_group_ids([group_id])[group_id]
  end

1162 1163 1164 1165 1166
  protected

  # override, from Devise::Validatable
  def password_required?
    return false if internal?
1167

1168 1169 1170
    super
  end

1171 1172
  private

1173 1174 1175 1176 1177 1178 1179 1180
  def ci_projects_union
    scope  = { access_level: [Gitlab::Access::MASTER, Gitlab::Access::OWNER] }
    groups = groups_projects.where(members: scope)
    other  = projects.where(members: scope)

    Gitlab::SQL::Union.new([personal_projects.select(:id), groups.select(:id),
                            other.select(:id)])
  end
V
Valery Sizov 已提交
1181 1182 1183

  # Added according to https://github.com/plataformatec/devise/blob/7df57d5081f9884849ca15e4fde179ef164a575f/README.md#activejob-integration
  def send_devise_notification(notification, *args)
1184
    return true unless can?(:receive_notifications)
1185

1186
    devise_mailer.__send__(notification, self, *args).deliver_later # rubocop:disable GitlabSecurity/PublicSend
V
Valery Sizov 已提交
1187
  end
Z
Zeger-Jan van de Weg 已提交
1188

1189 1190 1191 1192 1193 1194 1195 1196 1197
  # This works around a bug in Devise 4.2.0 that erroneously causes a user to
  # be considered active in MySQL specs due to a sub-second comparison
  # issue. For more details, see: https://gitlab.com/gitlab-org/gitlab-ee/issues/2362#note_29004709
  def confirmation_period_valid?
    return false if self.class.allow_unconfirmed_access_for == 0.days

    super
  end

1198 1199 1200 1201 1202
  def ensure_user_rights_and_limits
    if external?
      self.can_create_group = false
      self.projects_limit   = 0
    else
1203 1204
      # Only revert these back to the default if they weren't specifically changed in this update.
      self.can_create_group = gitlab_config.default_can_create_group unless can_create_group_changed?
1205
      self.projects_limit = Gitlab::CurrentSettings.default_projects_limit unless projects_limit_changed?
1206
    end
Z
Zeger-Jan van de Weg 已提交
1207
  end
1208

1209 1210 1211 1212
  def signup_domain_valid?
    valid = true
    error = nil

1213 1214
    if Gitlab::CurrentSettings.domain_blacklist_enabled?
      blocked_domains = Gitlab::CurrentSettings.domain_blacklist
1215
      if domain_matches?(blocked_domains, email)
1216 1217 1218 1219 1220
        error = 'is not from an allowed domain.'
        valid = false
      end
    end

1221
    allowed_domains = Gitlab::CurrentSettings.domain_whitelist
1222
    unless allowed_domains.blank?
1223
      if domain_matches?(allowed_domains, email)
1224 1225
        valid = true
      else
D
dev-chris 已提交
1226
        error = "domain is not authorized for sign-up"
1227 1228 1229 1230
        valid = false
      end
    end

1231
    errors.add(:email, error) unless valid
1232 1233 1234

    valid
  end
1235

1236
  def domain_matches?(email_domains, email)
1237 1238 1239 1240 1241 1242 1243
    signup_domain = Mail::Address.new(email).domain
    email_domains.any? do |domain|
      escaped = Regexp.escape(domain).gsub('\*', '.*?')
      regexp = Regexp.new "^#{escaped}$", Regexp::IGNORECASE
      signup_domain =~ regexp
    end
  end
1244 1245 1246 1247

  def generate_token(token_field)
    if token_field == :incoming_email_token
      # Needs to be all lowercase and alphanumeric because it's gonna be used in an email address.
1248
      SecureRandom.hex.to_i(16).to_s(36)
1249 1250 1251 1252
    else
      super
    end
  end
1253

1254 1255 1256 1257 1258 1259
  def self.unique_internal(scope, username, email_pattern, &b)
    scope.first || create_unique_internal(scope, username, email_pattern, &b)
  end

  def self.create_unique_internal(scope, username, email_pattern, &creation_block)
    # Since we only want a single one of these in an instance, we use an
1260
    # exclusive lease to ensure than this block is never run concurrently.
1261
    lease_key = "user:unique_internal:#{username}"
1262 1263 1264 1265 1266 1267 1268 1269
    lease = Gitlab::ExclusiveLease.new(lease_key, timeout: 1.minute.to_i)

    until uuid = lease.try_obtain
      # Keep trying until we obtain the lease. To prevent hammering Redis too
      # much we'll wait for a bit between retries.
      sleep(1)
    end

1270
    # Recheck if the user is already present. One might have been
1271 1272
    # added between the time we last checked (first line of this method)
    # and the time we acquired the lock.
1273 1274
    existing_user = uncached { scope.first }
    return existing_user if existing_user.present?
1275 1276 1277

    uniquify = Uniquify.new

1278
    username = uniquify.string(username) { |s| User.find_by_username(s) }
1279

1280
    email = uniquify.string(-> (n) { Kernel.sprintf(email_pattern, n) }) do |s|
1281 1282 1283
      User.find_by_email(s)
    end

1284
    user = scope.build(
1285 1286 1287
      username: username,
      email: email,
      &creation_block
1288
    )
J
James Lopez 已提交
1289

J
James Lopez 已提交
1290
    Users::UpdateService.new(user, user: user).execute(validate: false)
1291
    user
1292 1293 1294
  ensure
    Gitlab::ExclusiveLease.cancel(lease_key, uuid)
  end
G
gitlabhq 已提交
1295
end