user.rb 41.3 KB
Newer Older
1 2
require 'carrierwave/orm/activerecord'

gitlabhq's avatar
gitlabhq committed
3
class User < ActiveRecord::Base
4
  extend Gitlab::ConfigHelper
5 6

  include Gitlab::ConfigHelper
7
  include Gitlab::SQL::Pattern
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
Nick Thomas's avatar
Nick Thomas committed
20

21
  prepend EE::User
22

23 24
  DEFAULT_NOTIFICATION_LEVEL = :participating

25 26
  ignore_column :external_email
  ignore_column :email_provider
27
  ignore_column :authentication_token
28

29
  add_authentication_token_field :incoming_email_token
30
  add_authentication_token_field :rss_token
31

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

43
  attr_encrypted :otp_secret,
44
    key:       Gitlab::Application.secrets.otp_key_base,
45
    mode:      :per_attribute_iv_and_salt,
James Lopez's avatar
James Lopez committed
46
    insecure_mode: true,
47 48
    algorithm: 'aes-256-cbc'

49
  devise :two_factor_authenticatable,
50
         otp_secret_encryption_key: Gitlab::Application.secrets.otp_key_base
51

52
  devise :two_factor_backupable, otp_number_of_backup_codes: 10
53
  serialize :otp_backup_codes, JSON # rubocop:disable Cop/ActiveRecordSerialize
54

55
  devise :lockable, :recoverable, :rememberable, :trackable,
56 57 58 59
         :validatable, :omniauthable, :confirmable, :registerable

  BLOCKED_MESSAGE = "Your account has been blocked. Please contact your GitLab " \
                    "administrator if you think this is an error.".freeze
gitlabhq's avatar
gitlabhq committed
60

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

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

69
    Users::UpdateService.new(self, user: self).execute(validate: false)
70 71
  end

72
  attr_accessor :force_random_password
gitlabhq's avatar
gitlabhq committed
73

74 75 76
  # Virtual attribute for authenticating by either username or email
  attr_accessor :login

77 78 79 80
  #
  # Relations
  #

81
  # Namespace for personal projects
82
  has_one :namespace, -> { where(type: nil) }, dependent: :destroy, foreign_key: :owner_id, inverse_of: :owner, autosave: true # rubocop:disable Cop/ActiveRecordDependent
Andrey Kumanyaev's avatar
Andrey Kumanyaev committed
83

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

92 93 94 95 96
  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
97
  has_one :user_synced_attributes_metadata, autosave: true
98 99

  # Groups
100 101
  has_many :members
  has_many :group_members, -> { where(requested_at: nil) }, source: 'GroupMember'
102
  has_many :groups, through: :group_members
103 104
  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
105

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

117 118 119 120 121 122 123 124 125 126 127 128 129
  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
  has_many :recent_events, -> { order "id DESC" }, foreign_key: :author_id,   class_name: "Event"
  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
130
  has_many :todos
131 132 133
  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
134

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

139
  has_many :custom_attributes, class_name: 'UserCustomAttribute'
140
  has_many :callouts, class_name: 'UserCallout'
141
  has_many :uploads, as: :model, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
142

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

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

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
167 168
  before_validation :set_notification_email, if: :email_changed?
  before_validation :set_public_email, if: :public_email_changed?
Douwe Maan's avatar
Douwe Maan committed
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) }
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
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
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
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
  # Note: When adding an option, it MUST go on the end of the array.
  enum project_view: [:readme, :activity, :files]
193

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

196 197
  accepts_nested_attributes_for :namespace

198 199 200
  state_machine :state, initial: :active do
    event :block do
      transition active: :blocked
201 202 203 204 205
      transition ldap_blocked: :blocked
    end

    event :ldap_block do
      transition active: :ldap_blocked
206 207 208 209
    end

    event :activate do
      transition blocked: :active
210 211 212 213 214 215 216
      transition ldap_blocked: :active
    end

    state :blocked, :ldap_blocked do
      def blocked?
        true
      end
217 218 219 220 221 222

      def active_for_authentication?
        false
      end

      def inactive_message
223
        BLOCKED_MESSAGE
224
      end
225 226 227
    end
  end

Andrey Kumanyaev's avatar
Andrey Kumanyaev committed
228
  # Scopes
