user.rb 42.9 KB
Newer Older
1 2
# frozen_string_literal: true

S
Steven Thonus 已提交
3 4
require 'carrierwave/orm/activerecord'

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

  include Gitlab::ConfigHelper
H
Hiroyuki Sato 已提交
9
  include Gitlab::SQL::Pattern
D
Douwe Maan 已提交
10
  include AfterCommitQueue
11
  include Avatarable
12 13
  include Referable
  include Sortable
14
  include CaseSensitivity
15
  include TokenAuthenticatable
16
  include IgnorableColumn
17
  include FeatureGate
18
  include CreatedAtFilterable
19
  include BulkMemberAccessLoad
20
  include BlocksJsonSerialization
J
Jan Provaznik 已提交
21
  include WithUploads
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 :feed_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,
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
R
Robert Speicher 已提交
51

52
  devise :two_factor_backupable, otp_number_of_backup_codes: 10
53
  serialize :otp_backup_codes, JSON # rubocop:disable Cop/ActiveRecordSerialize
R
Robert Speicher 已提交
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
G
gitlabhq 已提交
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
    return if Gitlab::Database.read_only?

66 67
    update_tracked_fields(request)

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

J
James Lopez 已提交
71
    Users::UpdateService.new(self, user: self).execute(validate: false)
72 73
  end

74
  attr_accessor :force_random_password
G
gitlabhq 已提交
75

76 77 78
  # Virtual attribute for authenticating by either username or email
  attr_accessor :login

79 80 81 82
  #
  # Relations
  #

83
  # Namespace for personal projects
84
  has_one :namespace, -> { where(type: nil) }, dependent: :destroy, foreign_key: :owner_id, inverse_of: :owner, autosave: true # rubocop:disable Cop/ActiveRecordDependent
85 86

  # Profile
87 88
  has_many :keys, -> { where(type: ['Key', nil]) }, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
  has_many :deploy_keys, -> { where(type: 'DeployKey') }, dependent: :nullify # rubocop:disable Cop/ActiveRecordDependent
89
  has_many :gpg_keys
90

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

  # Groups
99 100
  has_many :members
  has_many :group_members, -> { where(requested_at: nil) }, source: 'GroupMember'
101 102
  has_many :groups, through: :group_members
  has_many :owned_groups, -> { where(members: { access_level: Gitlab::Access::OWNER }) }, through: :group_members, source: :group
103
  has_many :maintainers_groups, -> { where(members: { access_level: Gitlab::Access::MAINTAINER }) }, through: :group_members, source: :group
104 105 106 107
  has_many :owned_or_maintainers_groups,
           -> { where(members: { access_level: [Gitlab::Access::MAINTAINER, Gitlab::Access::OWNER] }) },
           through: :group_members,
           source: :group
108
  alias_attribute :masters_groups, :maintainers_groups
109

110
  # Projects
111 112
  has_many :groups_projects,          through: :groups, source: :projects
  has_many :personal_projects,        through: :namespace, source: :projects
113
  has_many :project_members, -> { where(requested_at: nil) }
114 115
  has_many :projects,                 through: :project_members
  has_many :created_projects,         foreign_key: :creator_id, class_name: 'Project'
116
  has_many :users_star_projects, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
117
  has_many :starred_projects, through: :users_star_projects, source: :project
118
  has_many :project_authorizations, dependent: :delete_all # rubocop:disable Cop/ActiveRecordDependent
119
  has_many :authorized_projects, through: :project_authorizations, source: :project
120

121
  has_many :user_interacted_projects
122
  has_many :project_interactions, through: :user_interacted_projects, source: :project, class_name: 'Project'
123

124 125 126 127 128 129 130 131 132 133 134 135
  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 :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
136
  has_many :todos
137
  has_many :notification_settings
138 139
  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
140

141
  has_many :issue_assignees
142
  has_many :assigned_issues, class_name: "Issue", through: :issue_assignees, source: :issue
143
  has_many :assigned_merge_requests,  dependent: :nullify, foreign_key: :assignee_id, class_name: "MergeRequest" # rubocop:disable Cop/ActiveRecordDependent
144

145
  has_many :custom_attributes, class_name: 'UserCustomAttribute'
M
Matija Čupić 已提交
146
  has_many :callouts, class_name: 'UserCallout'
147 148
  has_many :term_agreements
  belongs_to :accepted_term, class_name: 'ApplicationSetting::Term'
149

B
Bob Van Landuyt 已提交
150 151
  has_one :status, class_name: 'UserStatus'

152 153 154
  #
  # Validations
  #
155
  # Note: devise :validatable above adds validations for :email and :password
C
Cyril 已提交
156
  validates :name, presence: true
D
Douwe Maan 已提交
157
  validates :email, confirmation: true
158 159
  validates :notification_email, presence: true
  validates :notification_email, email: true, if: ->(user) { user.notification_email != user.email }
160
  validates :public_email, presence: true, uniqueness: true, email: true, allow_blank: true
161
  validates :bio, length: { maximum: 255 }, allow_blank: true
162 163 164
  validates :projects_limit,
    presence: true,
    numericality: { greater_than_or_equal_to: 0, less_than_or_equal_to: Gitlab::Database::MAX_INT_VALUE }
165
  validates :username, presence: true
166

167
  validates :namespace, presence: true
168 169
  validate :namespace_move_dir_allowed, if: :username_changed?

