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

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

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

22 23
  DEFAULT_NOTIFICATION_LEVEL = :participating

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

28
  add_authentication_token_field :incoming_email_token
29
  add_authentication_token_field :rss_token
30

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

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

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

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

54
  devise :lockable, :recoverable, :rememberable, :trackable,
55 56 57 58
         :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 已提交
59

60 61
  # Override Devise::Models::Trackable#update_tracked_fields!
  # to limit database writes to at most once every hour
62
  def update_tracked_fields!(request)
63 64
    return if Gitlab::Database.read_only?

65 66
    update_tracked_fields(request)

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

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

73
  attr_accessor :force_random_password
G
gitlabhq 已提交
74

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

78 79 80 81
  #
  # Relations
  #

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

  # Profile
86 87
  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
88
  has_many :gpg_keys
89

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

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

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

115
  has_many :user_interacted_projects
116
  has_many :project_interactions, through: :user_interacted_projects, source: :project, class_name: 'Project'
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 :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'
M
Matija Čupić 已提交
140
  has_many :callouts, class_name: 'UserCallout'
141 142
  has_many :term_agreements
  belongs_to :accepted_term, class_name: 'ApplicationSetting::Term'
143

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

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

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

167
  before_validation :sanitize_attrs
168
  before_validation :set_notification_email, if: :new_record?
T
Tiago Botelho 已提交
169
  before_validation :set_public_email, if: :public_email_changed?
170
  before_save :set_public_email, if: :public_email_changed? # in case validation is skipped
D
Douwe Maan 已提交
171
  before_save :ensure_incoming_email_token
172
  before_save :ensure_user_rights_and_limits, if: ->(user) { user.new_record? || user.external_changed? }
173
  before_save :skip_reconfirmation!, if: ->(user) { user.email_changed? && user.read_only_attribute?(:email) }
A
Alexandra 已提交
174
  before_save :check_for_verified_email, if: ->(user) { user.email_changed? && !user.new_record? }
175
  before_validation :ensure_namespace_correct
176
  before_save :ensure_namespace_correct # in case validation is skipped
177
  after_validation :set_username_errors
178
  after_update :username_changed_hook, if: :username_changed?
179
  after_destroy :post_destroy_hook
Y
Yorick Peterse 已提交
180
  after_destroy :remove_key_cache
181 182 183 184 185 186 187 188 189 190 191 192 193 194 195
  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
196

197
  after_initialize :set_projects_limit
D
Dmitriy Zaporozhets 已提交
198

199
  # User's Layout preference
200
  enum layout: [:fixed, :fluid]
201

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

206
  # User's Project preference
207 208 209
  # Note: When adding an option, it MUST go on the end of the array.
  enum project_view: [:readme, :activity, :files]

210
  delegate :path, to: :namespace, allow_nil: true, prefix: true
211

212 213 214
  state_machine :state, initial: :active do
    event :block do
      transition active: :blocked
215
      transition ldap_blocked: :blocked
216 217
    end

218 219 220 221
    event :ldap_block do
      transition active: :ldap_blocked
    end

222 223
    event :activate do
      transition blocked: :active
224
      transition ldap_blocked: :active
225
    end
226 227 228 229 230

    state :blocked, :ldap_blocked do
      def blocked?
        true
      end
231 232 233 234 235 236

      def active_for_authentication?
        false
      end

      def inactive_message
237
        BLOCKED_MESSAGE
238
      end
239
    end
240 241
  end

A
Andrey Kumanyaev 已提交
242
  # Scopes
243
  scope :admins, -> { where(admin: true) }
244
  scope :blocked, -> { with_states(:blocked, :ldap_blocked) }
245
  scope :external, -> { where(external: true) }
J
James Lopez 已提交
246
  scope :active, -> { with_state(:active).non_internal }
B
Ben Bodenmiller 已提交
247
  scope :without_projects, -> { where('id NOT IN (SELECT DISTINCT(user_id) FROM members WHERE user_id IS NOT NULL AND requested_at IS NULL)') }