229
  scope :admins, -> { where(admin: true) }
230
  scope :blocked, -> { with_states(:blocked, :ldap_blocked) }
231
  scope :external, -> { where(external: true) }
232
  scope :active, -> { with_state(:active).non_internal }
233
  scope :without_projects, -> { where('id NOT IN (SELECT DISTINCT(user_id) FROM members WHERE user_id IS NOT NULL AND requested_at IS NULL)') }
234
  scope :subscribed_for_admin_email, -> { where(admin_email_unsubscribed_at: nil) }
235
  scope :ldap, -> { joins(:identities).where('identities.provider LIKE ?', 'ldap%') }
236 237
  scope :with_provider, ->(provider) do
    joins(:identities).where(identities: { provider: provider })
Valery Sizov's avatar
Valery Sizov committed
238
  end
239
  scope :todo_authors, ->(user_id, state) { where(id: Todo.where(user_id: user_id, state: state).select(:author_id)) }
240 241
  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')) }
242 243

  def self.with_two_factor
244 245
    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])
246 247 248
  end

  def self.without_two_factor
249 250
    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)
251
  end
Andrey Kumanyaev's avatar
Andrey Kumanyaev committed
252

253 254 255
  #
  # Class methods
  #
Andrey Kumanyaev's avatar
Andrey Kumanyaev committed
256
  class << self
257
    # Devise method overridden to allow sign in with email or username
258 259 260
    def find_for_database_authentication(warden_conditions)
      conditions = warden_conditions.dup
      if login = conditions.delete(:login)
261
        where(conditions).find_by("lower(username) = :value OR lower(email) = :value", value: login.downcase.strip)
262
      else
Gabriel Mazetto's avatar
Gabriel Mazetto committed
263
        find_by(conditions)
264 265
      end
    end
266

Valery Sizov's avatar
Valery Sizov committed
267
    def sort(method)
268 269 270
      order_method = method || 'id_desc'

      case order_method.to_s
271 272
      when 'recent_sign_in' then order_recent_sign_in
      when 'oldest_sign_in' then order_oldest_sign_in
273
      else
274
        order_by(order_method)
Valery Sizov's avatar
Valery Sizov committed
275 276 277
      end
    end

278
    def for_github_id(id)
279
      joins(:identities).merge(Identity.with_extern_uid(:github, id))
280 281
    end

282 283
    # Find a User by their primary email or any associated secondary email
    def find_by_any_email(email)
284 285 286 287 288 289 290 291 292 293
      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}")
294
    end
295

296
    def existing_member?(email)
Valery Sizov's avatar
Valery Sizov committed
297
      User.where(email: email).any? || Email.where(email: email).any?
298 299
    end

300
    def filter(filter_name)
Andrey Kumanyaev's avatar
Andrey Kumanyaev committed
301
      case filter_name
302
      when 'admins'
303
        admins
304
      when 'blocked'
305
        blocked
306
      when 'two_factor_disabled'
307
        without_two_factor
308
      when 'two_factor_enabled'
309
        with_two_factor
310
      when 'wop'
311
        without_projects
312
      when 'external'
313
        external
Andrey Kumanyaev's avatar
Andrey Kumanyaev committed
314
      else
315
        active
Andrey Kumanyaev's avatar
Andrey Kumanyaev committed
316
      end
317 318
    end

319 320 321 322 323 324 325
    # 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.
326
    def search(query)
327 328
      return none if query.blank?

329 330
      query = query.downcase

331 332 333 334 335 336 337 338 339
      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

340
      where(
341 342 343
        fuzzy_arel_match(:name, query)
          .or(fuzzy_arel_match(:username, query))
          .or(arel_table[:email].eq(query))
344
      ).reorder(order % { query: ActiveRecord::Base.connection.quote(query) }, :name)
Andrey Kumanyaev's avatar
Andrey Kumanyaev committed
345
    end
gitlabhq's avatar
gitlabhq committed
346

347 348 349 350 351
    # 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)
352 353
      return none if query.blank?

354 355
      query = query.downcase

356
      email_table = Email.arel_table
357 358
      matched_by_emails_user_ids = email_table
        .project(email_table[:user_id])
359
        .where(email_table[:email].eq(query))