T
Tiago Botelho 已提交
170 171 172
  validate :unique_email, if: :email_changed?
  validate :owns_notification_email, if: :notification_email_changed?
  validate :owns_public_email, if: :public_email_changed?
173
  validate :signup_domain_valid?, on: :create, if: ->(user) { !user.created_by_id }
174

175
  before_validation :sanitize_attrs
176
  before_validation :set_notification_email, if: :new_record?
T
Tiago Botelho 已提交
177
  before_validation :set_public_email, if: :public_email_changed?
178
  before_save :set_public_email, if: :public_email_changed? # in case validation is skipped
D
Douwe Maan 已提交
179
  before_save :ensure_incoming_email_token
180
  before_save :ensure_user_rights_and_limits, if: ->(user) { user.new_record? || user.external_changed? }
181
  before_save :skip_reconfirmation!, if: ->(user) { user.email_changed? && user.read_only_attribute?(:email) }
A
Alexandra 已提交
182
  before_save :check_for_verified_email, if: ->(user) { user.email_changed? && !user.new_record? }
183
  before_validation :ensure_namespace_correct
184
  before_save :ensure_namespace_correct # in case validation is skipped
185
  after_validation :set_username_errors
186
  after_update :username_changed_hook, if: :username_changed?
187
  after_destroy :post_destroy_hook
Y
Yorick Peterse 已提交
188
  after_destroy :remove_key_cache
189 190 191 192 193 194 195 196 197 198 199 200 201 202 203
  after_commit(on: :update) do
    if previous_changes.key?('email')
      # Grab previous_email here since previous_changes changes after
      # #update_emails_with_primary_email and #update_notification_email are called
      previous_email = previous_changes[:email][0]

      update_emails_with_primary_email(previous_email)
      update_invalid_gpg_signatures

      if previous_email == notification_email
        self.notification_email = email
        save
      end
    end
  end
204

205
  after_initialize :set_projects_limit
D
Dmitriy Zaporozhets 已提交
206

207
  # User's Layout preference
208
  enum layout: [:fixed, :fluid]
209

210 211
  # User's Dashboard preference
  # Note: When adding an option, it MUST go on the end of the array.
212
  enum dashboard: [:projects, :stars, :project_activity, :starred_project_activity, :groups, :todos, :issues, :merge_requests]
213

214
  # User's Project preference
215 216 217
  # Note: When adding an option, it MUST go on the end of the array.
  enum project_view: [:readme, :activity, :files]

218
  delegate :path, to: :namespace, allow_nil: true, prefix: true
219

220 221 222
  state_machine :state, initial: :active do
    event :block do
      transition active: :blocked
223
      transition ldap_blocked: :blocked
224 225
    end

226 227 228 229
    event :ldap_block do
      transition active: :ldap_blocked
    end

230 231
    event :activate do
      transition blocked: :active
232
      transition ldap_blocked: :active
233
    end
234 235 236 237 238

    state :blocked, :ldap_blocked do
      def blocked?
        true
      end
239 240 241 242 243 244

      def active_for_authentication?
        false
      end

      def inactive_message
245
        BLOCKED_MESSAGE
246
      end
247
    end
248 249
  end

A
Andrey Kumanyaev 已提交
250
  # Scopes
251
  scope :admins, -> { where(admin: true) }
252
  scope :blocked, -> { with_states(:blocked, :ldap_blocked) }
253
  scope :external, -> { where(external: true) }
J
James Lopez 已提交
254
  scope :active, -> { with_state(:active).non_internal }
255
  scope :without_projects, -> { joins('LEFT JOIN project_authorizations ON users.id = project_authorizations.user_id').where(project_authorizations: { user_id: nil }) }
V
Valery Sizov 已提交
256
  scope :todo_authors, ->(user_id, state) { where(id: Todo.where(user_id: user_id, state: state).select(:author_id)) }
257 258
  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')) }
259
  scope :confirmed, -> { where.not(confirmed_at: nil) }
260

261
  def self.with_two_factor_indistinct
262
    joins("LEFT OUTER JOIN u2f_registrations AS u2f ON u2f.user_id = users.id")
263 264 265 266 267
      .where("u2f.id IS NOT NULL OR users.otp_required_for_login = ?", true)
  end

  def self.with_two_factor
    with_two_factor_indistinct.distinct(arel_table[:id])
268 269 270
  end

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

275 276 277
  #
  # Class methods
  #
A
Andrey Kumanyaev 已提交
278
  class << self
279
    # Devise method overridden to allow sign in with email or username
280 281 282
    def find_for_database_authentication(warden_conditions)
      conditions = warden_conditions.dup
      if login = conditions.delete(:login)
283
        where(conditions).find_by("lower(username) = :value OR lower(email) = :value", value: login.downcase.strip)
284
      else
G
Gabriel Mazetto 已提交
285
        find_by(conditions)
286 287
      end
    end
288

289
    def sort_by_attribute(method)
290 291 292
      order_method = method || 'id_desc'

      case order_method.to_s
293 294
      when 'recent_sign_in' then order_recent_sign_in
      when 'oldest_sign_in' then order_oldest_sign_in
295
      else
296
        order_by(order_method)
V
Valery Sizov 已提交
297 298 299
      end
    end

300
    def for_github_id(id)
301
      joins(:identities).merge(Identity.with_extern_uid(:github, id))