V
Valery Sizov 已提交
248
  scope :todo_authors, ->(user_id, state) { where(id: Todo.where(user_id: user_id, state: state).select(:author_id)) }
249 250
  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')) }
251

252
  def self.with_two_factor_indistinct
253
    joins("LEFT OUTER JOIN u2f_registrations AS u2f ON u2f.user_id = users.id")
254 255 256 257 258
      .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])
259 260 261
  end

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

266 267 268
  #
  # Class methods
  #
A
Andrey Kumanyaev 已提交
269
  class << self
270
    # Devise method overridden to allow sign in with email or username
271 272 273
    def find_for_database_authentication(warden_conditions)
      conditions = warden_conditions.dup
      if login = conditions.delete(:login)
274
        where(conditions).find_by("lower(username) = :value OR lower(email) = :value", value: login.downcase.strip)
275
      else
G
Gabriel Mazetto 已提交
276
        find_by(conditions)
277 278
      end
    end
279

280
    def sort_by_attribute(method)
281 282 283
      order_method = method || 'id_desc'

      case order_method.to_s
284 285
      when 'recent_sign_in' then order_recent_sign_in
      when 'oldest_sign_in' then order_oldest_sign_in
286
      else
287
        order_by(order_method)
V
Valery Sizov 已提交
288 289 290
      end
    end

291
    def for_github_id(id)
292
      joins(:identities).merge(Identity.with_extern_uid(:github, id))
293 294
    end

295 296
    # Find a User by their primary email or any associated secondary email
    def find_by_any_email(email)
Y
Yorick Peterse 已提交
297 298 299 300 301 302 303 304 305 306
      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}")
307
    end
308

309
    def filter(filter_name)
A
Andrey Kumanyaev 已提交
310
      case filter_name
311
      when 'admins'
312
        admins
313
      when 'blocked'
314
        blocked
315
      when 'two_factor_disabled'
316
        without_two_factor
317
      when 'two_factor_enabled'
318
        with_two_factor
319
      when 'wop'
320
        without_projects
321
      when 'external'
322
        external
A
Andrey Kumanyaev 已提交
323
      else
324
        active
A
Andrey Kumanyaev 已提交
325
      end
326 327
    end

328 329 330 331 332 333 334
    # 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.
335
    def search(query)
336 337
      return none if query.blank?

338 339
      query = query.downcase

340 341 342 343 344 345 346 347 348
      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

349
      where(
350 351
        fuzzy_arel_match(:name, query, lower_exact_match: true)
          .or(fuzzy_arel_match(:username, query, lower_exact_match: true))
352
          .or(arel_table[:email].eq(query))
353
      ).reorder(order % { query: ActiveRecord::Base.connection.quote(query) }, :name)
A
Andrey Kumanyaev 已提交
354
    end
355

356 357 358 359 360
    # 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)
361 362
      return none if query.blank?

363 364
      query = query.downcase

365
      email_table = Email.arel_table
366 367
      matched_by_emails_user_ids = email_table
        .project(email_table[:user_id])
368
        .where(email_table[:email].eq(query))
369 370

      where(
371 372
        fuzzy_arel_match(:name, query)
          .or(fuzzy_arel_match(:username, query))
373
          .or(arel_table[:email].eq(query))
374
          .or(arel_table[:id].in(matched_by_emails_user_ids))
375 376 377
      )
    end

378
    def by_login(login)
379 380 381 382 383 384 385
      return nil unless login

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

388 389 390 391
    def find_by_username(username)
      iwhere(username: username).take
    end

R
Robert Speicher 已提交
392
    def find_by_username!(username)
393
      iwhere(username: username).take!
R
Robert Speicher 已提交
394 395
    end

T
Timothy Andrew 已提交
396
    def find_by_personal_access_token(token_string)
397 398
      return unless token_string