360 361

      where(
362 363
        fuzzy_arel_match(:name, query)
          .or(fuzzy_arel_match(:username, query))
364
          .or(arel_table[:email].eq(query))
365
          .or(arel_table[:id].in(matched_by_emails_user_ids))
366 367 368
      )
    end

369
    def by_login(login)
370 371 372 373 374 375 376
      return nil unless login

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

379 380 381 382
    def find_by_username(username)
      iwhere(username: username).take
    end

383
    def find_by_username!(username)
384
      iwhere(username: username).take!
385 386
    end

387
    def find_by_personal_access_token(token_string)
388 389
      return unless token_string

390
      PersonalAccessTokensFinder.new(state: 'active').find_by(token: token_string)&.user
391 392
    end

393 394
    # Returns a user for the given SSH key.
    def find_by_ssh_key_id(key_id)
395
      Key.find_by(id: key_id)&.user
396 397
    end

398
    def find_by_full_path(path, follow_redirects: false)
399
      namespace = Namespace.for_user.find_by_full_path(path, follow_redirects: follow_redirects)
400 401 402
      namespace&.owner
    end

403
    def non_ldap
Douwe Maan's avatar
Douwe Maan committed
404 405
      joins('LEFT JOIN identities ON identities.user_id = users.id')
        .where('identities.provider IS NULL OR identities.provider NOT LIKE ?', 'ldap%')
406
    end
407

408 409 410
    def reference_prefix
      '@'
    end