302 303
    end

304
    # Find a User by their primary email or any associated secondary email
305 306
    def find_by_any_email(email, confirmed: false)
      by_any_email(email, confirmed: confirmed).take
Y
Yorick Peterse 已提交
307 308 309
    end

    # Returns a relation containing all the users for the given Email address
310
    def by_any_email(email, confirmed: false)
Y
Yorick Peterse 已提交
311
      users = where(email: email)
312 313
      users = users.confirmed if confirmed

Y
Yorick Peterse 已提交
314
      emails = joins(:emails).where(emails: { email: email })
315
      emails = emails.confirmed if confirmed
Y
Yorick Peterse 已提交
316 317 318
      union = Gitlab::SQL::Union.new([users, emails])

      from("(#{union.to_sql}) #{table_name}")
319
    end
320

321
    def filter(filter_name)
A
Andrey Kumanyaev 已提交
322
      case filter_name
323
      when 'admins'
324
        admins
325
      when 'blocked'
326
        blocked
327
      when 'two_factor_disabled'
328
        without_two_factor
329
      when 'two_factor_enabled'
330
        with_two_factor
331
      when 'wop'
332
        without_projects
333
      when 'external'
334
        external
A
Andrey Kumanyaev 已提交
335
      else
336
        active
A
Andrey Kumanyaev 已提交
337
      end
338 339
    end

340 341 342 343 344 345 346
    # 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.
347
    def search(query)
348 349
      return none if query.blank?

350 351
      query = query.downcase

352 353 354 355 356 357 358 359 360
      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

361
      where(
362 363
        fuzzy_arel_match(:name, query, lower_exact_match: true)
          .or(fuzzy_arel_match(:username, query, lower_exact_match: true))
364
          .or(arel_table[:email].eq(query))
365
      ).reorder(order % { query: ActiveRecord::Base.connection.quote(query) }, :name)
A
Andrey Kumanyaev 已提交
366
    end
367

368 369 370 371 372
    # 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)
373 374
      return none if query.blank?

375 376
      query = query.downcase

377
      email_table = Email.arel_table
378 379
      matched_by_emails_user_ids = email_table
        .project(email_table[:user_id])
380
        .where(email_table[:email].eq(query))
381 382

      where(
383 384
        fuzzy_arel_match(:name, query)
          .or(fuzzy_arel_match(:username, query))
385
          .or(arel_table[:email].eq(query))
386
          .or(arel_table[:id].in(matched_by_emails_user_ids))
387 388 389
      )
    end

390
    def by_login(login)
391 392 393 394 395 396 397
      return nil unless login

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

400 401 402 403
    def find_by_username(username)
      iwhere(username: username).take
    end

R
Robert Speicher 已提交
404
    def find_by_username!(username)
405
      iwhere(username: username).take!
R
Robert Speicher 已提交
406 407
    end

T
Timothy Andrew 已提交
408
    def find_by_personal_access_token(token_string)
409 410
      return unless token_string

411
      PersonalAccessTokensFinder.new(state: 'active').find_by(token: token_string)&.user
T
Timothy Andrew 已提交
412 413
    end

Y
Yorick Peterse 已提交
414 415
    # Returns a user for the given SSH key.
    def find_by_ssh_key_id(key_id)
416
      Key.find_by(id: key_id)&.user
Y
Yorick Peterse 已提交
417 418
    end

419
    def find_by_full_path(path, follow_redirects: false)
420 421
      namespace = Namespace.for_user.find_by_full_path(path, follow_redirects: follow_redirects)
      namespace&.owner
422 423
    end

424 425 426
    def reference_prefix
      '@'
    end
