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

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

  include Gitlab::ConfigHelper
8
  include Gitlab::CurrentSettings
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 IgnorableColumn
20
  include BulkMemberAccessLoad
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) { current_application_settings.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
    :validatable, :omniauthable, :confirmable, :registerable
G
gitlabhq 已提交
56

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

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

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

68
  attr_accessor :force_random_password
G
gitlabhq 已提交
69

70 71 72
  # Virtual attribute for authenticating by either username or email
  attr_accessor :login

73 74 75 76
  #
  # Relations
  #

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

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

88 89 90 91 92
  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
93
  has_one :user_synced_attributes_metadata, autosave: true
94 95

  # Groups
96 97
  has_many :members, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
  has_many :group_members, -> { where(requested_at: nil) }, dependent: :destroy, source: 'GroupMember' # rubocop:disable Cop/ActiveRecordDependent
98
  has_many :groups, through: :group_members
99 100
  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
101

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

113 114 115 116 117 118
  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
119
  has_many :recent_events, -> { order "id DESC" }, foreign_key: :author_id,   class_name: "Event"
120 121 122 123 124 125 126 127 128 129
  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
  has_many :todos,                    dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
  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
130

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

135 136
  has_many :custom_attributes, class_name: 'UserCustomAttribute'

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

T
Tiago Botelho 已提交
155
  validate :namespace_uniq, if: :username_changed?
156 157
  validate :namespace_move_dir_allowed, if: :username_changed?

158
  validate :avatar_type, if: ->(user) { user.avatar.present? && user.avatar_changed? }
T
Tiago Botelho 已提交
159 160 161
  validate :unique_email, if: :email_changed?
  validate :owns_notification_email, if: :notification_email_changed?
  validate :owns_public_email, if: :public_email_changed?
162
  validate :signup_domain_valid?, on: :create, if: ->(user) { !user.created_by_id }
163
  validates :avatar, file_size: { maximum: 200.kilobytes.to_i }
164

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

179
  after_initialize :set_projects_limit
D
Dmitriy Zaporozhets 已提交
180

181
  # User's Layout preference
182
  enum layout: [:fixed, :fluid]
183

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

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

192
  delegate :path, to: :namespace, allow_nil: true, prefix: true
193

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

200 201 202 203
    event :ldap_block do
      transition active: :ldap_blocked
    end

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

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

      def active_for_authentication?
        false
      end

      def inactive_message
        "Your account has been blocked. Please contact your GitLab " \
          "administrator if you think this is an error."
      end
222
    end
223 224
  end

225
  mount_uploader :avatar, AvatarUploader
226
  has_many :uploads, as: :model, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
S
Steven Thonus 已提交
227

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

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

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

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

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

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

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

277 278
    # Find a User by their primary email or any associated secondary email
    def find_by_any_email(email)
Y
Yorick Peterse 已提交
279 280 281 282 283 284 285 286 287 288
      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}")
289
    end
290

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

310 311 312 313 314 315 316
    # 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.
317
    def search(query)
318 319 320 321 322 323 324
<<<<<<< HEAD
=======
      table = arel_table
      query = query.downcase
      pattern = User.to_pattern(query)

>>>>>>> f45fc58d84... Merge branch 'bvl-10-2-email-disclosure' into 'security-10-2'
325 326 327 328 329 330 331 332 333
      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

334
<<<<<<< HEAD
335 336
      fuzzy_search(query, [:name, :email, :username])
        .reorder(order % { query: ActiveRecord::Base.connection.quote(query) }, :name)
337 338 339 340 341 342 343
=======
      where(
        table[:name].matches(pattern)
          .or(table[:email].eq(query))
          .or(table[:username].matches(pattern))
      ).reorder(order % { query: ActiveRecord::Base.connection.quote(query) }, :name)
>>>>>>> f45fc58d84... Merge branch 'bvl-10-2-email-disclosure' into 'security-10-2'
A
Andrey Kumanyaev 已提交
344
    end
345

346 347 348 349 350 351
    # searches user by given pattern
    # it compares name, email, username fields and user's secondary emails with given pattern
    # This method uses ILIKE on PostgreSQL and LIKE on MySQL.

    def search_with_secondary_emails(query)
      email_table = Email.arel_table
352
<<<<<<< HEAD
353 354 355
      matched_by_emails_user_ids = email_table
        .project(email_table[:user_id])
        .where(Email.fuzzy_arel_match(:email, query))