411 412 413 414

    # Pattern used to extract `@user` user references from text
    def reference_pattern
      %r{
415
        (?<!\w)
416
        #{Regexp.escape(reference_prefix)}
417
        (?<user>#{Gitlab::PathRegex::FULL_NAMESPACE_FORMAT_REGEX})
418 419
      }x
    end
420 421 422 423

    # Return (create if necessary) the ghost user. The ghost user
    # owns records previously belonging to deleted users.
    def ghost
424 425
      email = 'ghost%s@example.com'
      unique_internal(where(ghost: true), 'ghost', email) do |u|
426 427
        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'
428
        u.notification_email = email
429
      end
430
    end
vsizov's avatar
vsizov committed
431
  end
randx's avatar
randx committed
432

433 434 435 436
  def full_path
    username
  end

437 438 439 440
  def self.internal_attributes
    [:ghost]
  end

441
  def internal?
442 443 444 445 446 447 448 449
    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
450
    where(Hash[internal_attributes.zip([[false, nil]] * internal_attributes.size)])
451 452
  end

453 454 455
  #
  # Instance methods
  #
456 457 458 459 460

  def to_param
    username
  end

461
  def to_reference(_from = nil, target_project: nil, full: nil)
462 463 464
    "#{self.class.reference_prefix}#{username}"
  end

465 466
  def skip_confirmation=(bool)
    skip_confirmation! if bool
randx's avatar
randx committed
467
  end
468

Daniel Juarez's avatar
Daniel Juarez committed
469 470
  def skip_reconfirmation=(bool)
    skip_reconfirmation! if bool
randx's avatar
randx committed
471
  end
472

473
  def generate_reset_token
474
    @reset_token, enc = Devise.token_generator.generate(self.class, :reset_password_token)
475 476 477 478

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

479
    @reset_token
480 481
  end

482 483 484 485
  def recently_sent_password_reset?
    reset_password_sent_at.present? && reset_password_sent_at >= 1.minute.ago
  end

486
  def remember_me!
487
    super if ::Gitlab::Database.read_write?
488 489 490
  end

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

494
  def disable_two_factor!
495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512
    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?
513
    otp_required_for_login?
514 515 516
  end

  def two_factor_u2f_enabled?
517 518 519 520 521
    if u2f_registrations.loaded?
      u2f_registrations.any?
    else
      u2f_registrations.exists?
    end
522 523
  end

524 525 526 527 528 529
  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

530
  def unique_email
531 532
    if !emails.exists?(email: email) && Email.exists?(email: email)
      errors.add(:email, 'has already been taken')
533
    end
534 535
  end

536
  def owns_notification_email
537
    return if temp_oauth_email?
538

539
    errors.add(:notification_email, "is not an email you own") unless all_emails.include?(notification_email)
540 541
  end

542
  def owns_public_email
543
    return if public_email.blank?
544

545
    errors.add(:public_email, "is not an email you own") unless all_emails.include?(public_email)
546 547
  end

548 549 550 551 552
  # 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

553
  # Note: the use of the Emails services will cause `saves` on the user object, running
554
  # through the callbacks again and can have side effects, such as the `previous_changes`
555 556 557
  # 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
558
  def update_emails_with_primary_email
559
    previous_email = previous_changes[:email][0]  # grab this before the DestroyService is called
560
    primary_email_record = emails.find_by(email: email)
561
    Emails::DestroyService.new(self, user: self).execute(primary_email_record) if primary_email_record
562

563 564
    # 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
565
    Emails::CreateService.new(self, user: self, email: previous_email).execute(confirmed_at: confirmed_at)
566 567
  end

568
  def update_invalid_gpg_signatures
569
    gpg_keys.each(&:update_invalid_gpg_signatures)
570 571
  end

572
  # Returns the groups a user has access to, either through a membership or a project authorization
573
  def authorized_groups
574 575
    union = Gitlab::SQL::Union
      .new([groups.select(:id), authorized_projects.select(:namespace_id)])
576

577
    Group.where("namespaces.id IN (#{union.to_sql})") # rubocop:disable GitlabSecurity/SqlInjection
578 579
  end

580 581 582 583 584
  # Returns the groups a user is a member of, either directly or through a parent group
  def membership_groups
    Gitlab::GroupHierarchy.new(groups).base_and_descendants
  end

585 586
  # Returns a relation of groups the user has access to, including their parent
  # and child groups (recursively).
587
  def all_expanded_groups
588
    Gitlab::GroupHierarchy.new(groups).all_groups
589 590 591 592 593 594
  end

  def expanded_groups_requiring_two_factor_authentication
    all_expanded_groups.where(require_two_factor_authentication: true)
  end

595
  def refresh_authorized_projects
596 597 598 599
    Users::RefreshAuthorizedProjectsService.new(self).execute
  end

  def remove_project_authorizations(project_ids)
600
    project_authorizations.where(project_id: project_ids).delete_all
601 602
  end

603
  def authorized_projects(min_access_level = nil)
604 605
    # We're overriding an association, so explicitly call super with no
    # arguments or it would be passed as `force_reload` to the association
606
    projects = super()
607 608

    if min_access_level
609 610
      projects = projects
        .where('project_authorizations.access_level >= ?', min_access_level)
611
    end
612 613 614 615 616 617

    projects
  end

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

620 621 622 623 624 625 626 627 628 629
  # 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

630
  def owned_projects
631
    @owned_projects ||=
632 633
      Project.where('namespace_id IN (?) OR namespace_id = ?',
                    owned_groups.select(:id), namespace.id).joins(:namespace)
634 635
  end

636 637 638 639
  # 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
640
    authorized_projects(Gitlab::Access::REPORTER).non_archived.with_issues_enabled
641 642
  end

Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
643
  def require_ssh_key?
644 645 646
    count = Users::KeysCountService.new(self).count

    count.zero? && Gitlab::ProtocolAccess.allowed?('ssh')
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
647 648
  end

649 650 651 652 653 654
  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?
655 656
  end

657
  def require_personal_access_token_creation_for_git_auth?
658
    return false if allow_password_authentication_for_git? || ldap_user?
659 660

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

663 664 665 666
  def require_extra_setup_for_git_auth?
    require_password_creation_for_git? || require_personal_access_token_creation_for_git_auth?
  end

667
  def allow_password_authentication?
668 669 670 671
    allow_password_authentication_for_web? || allow_password_authentication_for_git?
  end

  def allow_password_authentication_for_web?
672
    Gitlab::CurrentSettings.password_authentication_enabled_for_web? && !ldap_user?
673 674 675
  end

  def allow_password_authentication_for_git?
676
    Gitlab::CurrentSettings.password_authentication_enabled_for_git? && !ldap_user?
677 678
  end

679
  def can_change_username?
680
    gitlab_config.username_changing_enabled
681 682
  end

Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
683
  def can_create_project?
684
    projects_limit_left > 0
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
685 686 687
  end

  def can_create_group?
688
    can?(:create_group)
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
689 690
  end

691 692 693 694
  def can_select_namespace?
    several_namespaces? || admin
  end

695
  def can?(action, subject = :global)
696
    Ability.allowed?(self, action, subject)
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
697 698
  end

699 700 701 702
  def confirm_deletion_with_password?
    !password_automatically_set? && allow_password_authentication?
  end

Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
703 704 705 706
  def first_name
    name.split.first unless name.blank?
  end

707
  def projects_limit_left
708 709 710 711 712
    projects_limit - personal_projects_count
  end

  def personal_projects_count
    @personal_projects_count ||= personal_projects.count
713 714
  end

715 716
  def recent_push(project = nil)
    service = Users::LastPushEventService.new(self)
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
717

718 719 720 721
    if project
      service.last_event_for_project(project)
    else
      service.last_event_for_user
722
    end
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
723 724 725
  end

  def several_namespaces?
726
    owned_groups.any? || masters_groups.any?
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
727 728 729 730 731
  end

  def namespace_id
    namespace.try :id
  end
732

733 734 735
  def name_with_username
    "#{name} (#{username})"
  end
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
736

737
  def already_forked?(project)
738 739 740
    !!fork_of(project)
  end

741
  def fork_of(project)
742
    namespace.find_fork_of(project)
743
  end
744 745

  def ldap_user?
746
    if identities.loaded?
747
      identities.find { |identity| Gitlab::OAuth::Provider.ldap_provider?(identity.provider) && !identity.extern_uid.nil? }
748 749 750
    else
      identities.exists?(["provider LIKE ? AND extern_uid IS NOT NULL", "ldap%"])
    end
751 752 753 754
  end

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

757
  def project_deploy_keys
758
    DeployKey.unscoped.in_projects(authorized_projects.pluck(:id)).distinct(:id)
759 760
  end

761
  def accessible_deploy_keys
762 763 764 765 766
    @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
767
  end
768 769

  def created_by
skv's avatar
skv committed
770
    User.find_by(id: created_by_id) if created_by_id
771
  end
772 773

  def sanitize_attrs
774 775 776
    %i[skype linkedin twitter].each do |attr|
      value = self[attr]
      self[attr] = Sanitize.clean(value) if value.present?
777 778
    end
  end
779

780
  def set_notification_email
781 782
    if notification_email.blank? || !all_emails.include?(notification_email)
      self.notification_email = email
783 784 785
    end
  end

786
  def set_public_email
787
    if public_email.blank? || !all_emails.include?(public_email)
788
      self.public_email = ''
789 790 791
    end
  end

792
  def update_secondary_emails!
793 794 795
    set_notification_email
    set_public_email
    save if notification_email_changed? || public_email_changed?
796 797
  end

798
  def set_projects_limit
799 800 801
    # `User.select(:id)` raises
    # `ActiveModel::MissingAttributeError: missing attribute: projects_limit`
    # without this safeguard!
802
    return unless has_attribute?(:projects_limit) && projects_limit.nil?
803

804
    self.projects_limit = Gitlab::CurrentSettings.default_projects_limit
805 806
  end

807
  def requires_ldap_check?
808 809 810
    if !Gitlab.config.ldap.enabled
      false
    elsif ldap_user?
811
      !last_credential_check_at || (last_credential_check_at + Gitlab.config.ldap['sync_time']) < Time.now
812 813 814
    else
      false
    end
815
  end
816

Jacob Vosmaer's avatar
Jacob Vosmaer committed
817 818 819 820 821 822 823
  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

824 825 826 827 828
  def solo_owned_groups
    @solo_owned_groups ||= owned_groups.select do |group|
      group.owners == [self]
    end
  end
829 830

  def with_defaults
831
    User.defaults.each do |k, v|
832
      public_send("#{k}=", v) # rubocop:disable GitlabSecurity/PublicSend
833
    end
834 835

    self
836
  end
837 838 839 840 841

  def can_leave_project?(project)
    project.namespace != namespace &&
      project.project_member(self)
  end
842

Jerome Dalbert's avatar
Jerome Dalbert committed
843
  def full_website_url
844
    return "http://#{website_url}" if website_url !~ %r{\Ahttps?://}
Jerome Dalbert's avatar
Jerome Dalbert committed
845 846 847 848 849

    website_url
  end

  def short_website_url
850
    website_url.sub(%r{\Ahttps?://}, '')
Jerome Dalbert's avatar
Jerome Dalbert committed
851
  end
GitLab's avatar
GitLab committed
852

853
  def all_ssh_keys
854
    keys.map(&:publishable_key)
855
  end
856 857

  def temp_oauth_email?
858
    email.start_with?('temp-email-for-oauth')
859 860
  end

861
  def avatar_url(size: nil, scale: 2, **args)
862
    GravatarService.new.execute(email, size, scale, username: username)
863
  end
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
864

865 866 867 868
  def primary_email_verified?
    confirmed? && !temp_oauth_email?
  end

869
  def all_emails
870
    all_emails = []
871 872
    all_emails << email unless temp_oauth_email?
    all_emails.concat(emails.map(&:email))
873
    all_emails
874 875
  end

876
  def verified_emails
877
    verified_emails = []
878
    verified_emails << email if primary_email_verified?
879
    verified_emails.concat(emails.confirmed.pluck(:email))
880 881 882
    verified_emails
  end

883
  def verified_email?(check_email)
884
    downcased = check_email.downcase
885
    email == downcased ? primary_email_verified? : emails.confirmed.where(email: downcased).exists?
886 887
  end

Kirill Zaitsev's avatar
Kirill Zaitsev committed
888 889 890 891
  def hook_attrs
    {
      name: name,
      username: username,
892
      avatar_url: avatar_url(only_path: false)
Kirill Zaitsev's avatar
Kirill Zaitsev committed
893 894 895
    }
  end

Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
896
  def ensure_namespace_correct
897 898 899 900
    if namespace
      namespace.path = namespace.name = username if username_changed?
    else
      build_namespace(path: username, name: username)
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
901 902 903
    end
  end

904 905 906 907 908
  def set_username_errors
    namespace_path_errors = self.errors.delete(:"namespace.path")
    self.errors[:username].concat(namespace_path_errors) if namespace_path_errors
  end

909 910 911 912
  def username_changed_hook
    system_hook_service.execute_hooks_for(self, :rename)
  end

Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
913
  def post_destroy_hook
914
    log_info("User \"#{name}\" (#{email})  was removed")
915

Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
916 917 918
    system_hook_service.execute_hooks_for(self, :destroy)
  end

919 920 921 922
  def remove_key_cache
    Users::KeysCountService.new(self).delete_cache
  end

923 924 925 926 927
  def delete_async(deleted_by:, params: {})
    block if params[:hard_delete]
    DeleteUserWorker.perform_async(deleted_by.id, id, params)
  end

Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
928
  def notification_service
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
929 930 931
    NotificationService.new
  end

932
  def log_info(message)
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
933 934 935 936 937 938
    Gitlab::AppLogger.info message
  end

  def system_hook_service
    SystemHooksService.new
  end
939 940 941 942

  def admin_unsubscribe!
    update_column :admin_email_unsubscribed_at, Time.now
  end
943

Ciro Santilli's avatar
Ciro Santilli committed
944
  def starred?(project)
945
    starred_projects.exists?(project.id)
Ciro Santilli's avatar
Ciro Santilli committed
946 947 948
  end

  def toggle_star(project)
949
    UsersStarProject.transaction do
950 951
      user_star_project = users_star_projects
          .where(project: project, user: self).lock(true).first
952 953 954 955 956 957

      if user_star_project
        user_star_project.destroy
      else
        UsersStarProject.create!(project: project, user: self)
      end
Ciro Santilli's avatar
Ciro Santilli committed
958 959
    end
  end
960 961

  def manageable_namespaces
962 963 964 965 966 967 968 969 970 971
    @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
972
  end
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
973

974 975 976 977 978 979
  def namespaces
    namespace_ids = groups.pluck(:id)
    namespace_ids.push(namespace.id)
    Namespace.where(id: namespace_ids)
  end

Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
980
  def oauth_authorized_tokens
981
    Doorkeeper::AccessToken.where(resource_owner_id: id, revoked_at: nil)
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
982
  end
983

984 985 986 987 988 989 990 991 992 993 994 995
  # 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
  # ms on a database with a similar size to GitLab.com's database. On the other
  # hand, using a subquery means we can get the exact same data in about 40 ms.
  def contributed_projects
996 997 998 999 1000
    events = Event.select(:project_id)
      .contributions.where(author_id: self)
      .where("created_at > ?", Time.now - 1.year)
      .uniq
      .reorder(nil)
1001 1002

    Project.where(id: events)
1003
  end
1004

1005 1006 1007
  def can_be_removed?
    !solo_owned_groups.present?
  end
1008 1009

  def ci_authorized_runners
1010
    @ci_authorized_runners ||= begin
1011
      runner_ids = Ci::RunnerProject
1012
        .where("ci_runner_projects.project_id IN (#{ci_projects_union.to_sql})") # rubocop:disable GitlabSecurity/SqlInjection
1013
        .select(:runner_id)
1014 1015
      Ci::Runner.specific.where(id: runner_ids)
    end
1016
  end
1017

1018
  def notification_settings_for(source)
1019 1020 1021 1022 1023
    if notification_settings.loaded?
      notification_settings.find { |notification| notification.source == source }
    else
      notification_settings.find_or_initialize_by(source: source)
    end
1024 1025
  end

1026 1027 1028
  # Lazy load global notification setting
  # Initializes User setting with Participating level if setting not persisted
  def global_notification_setting
1029 1030 1031 1032 1033 1034
    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
1035 1036
  end

1037
  def assigned_open_merge_requests_count(force: false)
1038
    Rails.cache.fetch(['users', id, 'assigned_open_merge_requests_count'], force: force, expires_in: 20.minutes) do
1039
      MergeRequestsFinder.new(self, assignee_id: self.id, state: 'opened').execute.count
1040 1041 1042
    end
  end

1043
  def assigned_open_issues_count(force: false)
1044
    Rails.cache.fetch(['users', id, 'assigned_open_issues_count'], force: force, expires_in: 20.minutes) do
1045
      IssuesFinder.new(self, assignee_id: self.id, state: 'opened').execute.count
1046
    end
1047 1048
  end

1049
  def update_cache_counts
1050
    assigned_open_merge_requests_count(force: true)
1051 1052 1053
    assigned_open_issues_count(force: true)
  end

1054
  def invalidate_cache_counts
1055 1056 1057 1058 1059
    invalidate_issue_cache_counts
    invalidate_merge_request_cache_counts
  end

  def invalidate_issue_cache_counts
1060 1061 1062
    Rails.cache.delete(['users', id, 'assigned_open_issues_count'])
  end

1063 1064 1065 1066
  def invalidate_merge_request_cache_counts
    Rails.cache.delete(['users', id, 'assigned_open_merge_requests_count'])
  end

1067
  def todos_done_count(force: false)
1068
    Rails.cache.fetch(['users', id, 'todos_done_count'], force: force, expires_in: 20.minutes) do
1069
      TodosFinder.new(self, state: :done).execute.count
1070 1071 1072 1073
    end
  end

  def todos_pending_count(force: false)
1074
    Rails.cache.fetch(['users', id, 'todos_pending_count'], force: force, expires_in: 20.minutes) do
1075
      TodosFinder.new(self, state: :pending).execute.count
1076 1077 1078 1079 1080 1081 1082 1083
    end
  end

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

1084 1085 1086 1087 1088 1089 1090 1091 1092 1093 1094 1095
  # 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
1096
      Users::UpdateService.new(self, user: self).execute(validate: false)
1097 1098 1099
    end
  end

Douwe Maan's avatar
Douwe Maan committed
1100 1101 1102 1103 1104 1105 1106 1107 1108 1109
  def access_level
    if admin?
      :admin
    else
      :regular
    end
  end

  def access_level=(new_level)
    new_level = new_level.to_s
1110
    return unless %w(admin regular).include?(new_level)
Douwe Maan's avatar
Douwe Maan committed
1111 1112

    self.admin = (new_level == 'admin')
1113 1114 1115
  end

  # Does the user have access to all private groups & projects?
1116 1117
  # Overridden in EE to also check auditor?
  def full_private_access?
1118
    admin?
Douwe Maan's avatar
Douwe Maan committed
1119 1120
  end

1121
  def update_two_factor_requirement
1122
    periods = expanded_groups_requiring_two_factor_authentication.pluck(:two_factor_grace_period)
1123

1124
    self.require_two_factor_authentication_from_group = periods.any?
1125 1126 1127 1128 1129
    self.two_factor_grace_period = periods.min || User.column_defaults['two_factor_grace_period']

    save
  end

Alexis Reigel's avatar
Alexis Reigel committed
1130 1131 1132 1133 1134 1135 1136
  # 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

1137 1138 1139 1140 1141 1142 1143 1144 1145 1146 1147 1148 1149 1150 1151 1152
  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

Brian Neel's avatar
Brian Neel committed
1153 1154
  # override, from Devise
  def lock_access!
1155
    Gitlab::AppLogger.info("Account Locked: username=#{username}")
Brian Neel's avatar
Brian Neel committed
1156 1157 1158
    super
  end

1159 1160 1161 1162 1163 1164 1165 1166 1167 1168 1169 1170 1171 1172 1173 1174 1175 1176 1177 1178 1179 1180 1181 1182 1183 1184 1185 1186
  # 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

1187 1188 1189 1190 1191
  protected

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

1193 1194 1195
    super
  end

1196 1197 1198 1199 1200 1201 1202 1203 1204 1205
  private

  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
1206 1207 1208

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

1211
    devise_mailer.__send__(notification, self, *args).deliver_later # rubocop:disable GitlabSecurity/PublicSend
1212
  end
Zeger-Jan van de Weg's avatar
Zeger-Jan van de Weg committed
1213

1214 1215 1216 1217 1218 1219 1220 1221 1222
  # 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

1223 1224 1225 1226 1227
  def ensure_user_rights_and_limits
    if external?
      self.can_create_group = false
      self.projects_limit   = 0
    else
1228 1229
      # 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?
1230
      self.projects_limit = Gitlab::CurrentSettings.default_projects_limit unless projects_limit_changed?
1231
    end
Zeger-Jan van de Weg's avatar
Zeger-Jan van de Weg committed
1232
  end
1233

1234 1235 1236 1237
  def signup_domain_valid?
    valid = true
    error = nil

1238 1239
    if Gitlab::CurrentSettings.domain_blacklist_enabled?
      blocked_domains = Gitlab::CurrentSettings.domain_blacklist
1240
      if domain_matches?(blocked_domains, email)
1241 1242 1243 1244 1245
        error = 'is not from an allowed domain.'
        valid = false
      end
    end

1246
    allowed_domains = Gitlab::CurrentSettings.domain_whitelist
1247
    unless allowed_domains.blank?
1248
      if domain_matches?(allowed_domains, email)
1249 1250
        valid = true
      else
1251
        error = "domain is not authorized for sign-up"
1252 1253 1254 1255
        valid = false
      end
    end

1256
    errors.add(:email, error) unless valid
1257 1258 1259

    valid
  end
1260

1261
  def domain_matches?(email_domains, email)
1262 1263 1264 1265 1266 1267 1268
    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
1269 1270 1271 1272

  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.
1273
      SecureRandom.hex.to_i(16).to_s(36)
1274 1275 1276 1277
    else
      super
    end
  end
1278

1279 1280 1281 1282 1283 1284
  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
1285
    # exclusive lease to ensure than this block is never run concurrently.
1286
    lease_key = "user:unique_internal:#{username}"
1287 1288 1289 1290 1291 1292 1293 1294
    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

1295
    # Recheck if the user is already present. One might have been
1296 1297
    # added between the time we last checked (first line of this method)
    # and the time we acquired the lock.
1298 1299
    existing_user = uncached { scope.first }
    return existing_user if existing_user.present?
1300 1301 1302

    uniquify = Uniquify.new

1303
    username = uniquify.string(username) { |s| User.find_by_username(s) }
1304

1305
    email = uniquify.string(-> (n) { Kernel.sprintf(email_pattern, n) }) do |s|
1306 1307 1308
      User.find_by_email(s)
    end

1309
    user = scope.build(
1310 1311 1312
      username: username,
      email: email,
      &creation_block
1313
    )
James Lopez's avatar
James Lopez committed
1314

1315
    Users::UpdateService.new(user, user: user).execute(validate: false)
1316
    user
1317 1318 1319
  ensure
    Gitlab::ExclusiveLease.cancel(lease_key, uuid)
  end
gitlabhq's avatar
gitlabhq committed
1320
end