399
      PersonalAccessTokensFinder.new(state: 'active').find_by(token: token_string)&.user
T
Timothy Andrew 已提交
400 401
    end

Y
Yorick Peterse 已提交
402 403
    # Returns a user for the given SSH key.
    def find_by_ssh_key_id(key_id)
404
      Key.find_by(id: key_id)&.user
Y
Yorick Peterse 已提交
405 406
    end

407
    def find_by_full_path(path, follow_redirects: false)
408 409
      namespace = Namespace.for_user.find_by_full_path(path, follow_redirects: follow_redirects)
      namespace&.owner
410 411
    end

412 413 414
    def reference_prefix
      '@'
    end
415 416 417 418

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

    # Return (create if necessary) the ghost user. The ghost user
    # owns records previously belonging to deleted users.
    def ghost
428 429
      email = 'ghost%s@example.com'
      unique_internal(where(ghost: true), 'ghost', email) do |u|
430 431 432
        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
433
    end
V
vsizov 已提交
434
  end
R
randx 已提交
435

M
Michael Kozono 已提交
436 437 438 439
  def full_path
    username
  end

440 441 442 443
  def self.internal_attributes
    [:ghost]
  end

444
  def internal?
445 446 447 448 449 450 451 452
    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
453
    where(internal_attributes.map { |attr| "#{attr} IS NOT TRUE" }.join(" AND "))
454 455
  end

456 457 458
  #
  # Instance methods
  #
459 460 461 462 463

  def to_param
    username
  end

J
Jarka Kadlecova 已提交
464
  def to_reference(_from = nil, target_project: nil, full: nil)
465 466 467
    "#{self.class.reference_prefix}#{username}"
  end

468 469
  def skip_confirmation=(bool)
    skip_confirmation! if bool
D
Daniel Juarez 已提交
470 471 472 473
  end

  def skip_reconfirmation=(bool)
    skip_reconfirmation! if bool
R
randx 已提交
474
  end
475

476
  def generate_reset_token
M
Marin Jankovski 已提交
477
    @reset_token, enc = Devise.token_generator.generate(self.class, :reset_password_token)
478 479 480 481

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

M
Marin Jankovski 已提交
482
    @reset_token
483 484
  end

485 486 487 488
  def recently_sent_password_reset?
    reset_password_sent_at.present? && reset_password_sent_at >= 1.minute.ago
  end

T
Toon Claes 已提交
489 490 491 492 493 494 495 496
  def remember_me!
    super if ::Gitlab::Database.read_write?
  end

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

R
Robert Speicher 已提交
497
  def disable_two_factor!
498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515
    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?
516
    otp_required_for_login?
517 518 519
  end

  def two_factor_u2f_enabled?
520 521 522 523 524
    if u2f_registrations.loaded?
      u2f_registrations.any?
    else
      u2f_registrations.exists?
    end
R
Robert Speicher 已提交
525 526
  end

527 528 529 530 531 532
  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

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

539
  def owns_notification_email
540
    return if temp_oauth_email?
541

542
    errors.add(:notification_email, "is not an email you own") unless all_emails.include?(notification_email)
543 544
  end

545
  def owns_public_email
546
    return if public_email.blank?
547

548
    errors.add(:public_email, "is not an email you own") unless all_emails.include?(public_email)
549 550
  end

A
Alexandra 已提交
551 552 553 554 555
  # 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

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

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

570
  def update_invalid_gpg_signatures
571
    gpg_keys.each(&:update_invalid_gpg_signatures)
572 573
  end

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

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

582 583 584 585 586
  # 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

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

  def expanded_groups_requiring_two_factor_authentication
    all_expanded_groups.where(require_two_factor_authentication: true)
  end

597
  def refresh_authorized_projects
598 599 600 601
    Users::RefreshAuthorizedProjectsService.new(self).execute
  end

  def remove_project_authorizations(project_ids)
602
    project_authorizations.where(project_id: project_ids).delete_all