427 428 429 430

    # Pattern used to extract `@user` user references from text
    def reference_pattern
      %r{
431
        (?<!\w)
432
        #{Regexp.escape(reference_prefix)}
433
        (?<user>#{Gitlab::PathRegex::FULL_NAMESPACE_FORMAT_REGEX})
434 435
      }x
    end
436 437 438 439

    # Return (create if necessary) the ghost user. The ghost user
    # owns records previously belonging to deleted users.
    def ghost
440 441
      email = 'ghost%s@example.com'
      unique_internal(where(ghost: true), 'ghost', email) do |u|
442 443 444
        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'
      end
445
    end
V
vsizov 已提交
446
  end
R
randx 已提交
447

M
Michael Kozono 已提交
448 449 450 451
  def full_path
    username
  end

452 453 454 455
  def self.internal_attributes
    [:ghost]
  end

456
  def internal?
457 458 459 460 461 462 463 464
    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
465
    where(internal_attributes.map { |attr| "#{attr} IS NOT TRUE" }.join(" AND "))
466 467
  end

468 469 470
  #
  # Instance methods
  #
471 472 473 474 475

  def to_param
    username
  end

J
Jarka Kadlecova 已提交
476
  def to_reference(_from = nil, target_project: nil, full: nil)
477 478 479
    "#{self.class.reference_prefix}#{username}"
  end

480 481
  def skip_confirmation=(bool)
    skip_confirmation! if bool
D
Daniel Juarez 已提交
482 483 484 485
  end

  def skip_reconfirmation=(bool)
    skip_reconfirmation! if bool
R
randx 已提交
486
  end
487

488
  def generate_reset_token
M
Marin Jankovski 已提交
489
    @reset_token, enc = Devise.token_generator.generate(self.class, :reset_password_token)
490 491 492 493

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

M
Marin Jankovski 已提交
494
    @reset_token
495 496
  end

497 498 499 500
  def recently_sent_password_reset?
    reset_password_sent_at.present? && reset_password_sent_at >= 1.minute.ago
  end

T
Toon Claes 已提交
501 502 503 504 505 506 507 508
  def remember_me!
    super if ::Gitlab::Database.read_write?
  end

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

R
Robert Speicher 已提交
509
  def disable_two_factor!
510
    transaction do
L
Lin Jen-Shin 已提交
511
      update(
512 513 514 515 516 517 518
        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
      )
519
      self.u2f_registrations.destroy_all # rubocop: disable DestroyAll
520 521 522 523 524 525 526 527
    end
  end

  def two_factor_enabled?
    two_factor_otp_enabled? || two_factor_u2f_enabled?
  end

  def two_factor_otp_enabled?
528
    otp_required_for_login?
529 530 531
  end

  def two_factor_u2f_enabled?
532 533 534 535 536
    if u2f_registrations.loaded?
      u2f_registrations.any?
    else
      u2f_registrations.exists?
    end
R
Robert Speicher 已提交
537 538
  end

539 540 541 542 543 544
  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

545
  def unique_email
546 547
    if !emails.exists?(email: email) && Email.exists?(email: email)
      errors.add(:email, 'has already been taken')
548
    end
549 550
  end

551
  def owns_notification_email
552
    return if temp_oauth_email?
553

554
    errors.add(:notification_email, "is not an email you own") unless all_emails.include?(notification_email)
555 556
  end

557
  def owns_public_email
558
    return if public_email.blank?
559

560
    errors.add(:public_email, "is not an email you own") unless all_emails.include?(public_email)
561 562
  end

A
Alexandra 已提交
563 564 565 566 567
  # 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

568
  # Note: the use of the Emails services will cause `saves` on the user object, running
569
  # through the callbacks again and can have side effects, such as the `previous_changes`
570 571 572
  # 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
573
  def update_emails_with_primary_email(previous_email)
574
    primary_email_record = emails.find_by(email: email)
575
    Emails::DestroyService.new(self, user: self).execute(primary_email_record) if primary_email_record
576

577 578
    # 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
579
    Emails::CreateService.new(self, user: self, email: previous_email).execute(confirmed_at: confirmed_at)
580 581
  end

582
  def update_invalid_gpg_signatures
583
    gpg_keys.each(&:update_invalid_gpg_signatures)
584 585
  end

586
  # Returns the groups a user has access to, either through a membership or a project authorization
587
  def authorized_groups
588 589
    union = Gitlab::SQL::Union
      .new([groups.select(:id), authorized_projects.select(:namespace_id)])
590

591
    Group.where("namespaces.id IN (#{union.to_sql})") # rubocop:disable GitlabSecurity/SqlInjection
592 593
  end

594 595 596 597 598
  # 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

599 600
  # Returns a relation of groups the user has access to, including their parent
  # and child groups (recursively).
601
  def all_expanded_groups
602
    Gitlab::GroupHierarchy.new(groups).all_groups
603 604 605 606 607 608
  end

  def expanded_groups_requiring_two_factor_authentication
    all_expanded_groups.where(require_two_factor_authentication: true)
  end

609
  def refresh_authorized_projects
610 611 612 613
    Users::RefreshAuthorizedProjectsService.new(self).execute
  end

  def remove_project_authorizations(project_ids)
614
    project_authorizations.where(project_id: project_ids).delete_all
615 616
  end

617
  def authorized_projects(min_access_level = nil)
618 619
    # We're overriding an association, so explicitly call super with no
    # arguments or it would be passed as `force_reload` to the association
620
    projects = super()
621 622

    if min_access_level
623 624
      projects = projects
        .where('project_authorizations.access_level >= ?', min_access_level)
625
    end
626 627 628 629 630 631

    projects
  end

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

634 635 636 637 638 639 640 641 642
  # Typically used in conjunction with projects table to get projects
  # a user has been given access to.
  #
  # Example use:
  # `Project.where('EXISTS(?)', user.authorizations_for_projects)`
  def authorizations_for_projects
    project_authorizations.select(1).where('project_authorizations.project_id = projects.id')
  end

643 644 645 646 647 648 649 650 651 652
  # 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

653
  def owned_projects
654
    @owned_projects ||= Project.from("(#{owned_projects_union.to_sql}) AS projects")
655 656
  end

657 658 659 660
  # 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 已提交
661
    authorized_projects(Gitlab::Access::REPORTER).non_archived.with_issues_enabled
662 663
  end

D
Dmitriy Zaporozhets 已提交
664
  def require_ssh_key?
Y
Yorick Peterse 已提交
665 666 667
    count = Users::KeysCountService.new(self).count

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

670 671 672 673 674 675
  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?
676 677
  end

678
  def require_personal_access_token_creation_for_git_auth?
679
    return false if allow_password_authentication_for_git? || ldap_user?
680 681

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

684 685 686 687
  def require_extra_setup_for_git_auth?
    require_password_creation_for_git? || require_personal_access_token_creation_for_git_auth?
  end

688
  def allow_password_authentication?
689 690 691 692
    allow_password_authentication_for_web? || allow_password_authentication_for_git?
  end

  def allow_password_authentication_for_web?
693
    Gitlab::CurrentSettings.password_authentication_enabled_for_web? && !ldap_user?
694 695 696
  end

  def allow_password_authentication_for_git?
697
    Gitlab::CurrentSettings.password_authentication_enabled_for_git? && !ldap_user?
698 699
  end

700
  def can_change_username?
701
    gitlab_config.username_changing_enabled
702 703
  end

D
Dmitriy Zaporozhets 已提交
704
  def can_create_project?
705
    projects_limit_left > 0
D
Dmitriy Zaporozhets 已提交
706 707 708
  end

  def can_create_group?
709
    can?(:create_group)
D
Dmitriy Zaporozhets 已提交
710 711
  end

712 713 714 715
  def can_select_namespace?
    several_namespaces? || admin
  end

716
  def can?(action, subject = :global)
H
http://jneen.net/ 已提交
717
    Ability.allowed?(self, action, subject)
D
Dmitriy Zaporozhets 已提交
718 719
  end

720 721 722 723
  def confirm_deletion_with_password?
    !password_automatically_set? && allow_password_authentication?
  end

D
Dmitriy Zaporozhets 已提交
724 725 726 727
  def first_name
    name.split.first unless name.blank?
  end

728
  def projects_limit_left
729 730 731
    projects_limit - personal_projects_count
  end

732 733
  def recent_push(project = nil)
    service = Users::LastPushEventService.new(self)
D
Dmitriy Zaporozhets 已提交
734

735 736 737 738
    if project
      service.last_event_for_project(project)
    else
      service.last_event_for_user
739
    end
D
Dmitriy Zaporozhets 已提交
740 741 742
  end

  def several_namespaces?
743
    owned_groups.any? || maintainers_groups.any?
D
Dmitriy Zaporozhets 已提交
744 745 746 747 748
  end

  def namespace_id
    namespace.try :id
  end
749

750 751 752
  def name_with_username
    "#{name} (#{username})"
  end
D
Dmitriy Zaporozhets 已提交
753

754
  def already_forked?(project)
755 756 757
    !!fork_of(project)
  end

758
  def fork_of(project)
759
    namespace.find_fork_of(project)
760
  end
761 762

  def ldap_user?
763
    if identities.loaded?
764
      identities.find { |identity| Gitlab::Auth::OAuth::Provider.ldap_provider?(identity.provider) && !identity.extern_uid.nil? }
765 766 767
    else
      identities.exists?(["provider LIKE ? AND extern_uid IS NOT NULL", "ldap%"])
    end
768 769 770 771
  end

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

774
  def project_deploy_keys
775
    DeployKey.unscoped.in_projects(authorized_projects.pluck(:id)).distinct(:id)
776 777
  end

778
  def accessible_deploy_keys
779 780 781 782 783
    @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
784
  end
785 786

  def created_by
S
skv 已提交
787
    User.find_by(id: created_by_id) if created_by_id
788
  end
789 790

  def sanitize_attrs
791 792 793
    %i[skype linkedin twitter].each do |attr|
      value = self[attr]
      self[attr] = Sanitize.clean(value) if value.present?
794 795
    end
  end
796

797
  def set_notification_email
798
    if notification_email.blank? || all_emails.exclude?(notification_email)
799
      self.notification_email = email
800 801 802
    end
  end

803
  def set_public_email
804
    if public_email.blank? || all_emails.exclude?(public_email)
805
      self.public_email = ''
806 807 808
    end
  end

809
  def update_secondary_emails!
810 811 812
    set_notification_email
    set_public_email
    save if notification_email_changed? || public_email_changed?
813 814
  end

815
  def set_projects_limit
816 817 818
    # `User.select(:id)` raises
    # `ActiveModel::MissingAttributeError: missing attribute: projects_limit`
    # without this safeguard!
819
    return unless has_attribute?(:projects_limit) && projects_limit.nil?
820

821
    self.projects_limit = Gitlab::CurrentSettings.default_projects_limit
822 823
  end

824
  def requires_ldap_check?
825 826 827
    if !Gitlab.config.ldap.enabled
      false
    elsif ldap_user?
828 829 830 831 832 833
      !last_credential_check_at || (last_credential_check_at + 1.hour) < Time.now
    else
      false
    end
  end

J
Jacob Vosmaer 已提交
834 835 836 837 838 839 840
  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

841 842 843 844 845
  def solo_owned_groups
    @solo_owned_groups ||= owned_groups.select do |group|
      group.owners == [self]
    end
  end
846 847

  def with_defaults
848
    User.defaults.each do |k, v|
849
      public_send("#{k}=", v) # rubocop:disable GitlabSecurity/PublicSend
850
    end
851 852

    self
853
  end
854

855 856 857 858
  def can_leave_project?(project)
    project.namespace != namespace &&
      project.project_member(self)
  end
859

J
Jerome Dalbert 已提交
860
  def full_website_url
861
    return "http://#{website_url}" if website_url !~ %r{\Ahttps?://}
J
Jerome Dalbert 已提交
862 863 864 865 866

    website_url
  end

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

870
  def all_ssh_keys
871
    keys.map(&:publishable_key)
872
  end
873 874

  def temp_oauth_email?
875
    email.start_with?('temp-email-for-oauth')
876 877
  end

878
  def avatar_url(size: nil, scale: 2, **args)
879
    GravatarService.new.execute(email, size, scale, username: username)
880
  end
D
Dmitriy Zaporozhets 已提交
881

882 883 884 885
  def primary_email_verified?
    confirmed? && !temp_oauth_email?
  end

886 887 888 889 890 891 892 893 894 895
  def accept_pending_invitations!
    pending_invitations.select do |member|
      member.accept_invite!(self)
    end
  end

  def pending_invitations
    Member.where(invite_email: verified_emails).invite
  end

896
  def all_emails
897
    all_emails = []
898 899
    all_emails << email unless temp_oauth_email?
    all_emails.concat(emails.map(&:email))
900
    all_emails
901 902
  end

903
  def verified_emails
904
    verified_emails = []
905
    verified_emails << email if primary_email_verified?
906
    verified_emails.concat(emails.confirmed.pluck(:email))
907 908 909
    verified_emails
  end

910
  def verified_email?(check_email)
911
    downcased = check_email.downcase
A
Alexandra 已提交
912
    email == downcased ? primary_email_verified? : emails.confirmed.where(email: downcased).exists?
913 914
  end

K
Kirill Zaitsev 已提交
915 916 917 918
  def hook_attrs
    {
      name: name,
      username: username,
919
      avatar_url: avatar_url(only_path: false)
K
Kirill Zaitsev 已提交
920 921 922
    }
  end

D
Dmitriy Zaporozhets 已提交
923
  def ensure_namespace_correct
924 925 926 927
    if namespace
      namespace.path = namespace.name = username if username_changed?
    else
      build_namespace(path: username, name: username)
D
Dmitriy Zaporozhets 已提交
928 929 930
    end
  end

931 932 933 934 935
  def set_username_errors
    namespace_path_errors = self.errors.delete(:"namespace.path")
    self.errors[:username].concat(namespace_path_errors) if namespace_path_errors
  end

936 937 938 939
  def username_changed_hook
    system_hook_service.execute_hooks_for(self, :rename)
  end

D
Dmitriy Zaporozhets 已提交
940
  def post_destroy_hook
941
    log_info("User \"#{name}\" (#{email})  was removed")
D
Douwe Maan 已提交
942

D
Dmitriy Zaporozhets 已提交
943 944 945
    system_hook_service.execute_hooks_for(self, :destroy)
  end

Y
Yorick Peterse 已提交
946 947 948 949
  def remove_key_cache
    Users::KeysCountService.new(self).delete_cache
  end

N
Nick Thomas 已提交
950 951
  def delete_async(deleted_by:, params: {})
    block if params[:hard_delete]
952
    DeleteUserWorker.perform_async(deleted_by.id, id, params.to_h)
N
Nick Thomas 已提交
953 954
  end

D
Dmitriy Zaporozhets 已提交
955
  def notification_service
D
Dmitriy Zaporozhets 已提交
956 957 958
    NotificationService.new
  end

959
  def log_info(message)
D
Dmitriy Zaporozhets 已提交
960 961 962 963 964 965
    Gitlab::AppLogger.info message
  end

  def system_hook_service
    SystemHooksService.new
  end
C
Ciro Santilli 已提交
966 967

  def starred?(project)
V
Valery Sizov 已提交
968
    starred_projects.exists?(project.id)
C
Ciro Santilli 已提交
969 970 971
  end

  def toggle_star(project)
972
    UsersStarProject.transaction do
973 974
      user_star_project = users_star_projects
          .where(project: project, user: self).lock(true).first
975 976 977 978 979 980

      if user_star_project
        user_star_project.destroy
      else
        UsersStarProject.create!(project: project, user: self)
      end
C
Ciro Santilli 已提交
981 982
    end
  end
983 984

  def manageable_namespaces
985 986 987 988
    @manageable_namespaces ||= [namespace] + manageable_groups
  end

  def manageable_groups
989
    Gitlab::GroupHierarchy.new(owned_or_maintainers_groups).base_and_descendants
990
  end
D
Dmitriy Zaporozhets 已提交
991

992 993 994 995 996 997
  def namespaces
    namespace_ids = groups.pluck(:id)
    namespace_ids.push(namespace.id)
    Namespace.where(id: namespace_ids)
  end

D
Dmitriy Zaporozhets 已提交
998
  def oauth_authorized_tokens
999
    Doorkeeper::AccessToken.where(resource_owner_id: id, revoked_at: nil)
D
Dmitriy Zaporozhets 已提交
1000
  end
1001

1002 1003 1004 1005 1006 1007 1008 1009 1010
  # 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
1011
  # ms on a database with a similar size to GitLab.com's database. On the other
1012 1013
  # hand, using a subquery means we can get the exact same data in about 40 ms.
  def contributed_projects
1014 1015 1016 1017 1018
    events = Event.select(:project_id)
      .contributions.where(author_id: self)
      .where("created_at > ?", Time.now - 1.year)
      .uniq
      .reorder(nil)
1019 1020

    Project.where(id: events)
1021
  end
1022

1023 1024 1025
  def can_be_removed?
    !solo_owned_groups.present?
  end
1026

1027 1028
  def ci_owned_runners
    @ci_owned_runners ||= begin
1029
      project_runner_ids = Ci::RunnerProject
1030
        .where(project: authorized_projects(Gitlab::Access::MAINTAINER))
1031
        .select(:runner_id)
1032 1033

      group_runner_ids = Ci::RunnerNamespace
1034
        .where(namespace_id: owned_or_maintainers_groups.select(:id))
1035 1036 1037 1038
        .select(:runner_id)

      union = Gitlab::SQL::Union.new([project_runner_ids, group_runner_ids])

1039
      Ci::Runner.where("ci_runners.id IN (#{union.to_sql})") # rubocop:disable GitlabSecurity/SqlInjection
K
Kamil Trzcinski 已提交
1040
    end
1041
  end
1042

1043
  def notification_settings_for(source)
1044
    if notification_settings.loaded?
1045 1046 1047 1048
      notification_settings.find do |notification|
        notification.source_type == source.class.base_class.name &&
          notification.source_id == source.id
      end
1049 1050 1051
    else
      notification_settings.find_or_initialize_by(source: source)
    end
1052 1053
  end

1054 1055 1056
  # Lazy load global notification setting
  # Initializes User setting with Participating level if setting not persisted
  def global_notification_setting
1057 1058 1059
    return @global_notification_setting if defined?(@global_notification_setting)

    @global_notification_setting = notification_settings.find_or_initialize_by(source: nil)
L
Lin Jen-Shin 已提交
1060
    @global_notification_setting.update(level: NotificationSetting.levels[DEFAULT_NOTIFICATION_LEVEL]) unless @global_notification_setting.persisted?
1061 1062

    @global_notification_setting
1063 1064
  end

1065
  def assigned_open_merge_requests_count(force: false)
1066
    Rails.cache.fetch(['users', id, 'assigned_open_merge_requests_count'], force: force, expires_in: 20.minutes) do
1067
      MergeRequestsFinder.new(self, assignee_id: self.id, state: 'opened').execute.count
1068 1069 1070
    end
  end

J
Josh Frye 已提交
1071
  def assigned_open_issues_count(force: false)
1072
    Rails.cache.fetch(['users', id, 'assigned_open_issues_count'], force: force, expires_in: 20.minutes) do
1073
      IssuesFinder.new(self, assignee_id: self.id, state: 'opened').execute.count
1074
    end
1075 1076
  end

1077 1078 1079 1080 1081 1082 1083 1084 1085 1086 1087 1088
  def todos_done_count(force: false)
    Rails.cache.fetch(['users', id, 'todos_done_count'], force: force, expires_in: 20.minutes) do
      TodosFinder.new(self, state: :done).execute.count
    end
  end

  def todos_pending_count(force: false)
    Rails.cache.fetch(['users', id, 'todos_pending_count'], force: force, expires_in: 20.minutes) do
      TodosFinder.new(self, state: :pending).execute.count
    end
  end

A
Andreas Brandl 已提交
1089 1090 1091 1092 1093 1094
  def personal_projects_count(force: false)
    Rails.cache.fetch(['users', id, 'personal_projects_count'], force: force, expires_in: 24.hours, raw: true) do
      personal_projects.count
    end.to_i
  end

1095 1096 1097 1098 1099
  def update_todos_count_cache
    todos_done_count(force: true)
    todos_pending_count(force: true)
  end

1100
  def invalidate_cache_counts
1101 1102
    invalidate_issue_cache_counts
    invalidate_merge_request_cache_counts
1103 1104
    invalidate_todos_done_count
    invalidate_todos_pending_count
A
Andreas Brandl 已提交
1105
    invalidate_personal_projects_count
1106 1107 1108
  end

  def invalidate_issue_cache_counts
1109 1110 1111
    Rails.cache.delete(['users', id, 'assigned_open_issues_count'])
  end

1112 1113 1114 1115
  def invalidate_merge_request_cache_counts
    Rails.cache.delete(['users', id, 'assigned_open_merge_requests_count'])
  end

1116 1117
  def invalidate_todos_done_count
    Rails.cache.delete(['users', id, 'todos_done_count'])
P
Paco Guzman 已提交
1118 1119
  end

1120 1121
  def invalidate_todos_pending_count
    Rails.cache.delete(['users', id, 'todos_pending_count'])
P
Paco Guzman 已提交
1122 1123
  end

A
Andreas Brandl 已提交
1124 1125 1126 1127
  def invalidate_personal_projects_count
    Rails.cache.delete(['users', id, 'personal_projects_count'])
  end

1128 1129 1130 1131 1132 1133 1134
  # 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!
1135 1136
    return if ::Gitlab::Database.read_only?

1137 1138
    self.failed_attempts ||= 0
    self.failed_attempts += 1
1139

1140 1141 1142
    if attempts_exceeded?
      lock_access! unless access_locked?
    else
J
James Lopez 已提交
1143
      Users::UpdateService.new(self, user: self).execute(validate: false)
1144 1145 1146
    end
  end

1147 1148 1149 1150 1151 1152 1153 1154 1155
  def access_level
    if admin?
      :admin
    else
      :regular
    end
  end

  def access_level=(new_level)
D
Douwe Maan 已提交
1156 1157
    new_level = new_level.to_s
    return unless %w(admin regular).include?(new_level)
1158

D
Douwe Maan 已提交
1159 1160
    self.admin = (new_level == 'admin')
  end
1161

1162 1163 1164 1165 1166 1167
  # Does the user have access to all private groups & projects?
  # Overridden in EE to also check auditor?
  def full_private_access?
    admin?
  end

1168
  def update_two_factor_requirement
1169
    periods = expanded_groups_requiring_two_factor_authentication.pluck(:two_factor_grace_period)
1170

1171
    self.require_two_factor_authentication_from_group = periods.any?
1172 1173 1174 1175 1176
    self.two_factor_grace_period = periods.min || User.column_defaults['two_factor_grace_period']

    save
  end

1177
  # each existing user needs to have an `feed_token`.
A
Alexis Reigel 已提交
1178 1179
  # we do this on read since migrating all existing users is not a feasible
  # solution.
1180 1181
  def feed_token
    ensure_feed_token!
A
Alexis Reigel 已提交
1182 1183
  end

1184 1185 1186 1187 1188 1189 1190 1191 1192 1193 1194 1195 1196 1197 1198 1199
  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 已提交
1200 1201
  # override, from Devise
  def lock_access!
1202
    Gitlab::AppLogger.info("Account Locked: username=#{username}")
B
Brian Neel 已提交
1203 1204 1205
    super
  end

1206 1207 1208 1209 1210 1211 1212 1213 1214 1215 1216 1217 1218 1219 1220 1221 1222 1223 1224 1225 1226 1227 1228 1229 1230 1231 1232 1233
  # 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

1234 1235 1236 1237
  def terms_accepted?
    accepted_term_id.present?
  end

1238 1239 1240 1241 1242
  def required_terms_not_accepted?
    Gitlab::CurrentSettings.current_application_settings.enforce_terms? &&
      !terms_accepted?
  end

1243 1244 1245
  # @deprecated
  alias_method :owned_or_masters_groups, :owned_or_maintainers_groups

1246 1247 1248 1249 1250
  protected

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

1252 1253 1254
    super
  end

1255 1256
  private

1257 1258 1259 1260 1261 1262 1263 1264 1265
  def owned_projects_union
    Gitlab::SQL::Union.new([
      Project.where(namespace: namespace),
      Project.joins(:project_authorizations)
        .where("projects.namespace_id <> ?", namespace.id)
        .where(project_authorizations: { user_id: id, access_level: Gitlab::Access::OWNER })
    ], remove_duplicates: false)
  end

V
Valery Sizov 已提交
1266 1267
  # Added according to https://github.com/plataformatec/devise/blob/7df57d5081f9884849ca15e4fde179ef164a575f/README.md#activejob-integration
  def send_devise_notification(notification, *args)
1268
    return true unless can?(:receive_notifications)
1269

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

1273 1274 1275 1276 1277 1278 1279 1280 1281
  # 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

1282 1283 1284 1285 1286
  def ensure_user_rights_and_limits
    if external?
      self.can_create_group = false
      self.projects_limit   = 0
    else
1287 1288
      # 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?
1289
      self.projects_limit = Gitlab::CurrentSettings.default_projects_limit unless projects_limit_changed?
1290
    end
Z
Zeger-Jan van de Weg 已提交
1291
  end
1292

1293 1294 1295 1296
  def signup_domain_valid?
    valid = true
    error = nil

1297 1298
    if Gitlab::CurrentSettings.domain_blacklist_enabled?
      blocked_domains = Gitlab::CurrentSettings.domain_blacklist
1299
      if domain_matches?(blocked_domains, email)
1300 1301 1302 1303 1304
        error = 'is not from an allowed domain.'
        valid = false
      end
    end

1305
    allowed_domains = Gitlab::CurrentSettings.domain_whitelist
1306
    unless allowed_domains.blank?
1307
      if domain_matches?(allowed_domains, email)
1308 1309
        valid = true
      else
D
dev-chris 已提交
1310
        error = "domain is not authorized for sign-up"
1311 1312 1313 1314
        valid = false
      end
    end

1315
    errors.add(:email, error) unless valid
1316 1317 1318

    valid
  end
1319

1320
  def domain_matches?(email_domains, email)
1321 1322 1323 1324 1325 1326 1327
    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
1328 1329 1330 1331

  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.
1332
      SecureRandom.hex.to_i(16).to_s(36)
1333 1334 1335 1336
    else
      super
    end
  end
1337

1338 1339
  def self.unique_internal(scope, username, email_pattern, &block)
    scope.first || create_unique_internal(scope, username, email_pattern, &block)
1340 1341 1342 1343
  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
1344
    # exclusive lease to ensure than this block is never run concurrently.
1345
    lease_key = "user:unique_internal:#{username}"
1346 1347 1348 1349 1350 1351 1352 1353
    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

1354
    # Recheck if the user is already present. One might have been
1355 1356
    # added between the time we last checked (first line of this method)
    # and the time we acquired the lock.
1357 1358
    existing_user = uncached { scope.first }
    return existing_user if existing_user.present?
1359 1360 1361

    uniquify = Uniquify.new

1362
    username = uniquify.string(username) { |s| User.find_by_username(s) }
1363

1364
    email = uniquify.string(-> (n) { Kernel.sprintf(email_pattern, n) }) do |s|
1365 1366 1367
      User.find_by_email(s)
    end

1368
    user = scope.build(
1369 1370 1371
      username: username,
      email: email,
      &creation_block
1372
    )
J
James Lopez 已提交
1373

J
James Lopez 已提交
1374
    Users::UpdateService.new(user, user: user).execute(validate: false)
1375
    user
1376 1377 1378
  ensure
    Gitlab::ExclusiveLease.cancel(lease_key, uuid)
  end
G
gitlabhq 已提交
1379
end