356 357

      where(
358 359 360 361
        fuzzy_arel_match(:name, query)
          .or(fuzzy_arel_match(:email, query))
          .or(fuzzy_arel_match(:username, query))
          .or(arel_table[:id].in(matched_by_emails_user_ids))
362 363 364 365 366 367 368 369 370 371 372
=======
      query = query.downcase
      pattern = User.to_pattern(query)
      matched_by_emails_user_ids = email_table.project(email_table[:user_id]).where(email_table[:email].eq(query))

      where(
        table[:name].matches(pattern)
          .or(table[:email].eq(query))
          .or(table[:username].matches(pattern))
          .or(table[:id].in(matched_by_emails_user_ids))
>>>>>>> f45fc58d84... Merge branch 'bvl-10-2-email-disclosure' into 'security-10-2'
373 374 375
      )
    end

376
    def by_login(login)
377 378 379 380 381 382 383
      return nil unless login

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

386 387 388 389
    def find_by_username(username)
      iwhere(username: username).take
    end

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

T
Timothy Andrew 已提交
394
    def find_by_personal_access_token(token_string)
395 396
      return unless token_string

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

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

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

410 411 412
    def reference_prefix
      '@'
    end
413 414 415 416

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

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

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

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

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

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

  def to_param
    username
  end

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

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

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

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

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

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

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

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

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

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

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

526
  def namespace_uniq
527
    # Return early if username already failed the first uniqueness validation
528
    return if errors.key?(:username) &&
529
        errors[:username].include?('has already been taken')
530

531 532 533
    existing_namespace = Namespace.by_path(username)
    if existing_namespace && existing_namespace != namespace
      errors.add(:username, 'has already been taken')
534 535
    end
  end
536

537 538 539 540 541 542
  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

543
  def avatar_type
544 545
    unless avatar.image?
      errors.add :avatar, "only images allowed"
546 547 548
    end
  end

549
  def unique_email
550 551
    if !emails.exists?(email: email) && Email.exists?(email: email)
      errors.add(:email, 'has already been taken')
552
    end
553 554
  end

555
  def owns_notification_email
556
    return if temp_oauth_email?
557

558
    errors.add(:notification_email, "is not an email you own") unless all_emails.include?(notification_email)
559 560
  end

561
  def owns_public_email
562
    return if public_email.blank?
563

564
    errors.add(:public_email, "is not an email you own") unless all_emails.include?(public_email)
565 566
  end

A
Alexandra 已提交
567 568 569 570 571
  # 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

572
  # Note: the use of the Emails services will cause `saves` on the user object, running
573
  # through the callbacks again and can have side effects, such as the `previous_changes`
574 575 576
  # 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
577
  def update_emails_with_primary_email
578
    previous_email = previous_changes[:email][0]  # grab this before the DestroyService is called
579
    primary_email_record = emails.find_by(email: email)
580
    Emails::DestroyService.new(self, user: self).execute(primary_email_record) if primary_email_record
581

582 583
    # 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
584
    Emails::CreateService.new(self, user: self, email: previous_email).execute(confirmed_at: confirmed_at)
585 586
  end

587
  def update_invalid_gpg_signatures
588
    gpg_keys.each(&:update_invalid_gpg_signatures)
589 590
  end

591 592
  # Returns the groups a user has access to
  def authorized_groups
593 594
    union = Gitlab::SQL::Union
      .new([groups.select(:id), authorized_projects.select(:namespace_id)])
595

596
    Group.where("namespaces.id IN (#{union.to_sql})") # rubocop:disable GitlabSecurity/SqlInjection
597 598
  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 643
  # 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

644
  def owned_projects
645
    @owned_projects ||=
646 647
      Project.where('namespace_id IN (?) OR namespace_id = ?',
                    owned_groups.select(:id), namespace.id).joins(:namespace)
648 649
  end

650 651 652 653
  # 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 已提交
654
    authorized_projects(Gitlab::Access::REPORTER).non_archived.with_issues_enabled
655 656
  end

D
Dmitriy Zaporozhets 已提交
657
  def require_ssh_key?
Y
Yorick Peterse 已提交
658 659 660
    count = Users::KeysCountService.new(self).count

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

663 664 665 666 667 668
  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?
669 670
  end

671
  def require_personal_access_token_creation_for_git_auth?
672
    return false if allow_password_authentication_for_git? || ldap_user?
673 674

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

677 678 679 680
  def require_extra_setup_for_git_auth?
    require_password_creation_for_git? || require_personal_access_token_creation_for_git_auth?
  end

681
  def allow_password_authentication?
682 683 684 685 686 687 688 689 690
    allow_password_authentication_for_web? || allow_password_authentication_for_git?
  end

  def allow_password_authentication_for_web?
    current_application_settings.password_authentication_enabled_for_web? && !ldap_user?
  end

  def allow_password_authentication_for_git?
    current_application_settings.password_authentication_enabled_for_git? && !ldap_user?
691 692
  end

693
  def can_change_username?
694
    gitlab_config.username_changing_enabled
695 696
  end

D
Dmitriy Zaporozhets 已提交
697
  def can_create_project?
698
    projects_limit_left > 0
D
Dmitriy Zaporozhets 已提交
699 700 701
  end

  def can_create_group?
702
    can?(:create_group)
D
Dmitriy Zaporozhets 已提交
703 704
  end

705 706 707 708
  def can_select_namespace?
    several_namespaces? || admin
  end

709
  def can?(action, subject = :global)
H
http://jneen.net/ 已提交
710
    Ability.allowed?(self, action, subject)
D
Dmitriy Zaporozhets 已提交
711 712
  end

713 714 715 716
  def confirm_deletion_with_password?
    !password_automatically_set? && allow_password_authentication?
  end

D
Dmitriy Zaporozhets 已提交
717 718 719 720
  def first_name
    name.split.first unless name.blank?
  end

721
  def projects_limit_left
722 723 724 725 726
    projects_limit - personal_projects_count
  end

  def personal_projects_count
    @personal_projects_count ||= personal_projects.count
727 728
  end

729 730
  def recent_push(project = nil)
    service = Users::LastPushEventService.new(self)
D
Dmitriy Zaporozhets 已提交
731

732 733 734 735
    if project
      service.last_event_for_project(project)
    else
      service.last_event_for_user
736
    end
D
Dmitriy Zaporozhets 已提交
737 738 739
  end

  def several_namespaces?
740
    owned_groups.any? || masters_groups.any?
D
Dmitriy Zaporozhets 已提交
741 742 743 744 745
  end

  def namespace_id
    namespace.try :id
  end
746

747 748 749
  def name_with_username
    "#{name} (#{username})"
  end
D
Dmitriy Zaporozhets 已提交
750

751
  def already_forked?(project)
752 753 754
    !!fork_of(project)
  end

755
  def fork_of(project)
756
    namespace.find_fork_of(project)
757
  end
758 759

  def ldap_user?
760 761 762 763 764
    if identities.loaded?
      identities.find { |identity| identity.provider.start_with?('ldap') && !identity.extern_uid.nil? }
    else
      identities.exists?(["provider LIKE ? AND extern_uid IS NOT NULL", "ldap%"])
    end
765 766 767 768
  end

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

771
  def project_deploy_keys
772
    DeployKey.unscoped.in_projects(authorized_projects.pluck(:id)).distinct(:id)
773 774
  end

775
  def accessible_deploy_keys
776 777 778 779 780
    @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
781
  end
782 783

  def created_by
S
skv 已提交
784
    User.find_by(id: created_by_id) if created_by_id
785
  end
786 787

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

794
  def set_notification_email
795 796
    if notification_email.blank? || !all_emails.include?(notification_email)
      self.notification_email = email
797 798 799
    end
  end

800
  def set_public_email
801
    if public_email.blank? || !all_emails.include?(public_email)
802
      self.public_email = ''
803 804 805
    end
  end

806
  def update_secondary_emails!
807 808 809
    set_notification_email
    set_public_email
    save if notification_email_changed? || public_email_changed?
810 811
  end

812
  def set_projects_limit
813 814 815
    # `User.select(:id)` raises
    # `ActiveModel::MissingAttributeError: missing attribute: projects_limit`
    # without this safeguard!
816
    return unless has_attribute?(:projects_limit)
817

818
    connection_default_value_defined = new_record? && !projects_limit_changed?
819
    return unless projects_limit.nil? || connection_default_value_defined
820 821 822 823

    self.projects_limit = current_application_settings.default_projects_limit
  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 !~ /\Ahttps?:\/\//
J
Jerome Dalbert 已提交
862 863 864 865 866

    website_url
  end

  def short_website_url
867
    website_url.sub(/\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 879 880
  def avatar_url(size: nil, scale: 2, **args)
    # We use avatar_path instead of overriding avatar_url because of carrierwave.
    # See https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/11001/diffs#note_28659864
881
    avatar_path(args) || GravatarService.new.execute(email, size, scale, username: username)
882
  end
D
Dmitriy Zaporozhets 已提交
883

884 885 886 887
  def primary_email_verified?
    confirmed? && !temp_oauth_email?
  end

888
  def all_emails
889
    all_emails = []
890 891
    all_emails << email unless temp_oauth_email?
    all_emails.concat(emails.map(&:email))
892
    all_emails
893 894
  end

895
  def verified_emails
896
    verified_emails = []
897
    verified_emails << email if primary_email_verified?
898
    verified_emails.concat(emails.confirmed.pluck(:email))
899 900 901
    verified_emails
  end

902
  def verified_email?(check_email)
903
    downcased = check_email.downcase
A
Alexandra 已提交
904
    email == downcased ? primary_email_verified? : emails.confirmed.where(email: downcased).exists?
905 906
  end

K
Kirill Zaitsev 已提交
907 908 909 910
  def hook_attrs
    {
      name: name,
      username: username,
911
      avatar_url: avatar_url(only_path: false)
K
Kirill Zaitsev 已提交
912 913 914
    }
  end

D
Dmitriy Zaporozhets 已提交
915 916
  def ensure_namespace_correct
    # Ensure user has namespace
917
    create_namespace!(path: username, name: username) unless namespace
D
Dmitriy Zaporozhets 已提交
918

919
    if username_changed?
920 921 922 923 924 925
      unless namespace.update_attributes(path: username, name: username)
        namespace.errors.each do |attribute, message|
          self.errors.add(:"namespace_#{attribute}", message)
        end
        raise ActiveRecord::RecordInvalid.new(namespace)
      end
D
Dmitriy Zaporozhets 已提交
926 927 928
    end
  end

929 930 931 932
  def username_changed_hook
    system_hook_service.execute_hooks_for(self, :rename)
  end

D
Dmitriy Zaporozhets 已提交
933
  def post_destroy_hook
934
    log_info("User \"#{name}\" (#{email})  was removed")
D
Douwe Maan 已提交
935

D
Dmitriy Zaporozhets 已提交
936 937 938
    system_hook_service.execute_hooks_for(self, :destroy)
  end

Y
Yorick Peterse 已提交
939 940 941 942
  def remove_key_cache
    Users::KeysCountService.new(self).delete_cache
  end

N
Nick Thomas 已提交
943 944 945 946 947
  def delete_async(deleted_by:, params: {})
    block if params[:hard_delete]
    DeleteUserWorker.perform_async(deleted_by.id, id, params)
  end

D
Dmitriy Zaporozhets 已提交
948
  def notification_service
D
Dmitriy Zaporozhets 已提交
949 950 951
    NotificationService.new
  end

952
  def log_info(message)
D
Dmitriy Zaporozhets 已提交
953 954 955 956 957 958
    Gitlab::AppLogger.info message
  end

  def system_hook_service
    SystemHooksService.new
  end
C
Ciro Santilli 已提交
959 960

  def starred?(project)
V
Valery Sizov 已提交
961
    starred_projects.exists?(project.id)
C
Ciro Santilli 已提交
962 963 964
  end

  def toggle_star(project)
965
    UsersStarProject.transaction do
966 967
      user_star_project = users_star_projects
          .where(project: project, user: self).lock(true).first
968 969 970 971 972 973

      if user_star_project
        user_star_project.destroy
      else
        UsersStarProject.create!(project: project, user: self)
      end
C
Ciro Santilli 已提交
974 975
    end
  end
976 977

  def manageable_namespaces
978 979 980 981 982 983 984 985 986 987
    @manageable_namespaces ||= [namespace] + manageable_groups
  end

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

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

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

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

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

    Project.where(id: events)
1019
  end
1020

1021 1022 1023
  def can_be_removed?
    !solo_owned_groups.present?
  end
1024 1025

  def ci_authorized_runners
K
Kamil Trzcinski 已提交
1026
    @ci_authorized_runners ||= begin
1027
      runner_ids = Ci::RunnerProject
1028
        .where("ci_runner_projects.project_id IN (#{ci_projects_union.to_sql})") # rubocop:disable GitlabSecurity/SqlInjection
1029
        .select(:runner_id)
K
Kamil Trzcinski 已提交
1030 1031
      Ci::Runner.specific.where(id: runner_ids)
    end
1032
  end
1033

1034
  def notification_settings_for(source)
1035 1036 1037 1038 1039
    if notification_settings.loaded?
      notification_settings.find { |notification| notification.source == source }
    else
      notification_settings.find_or_initialize_by(source: source)
    end
1040 1041
  end

1042 1043 1044
  # Lazy load global notification setting
  # Initializes User setting with Participating level if setting not persisted
  def global_notification_setting
1045 1046 1047 1048 1049 1050
    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
1051 1052
  end

1053
  def assigned_open_merge_requests_count(force: false)
1054
    Rails.cache.fetch(['users', id, 'assigned_open_merge_requests_count'], force: force, expires_in: 20.minutes) do
1055
      MergeRequestsFinder.new(self, assignee_id: self.id, state: 'opened').execute.count
1056 1057 1058
    end
  end

J
Josh Frye 已提交
1059
  def assigned_open_issues_count(force: false)
1060
    Rails.cache.fetch(['users', id, 'assigned_open_issues_count'], force: force, expires_in: 20.minutes) do
1061
      IssuesFinder.new(self, assignee_id: self.id, state: 'opened').execute.count
1062
    end
1063 1064
  end

J
Josh Frye 已提交
1065
  def update_cache_counts
1066
    assigned_open_merge_requests_count(force: true)
J
Josh Frye 已提交
1067 1068 1069
    assigned_open_issues_count(force: true)
  end

1070
  def invalidate_cache_counts
1071 1072 1073 1074 1075
    invalidate_issue_cache_counts
    invalidate_merge_request_cache_counts
  end

  def invalidate_issue_cache_counts
1076 1077 1078
    Rails.cache.delete(['users', id, 'assigned_open_issues_count'])
  end

1079 1080 1081 1082
  def invalidate_merge_request_cache_counts
    Rails.cache.delete(['users', id, 'assigned_open_merge_requests_count'])
  end

P
Paco Guzman 已提交
1083 1084
  def todos_done_count(force: false)
    Rails.cache.fetch(['users', id, 'todos_done_count'], force: force) do
1085
      TodosFinder.new(self, state: :done).execute.count
P
Paco Guzman 已提交
1086 1087 1088 1089 1090
    end
  end

  def todos_pending_count(force: false)
    Rails.cache.fetch(['users', id, 'todos_pending_count'], force: force) do
1091
      TodosFinder.new(self, state: :pending).execute.count
P
Paco Guzman 已提交
1092 1093 1094 1095 1096 1097 1098 1099
    end
  end

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

1100 1101 1102 1103 1104 1105 1106 1107 1108 1109 1110 1111
  # This is copied from Devise::Models::Lockable#valid_for_authentication?, as our auth
  # flow means we don't call that automatically (and can't conveniently do so).
  #
  # See:
  #   <https://github.com/plataformatec/devise/blob/v4.0.0/lib/devise/models/lockable.rb#L92>
  #
  def increment_failed_attempts!
    self.failed_attempts ||= 0
    self.failed_attempts += 1
    if attempts_exceeded?
      lock_access! unless access_locked?
    else
J
James Lopez 已提交
1112
      Users::UpdateService.new(self, user: self).execute(validate: false)
1113 1114 1115
    end
  end

1116 1117 1118 1119 1120 1121 1122 1123 1124
  def access_level
    if admin?
      :admin
    else
      :regular
    end
  end

  def access_level=(new_level)
D
Douwe Maan 已提交
1125 1126
    new_level = new_level.to_s
    return unless %w(admin regular).include?(new_level)
1127

D
Douwe Maan 已提交
1128 1129
    self.admin = (new_level == 'admin')
  end
1130

1131 1132 1133 1134 1135 1136
  # Does the user have access to all private groups & projects?
  # Overridden in EE to also check auditor?
  def full_private_access?
    admin?
  end

1137
  def update_two_factor_requirement
1138
    periods = expanded_groups_requiring_two_factor_authentication.pluck(:two_factor_grace_period)
1139

1140
    self.require_two_factor_authentication_from_group = periods.any?
1141 1142 1143 1144 1145
    self.two_factor_grace_period = periods.min || User.column_defaults['two_factor_grace_period']

    save
  end

A
Alexis Reigel 已提交
1146 1147 1148 1149 1150 1151 1152
  # 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

1153 1154 1155 1156 1157 1158 1159 1160 1161 1162 1163 1164 1165 1166 1167 1168
  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 已提交
1169 1170
  # override, from Devise
  def lock_access!
1171
    Gitlab::AppLogger.info("Account Locked: username=#{username}")
B
Brian Neel 已提交
1172 1173 1174
    super
  end

1175 1176 1177 1178 1179 1180 1181 1182 1183 1184 1185 1186 1187 1188 1189 1190 1191 1192 1193 1194 1195 1196 1197 1198 1199 1200 1201 1202
  # 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

1203 1204 1205 1206 1207
  protected

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

1209 1210 1211
    super
  end

1212 1213
  private

1214 1215 1216 1217 1218 1219 1220 1221
  def ci_projects_union
    scope  = { access_level: [Gitlab::Access::MASTER, Gitlab::Access::OWNER] }
    groups = groups_projects.where(members: scope)
    other  = projects.where(members: scope)

    Gitlab::SQL::Union.new([personal_projects.select(:id), groups.select(:id),
                            other.select(:id)])
  end
V
Valery Sizov 已提交
1222 1223 1224

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

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

1230 1231 1232 1233 1234 1235 1236 1237 1238
  # 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

1239 1240 1241 1242 1243
  def ensure_user_rights_and_limits
    if external?
      self.can_create_group = false
      self.projects_limit   = 0
    else
1244 1245 1246
      # 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?
      self.projects_limit = current_application_settings.default_projects_limit unless projects_limit_changed?
1247
    end
Z
Zeger-Jan van de Weg 已提交
1248
  end
1249

1250 1251 1252 1253 1254 1255
  def signup_domain_valid?
    valid = true
    error = nil

    if current_application_settings.domain_blacklist_enabled?
      blocked_domains = current_application_settings.domain_blacklist
1256
      if domain_matches?(blocked_domains, email)
1257 1258 1259 1260 1261
        error = 'is not from an allowed domain.'
        valid = false
      end
    end

1262
    allowed_domains = current_application_settings.domain_whitelist
1263
    unless allowed_domains.blank?
1264
      if domain_matches?(allowed_domains, email)
1265 1266
        valid = true
      else
D
dev-chris 已提交
1267
        error = "domain is not authorized for sign-up"
1268 1269 1270 1271
        valid = false
      end
    end

1272
    errors.add(:email, error) unless valid
1273 1274 1275

    valid
  end
1276

1277
  def domain_matches?(email_domains, email)
1278 1279 1280 1281 1282 1283 1284
    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
1285 1286 1287 1288

  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.
1289
      SecureRandom.hex.to_i(16).to_s(36)
1290 1291 1292 1293
    else
      super
    end
  end
1294

1295 1296 1297 1298 1299 1300
  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
1301
    # exclusive lease to ensure than this block is never run concurrently.
1302
    lease_key = "user:unique_internal:#{username}"
1303 1304 1305 1306 1307 1308 1309 1310
    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

1311
    # Recheck if the user is already present. One might have been
1312 1313
    # added between the time we last checked (first line of this method)
    # and the time we acquired the lock.
1314 1315
    existing_user = uncached { scope.first }
    return existing_user if existing_user.present?
1316 1317 1318

    uniquify = Uniquify.new

1319
    username = uniquify.string(username) { |s| User.find_by_username(s) }
1320

1321
    email = uniquify.string(-> (n) { Kernel.sprintf(email_pattern, n) }) do |s|
1322 1323 1324
      User.find_by_email(s)
    end

1325
    user = scope.build(
1326 1327 1328
      username: username,
      email: email,
      &creation_block
1329
    )
J
James Lopez 已提交
1330

J
James Lopez 已提交
1331
    Users::UpdateService.new(user, user: user).execute(validate: false)
1332
    user
1333 1334 1335
  ensure
    Gitlab::ExclusiveLease.cancel(lease_key, uuid)
  end
G
gitlabhq 已提交
1336
end