603 604
  end

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

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

    projects
  end

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

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

631 632 633 634 635 636 637 638 639 640
  # 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

641
  def owned_projects
642
    @owned_projects ||= Project.from("(#{owned_projects_union.to_sql}) AS projects")
643 644
  end

645 646 647 648
  # 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 已提交
649
    authorized_projects(Gitlab::Access::REPORTER).non_archived.with_issues_enabled
650 651
  end

D
Dmitriy Zaporozhets 已提交
652
  def require_ssh_key?
Y
Yorick Peterse 已提交
653 654 655
    count = Users::KeysCountService.new(self).count

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

658 659 660 661 662 663
  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?
664 665
  end

666
  def require_personal_access_token_creation_for_git_auth?
667
    return false if allow_password_authentication_for_git? || ldap_user?
668 669

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

672 673 674 675
  def require_extra_setup_for_git_auth?
    require_password_creation_for_git? || require_personal_access_token_creation_for_git_auth?
  end

676
  def allow_password_authentication?
677 678 679 680
    allow_password_authentication_for_web? || allow_password_authentication_for_git?
  end

  def allow_password_authentication_for_web?
681
    Gitlab::CurrentSettings.password_authentication_enabled_for_web? && !ldap_user?
682 683 684
  end

  def allow_password_authentication_for_git?
685
    Gitlab::CurrentSettings.password_authentication_enabled_for_git? && !ldap_user?
686 687
  end

688
  def can_change_username?
689
    gitlab_config.username_changing_enabled
690 691
  end

D
Dmitriy Zaporozhets 已提交
692
  def can_create_project?
693
    projects_limit_left > 0
D
Dmitriy Zaporozhets 已提交
694 695 696
  end

  def can_create_group?
697
    can?(:create_group)
D
Dmitriy Zaporozhets 已提交
698 699
  end

700 701 702 703
  def can_select_namespace?
    several_namespaces? || admin
  end

704
  def can?(action, subject = :global)
H
http://jneen.net/ 已提交
705
    Ability.allowed?(self, action, subject)
D
Dmitriy Zaporozhets 已提交
706 707
  end

708 709 710 711
  def confirm_deletion_with_password?
    !password_automatically_set? && allow_password_authentication?
  end

D
Dmitriy Zaporozhets 已提交
712 713 714 715
  def first_name
    name.split.first unless name.blank?
  end

716
  def projects_limit_left
717 718 719
    projects_limit - personal_projects_count
  end

720 721
  def recent_push(project = nil)
    service = Users::LastPushEventService.new(self)
D
Dmitriy Zaporozhets 已提交
722

723 724 725 726
    if project
      service.last_event_for_project(project)
    else
      service.last_event_for_user
727
    end
D
Dmitriy Zaporozhets 已提交
728 729 730
  end

  def several_namespaces?
731
    owned_groups.any? || masters_groups.any?
D
Dmitriy Zaporozhets 已提交
732 733 734 735 736
  end

  def namespace_id
    namespace.try :id
  end
737

738 739 740
  def name_with_username
    "#{name} (#{username})"
  end
D
Dmitriy Zaporozhets 已提交
741

742
  def already_forked?(project)
743 744 745
    !!fork_of(project)
  end

746
  def fork_of(project)
747
    namespace.find_fork_of(project)
748
  end
749 750

  def ldap_user?
751
    if identities.loaded?
752
      identities.find { |identity| Gitlab::Auth::OAuth::Provider.ldap_provider?(identity.provider) && !identity.extern_uid.nil? }
753 754 755
    else
      identities.exists?(["provider LIKE ? AND extern_uid IS NOT NULL", "ldap%"])
    end
756 757 758 759
  end

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

762
  def project_deploy_keys
763
    DeployKey.unscoped.in_projects(authorized_projects.pluck(:id)).distinct(:id)
764 765
  end

766
  def accessible_deploy_keys
767 768 769 770 771
    @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
772
  end
773 774

  def created_by
S
skv 已提交
775
    User.find_by(id: created_by_id) if created_by_id
776
  end
777 778

  def sanitize_attrs
779 780 781
    %i[skype linkedin twitter].each do |attr|
      value = self[attr]
      self[attr] = Sanitize.clean(value) if value.present?
782 783
    end
  end
784

785
  def set_notification_email
786
    if notification_email.blank? || all_emails.exclude?(notification_email)
787
      self.notification_email = email
788 789 790
    end
  end

791
  def set_public_email
792
    if public_email.blank? || all_emails.exclude?(public_email)
793
      self.public_email = ''
794 795 796
    end
  end

797
  def update_secondary_emails!
798 799 800
    set_notification_email
    set_public_email
    save if notification_email_changed? || public_email_changed?
801 802
  end

803
  def set_projects_limit
804 805 806
    # `User.select(:id)` raises
    # `ActiveModel::MissingAttributeError: missing attribute: projects_limit`
    # without this safeguard!
807
    return unless has_attribute?(:projects_limit) && projects_limit.nil?
808

809
    self.projects_limit = Gitlab::CurrentSettings.default_projects_limit
810 811
  end

812
  def requires_ldap_check?
813 814 815
    if !Gitlab.config.ldap.enabled
      false
    elsif ldap_user?
816 817 818 819 820 821
      !last_credential_check_at || (last_credential_check_at + 1.hour) < Time.now
    else
      false
    end
  end

J
Jacob Vosmaer 已提交
822 823 824 825 826 827 828
  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

829 830 831 832 833
  def solo_owned_groups
    @solo_owned_groups ||= owned_groups.select do |group|
      group.owners == [self]
    end
  end
834 835

  def with_defaults
836
    User.defaults.each do |k, v|
837
      public_send("#{k}=", v) # rubocop:disable GitlabSecurity/PublicSend
838
    end
839 840

    self
841
  end
842

843 844 845 846
  def can_leave_project?(project)
    project.namespace != namespace &&
      project.project_member(self)
  end
847

J
Jerome Dalbert 已提交
848
  def full_website_url
849
    return "http://#{website_url}" if website_url !~ %r{\Ahttps?://}
J
Jerome Dalbert 已提交
850 851 852 853 854

    website_url
  end

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

858
  def all_ssh_keys
859
    keys.map(&:publishable_key)
860
  end
861 862

  def temp_oauth_email?
863
    email.start_with?('temp-email-for-oauth')
864 865
  end

866
  def avatar_url(size: nil, scale: 2, **args)
867
    GravatarService.new.execute(email, size, scale, username: username)
868
  end
D
Dmitriy Zaporozhets 已提交
869

870 871 872 873
  def primary_email_verified?
    confirmed? && !temp_oauth_email?
  end

874 875 876 877 878 879 880 881 882 883
  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

884
  def all_emails
885
    all_emails = []
886 887
    all_emails << email unless temp_oauth_email?
    all_emails.concat(emails.map(&:email))
888
    all_emails
889 890
  end

891
  def verified_emails
892
    verified_emails = []
893
    verified_emails << email if primary_email_verified?
894
    verified_emails.concat(emails.confirmed.pluck(:email))
895 896 897
    verified_emails
  end

898
  def verified_email?(check_email)
899
    downcased = check_email.downcase
A
Alexandra 已提交
900
    email == downcased ? primary_email_verified? : emails.confirmed.where(email: downcased).exists?
901 902
  end

K
Kirill Zaitsev 已提交
903 904 905 906
  def hook_attrs
    {
      name: name,
      username: username,
907
      avatar_url: avatar_url(only_path: false)
K
Kirill Zaitsev 已提交
908 909 910
    }
  end

D
Dmitriy Zaporozhets 已提交
911
  def ensure_namespace_correct
912 913 914 915
    if namespace
      namespace.path = namespace.name = username if username_changed?
    else
      build_namespace(path: username, name: username)
D
Dmitriy Zaporozhets 已提交
916 917 918
    end
  end

919 920 921 922 923
  def set_username_errors
    namespace_path_errors = self.errors.delete(:"namespace.path")
    self.errors[:username].concat(namespace_path_errors) if namespace_path_errors
  end

924 925 926 927
  def username_changed_hook
    system_hook_service.execute_hooks_for(self, :rename)
  end

D
Dmitriy Zaporozhets 已提交
928
  def post_destroy_hook
929
    log_info("User \"#{name}\" (#{email})  was removed")
D
Douwe Maan 已提交
930

D
Dmitriy Zaporozhets 已提交
931 932 933
    system_hook_service.execute_hooks_for(self, :destroy)
  end

Y
Yorick Peterse 已提交
934 935 936 937
  def remove_key_cache
    Users::KeysCountService.new(self).delete_cache
  end

N
Nick Thomas 已提交
938 939
  def delete_async(deleted_by:, params: {})
    block if params[:hard_delete]
940
    DeleteUserWorker.perform_async(deleted_by.id, id, params.to_h)
N
Nick Thomas 已提交
941 942
  end

D
Dmitriy Zaporozhets 已提交
943
  def notification_service
D
Dmitriy Zaporozhets 已提交
944 945 946
    NotificationService.new
  end

947
  def log_info(message)
D
Dmitriy Zaporozhets 已提交
948 949 950 951 952 953
    Gitlab::AppLogger.info message
  end

  def system_hook_service
    SystemHooksService.new
  end
C
Ciro Santilli 已提交
954 955

  def starred?(project)
V
Valery Sizov 已提交
956
    starred_projects.exists?(project.id)
C
Ciro Santilli 已提交
957 958 959
  end

  def toggle_star(project)
960
    UsersStarProject.transaction do
961 962
      user_star_project = users_star_projects
          .where(project: project, user: self).lock(true).first
963 964 965 966 967 968

      if user_star_project
        user_star_project.destroy
      else
        UsersStarProject.create!(project: project, user: self)
      end
C
Ciro Santilli 已提交
969 970
    end
  end
971 972

  def manageable_namespaces
973 974 975 976
    @manageable_namespaces ||= [namespace] + manageable_groups
  end

  def manageable_groups
977 978 979 980 981 982 983
    union_sql = Gitlab::SQL::Union.new([owned_groups.select(:id), masters_groups.select(:id)]).to_sql

    # Update this line to not use raw SQL when migrated to Rails 5.2.
    # Either ActiveRecord or Arel constructions are fine.
    # This was replaced with the raw SQL construction because of bugs in the arel gem.
    # Bugs were fixed in arel 9.0.0 (Rails 5.2).
    owned_and_master_groups = Group.where("namespaces.id IN (#{union_sql})") # rubocop:disable GitlabSecurity/SqlInjection
984 985

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

988 989 990 991 992 993
  def namespaces
    namespace_ids = groups.pluck(:id)
    namespace_ids.push(namespace.id)
    Namespace.where(id: namespace_ids)
  end

D
Dmitriy Zaporozhets 已提交
994
  def oauth_authorized_tokens
995
    Doorkeeper::AccessToken.where(resource_owner_id: id, revoked_at: nil)
D
Dmitriy Zaporozhets 已提交
996
  end
997

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

    Project.where(id: events)
1017
  end
1018

1019 1020 1021
  def can_be_removed?
    !solo_owned_groups.present?
  end
1022

1023 1024
  def ci_owned_runners
    @ci_owned_runners ||= begin
1025
      project_runner_ids = Ci::RunnerProject
1026
        .where(project: authorized_projects(Gitlab::Access::MASTER))
1027
        .select(:runner_id)
1028 1029 1030 1031 1032 1033 1034 1035

      group_runner_ids = Ci::RunnerNamespace
        .where(namespace_id: owned_or_masters_groups.select(:id))
        .select(:runner_id)

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

      Ci::Runner.specific.where("ci_runners.id IN (#{union.to_sql})") # rubocop:disable GitlabSecurity/SqlInjection
K
Kamil Trzcinski 已提交
1036
    end
1037
  end
1038

1039
  def notification_settings_for(source)
1040 1041 1042 1043 1044
    if notification_settings.loaded?
      notification_settings.find { |notification| notification.source == source }
    else
      notification_settings.find_or_initialize_by(source: source)
    end
1045 1046
  end

1047 1048 1049
  # Lazy load global notification setting
  # Initializes User setting with Participating level if setting not persisted
  def global_notification_setting
1050 1051 1052 1053 1054 1055
    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
1056 1057
  end

1058
  def assigned_open_merge_requests_count(force: false)
1059
    Rails.cache.fetch(['users', id, 'assigned_open_merge_requests_count'], force: force, expires_in: 20.minutes) do
1060
      MergeRequestsFinder.new(self, assignee_id: self.id, state: 'opened').execute.count
1061 1062 1063
    end
  end

J
Josh Frye 已提交
1064
  def assigned_open_issues_count(force: false)
1065
    Rails.cache.fetch(['users', id, 'assigned_open_issues_count'], force: force, expires_in: 20.minutes) do
1066
      IssuesFinder.new(self, assignee_id: self.id, state: 'opened').execute.count
1067
    end
1068 1069
  end

1070 1071 1072 1073 1074 1075 1076 1077 1078 1079 1080 1081
  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 已提交
1082 1083 1084 1085 1086 1087
  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

1088 1089 1090 1091 1092
  def update_todos_count_cache
    todos_done_count(force: true)
    todos_pending_count(force: true)
  end

1093
  def invalidate_cache_counts
1094 1095
    invalidate_issue_cache_counts
    invalidate_merge_request_cache_counts
1096 1097
    invalidate_todos_done_count
    invalidate_todos_pending_count
A
Andreas Brandl 已提交
1098
    invalidate_personal_projects_count
1099 1100 1101
  end

  def invalidate_issue_cache_counts
1102 1103 1104
    Rails.cache.delete(['users', id, 'assigned_open_issues_count'])
  end

1105 1106 1107 1108
  def invalidate_merge_request_cache_counts
    Rails.cache.delete(['users', id, 'assigned_open_merge_requests_count'])
  end

1109 1110
  def invalidate_todos_done_count
    Rails.cache.delete(['users', id, 'todos_done_count'])
P
Paco Guzman 已提交
1111 1112
  end

1113 1114
  def invalidate_todos_pending_count
    Rails.cache.delete(['users', id, 'todos_pending_count'])
P
Paco Guzman 已提交
1115 1116
  end

A
Andreas Brandl 已提交
1117 1118 1119 1120
  def invalidate_personal_projects_count
    Rails.cache.delete(['users', id, 'personal_projects_count'])
  end

1121 1122 1123 1124 1125 1126 1127
  # 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!
1128 1129
    return if ::Gitlab::Database.read_only?

1130 1131
    self.failed_attempts ||= 0
    self.failed_attempts += 1
1132

1133 1134 1135
    if attempts_exceeded?
      lock_access! unless access_locked?
    else
J
James Lopez 已提交
1136
      Users::UpdateService.new(self, user: self).execute(validate: false)
1137 1138 1139
    end
  end

1140 1141 1142 1143 1144 1145 1146 1147 1148
  def access_level
    if admin?
      :admin
    else
      :regular
    end
  end

  def access_level=(new_level)
D
Douwe Maan 已提交
1149 1150
    new_level = new_level.to_s
    return unless %w(admin regular).include?(new_level)
1151

D
Douwe Maan 已提交
1152 1153
    self.admin = (new_level == 'admin')
  end
1154

1155 1156 1157 1158 1159 1160
  # Does the user have access to all private groups & projects?
  # Overridden in EE to also check auditor?
  def full_private_access?
    admin?
  end

1161
  def update_two_factor_requirement
1162
    periods = expanded_groups_requiring_two_factor_authentication.pluck(:two_factor_grace_period)
1163

1164
    self.require_two_factor_authentication_from_group = periods.any?
1165 1166 1167 1168 1169
    self.two_factor_grace_period = periods.min || User.column_defaults['two_factor_grace_period']

    save
  end

A
Alexis Reigel 已提交
1170 1171 1172 1173 1174 1175 1176
  # 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

1177 1178 1179 1180 1181 1182 1183 1184 1185 1186 1187 1188 1189 1190 1191 1192
  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 已提交
1193 1194
  # override, from Devise
  def lock_access!
1195
    Gitlab::AppLogger.info("Account Locked: username=#{username}")
B
Brian Neel 已提交
1196 1197 1198
    super
  end

1199 1200 1201 1202 1203 1204 1205 1206 1207 1208 1209 1210 1211 1212 1213 1214 1215 1216 1217 1218 1219 1220 1221 1222 1223 1224 1225 1226
  # 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

1227 1228 1229 1230
  def terms_accepted?
    accepted_term_id.present?
  end

1231 1232 1233 1234 1235
  def required_terms_not_accepted?
    Gitlab::CurrentSettings.current_application_settings.enforce_terms? &&
      !terms_accepted?
  end

1236 1237 1238 1239 1240
  def owned_or_masters_groups
    union = Gitlab::SQL::Union.new([owned_groups, masters_groups])
    Group.from("(#{union.to_sql}) namespaces")
  end

1241 1242 1243 1244 1245
  protected

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

1247 1248 1249
    super
  end

1250 1251
  private

1252 1253 1254 1255 1256 1257 1258 1259 1260
  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 已提交
1261 1262
  # Added according to https://github.com/plataformatec/devise/blob/7df57d5081f9884849ca15e4fde179ef164a575f/README.md#activejob-integration
  def send_devise_notification(notification, *args)
1263
    return true unless can?(:receive_notifications)
1264

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

1268 1269 1270 1271 1272 1273 1274 1275 1276
  # 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

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

1288 1289 1290 1291
  def signup_domain_valid?
    valid = true
    error = nil

1292 1293
    if Gitlab::CurrentSettings.domain_blacklist_enabled?
      blocked_domains = Gitlab::CurrentSettings.domain_blacklist
1294
      if domain_matches?(blocked_domains, email)
1295 1296 1297 1298 1299
        error = 'is not from an allowed domain.'
        valid = false
      end
    end

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

1310
    errors.add(:email, error) unless valid
1311 1312 1313

    valid
  end
1314

1315
  def domain_matches?(email_domains, email)
1316 1317 1318 1319 1320 1321 1322
    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
1323 1324 1325 1326

  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.
1327
      SecureRandom.hex.to_i(16).to_s(36)
1328 1329 1330 1331
    else
      super
    end
  end
1332

1333 1334 1335 1336 1337 1338
  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
1339
    # exclusive lease to ensure than this block is never run concurrently.
1340
    lease_key = "user:unique_internal:#{username}"
1341 1342 1343 1344 1345 1346 1347 1348
    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

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

    uniquify = Uniquify.new

1357
    username = uniquify.string(username) { |s| User.find_by_username(s) }
1358

1359
    email = uniquify.string(-> (n) { Kernel.sprintf(email_pattern, n) }) do |s|
1360 1361 1362
      User.find_by_email(s)
    end

1363
    user = scope.build(
1364 1365 1366
      username: username,
      email: email,
      &creation_block
1367
    )
J
James Lopez 已提交
1368

J
James Lopez 已提交
1369
    Users::UpdateService.new(user, user: user).execute(validate: false)
1370
    user
1371 1372 1373
  ensure
    Gitlab::ExclusiveLease.cancel(lease_key, uuid)
  end
G
gitlabhq 已提交
1374
end