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

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

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

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

24 25
  DEFAULT_NOTIFICATION_LEVEL = :participating

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

30
  add_authentication_token_field :incoming_email_token
31
  add_authentication_token_field :feed_token
32

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

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

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

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

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

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

67 68
    update_tracked_fields(request)

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

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

75
  attr_accessor :force_random_password
G
gitlabhq 已提交
76

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

80 81 82 83
  #
  # Relations
  #

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

  # Profile
88 89
  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
90
  has_many :gpg_keys
91

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

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

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

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

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

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

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

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

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

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

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

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

206
  after_initialize :set_projects_limit
D
Dmitriy Zaporozhets 已提交
207

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

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

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

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

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

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

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

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

      def active_for_authentication?
        false
      end

      def inactive_message
246
        BLOCKED_MESSAGE
247
      end
248
    end
249 250
  end

A
Andrey Kumanyaev 已提交
251
  # Scopes
252
  scope :admins, -> { where(admin: true) }
253
  scope :blocked, -> { with_states(:blocked, :ldap_blocked) }
254
  scope :external, -> { where(external: true) }
J
James Lopez 已提交
255
  scope :active, -> { with_state(:active).non_internal }
256
  scope :without_projects, -> { joins('LEFT JOIN project_authorizations ON users.id = project_authorizations.user_id').where(project_authorizations: { user_id: nil }) }
257 258
  scope :order_recent_sign_in, -> { reorder(Gitlab::Database.nulls_last_order('current_sign_in_at', 'DESC')) }
  scope :order_oldest_sign_in, -> { reorder(Gitlab::Database.nulls_last_order('current_sign_in_at', 'ASC')) }
259
  scope :confirmed, -> { where.not(confirmed_at: nil) }
260
  scope :by_username, -> (usernames) { iwhere(username: usernames) }
261

Y
Yorick Peterse 已提交
262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292
  # Limits the users to those that have TODOs, optionally in the given state.
  #
  # user - The user to get the todos for.
  #
  # with_todos - If we should limit the result set to users that are the
  #              authors of todos.
  #
  # todo_state - An optional state to require the todos to be in.
  def self.limit_to_todo_authors(user: nil, with_todos: false, todo_state: nil)
    if user && with_todos
      where(id: Todo.where(user: user, state: todo_state).select(:author_id))
    else
      all
    end
  end

  # Returns a relation that optionally includes the given user.
  #
  # user_id - The ID of the user to include.
  def self.union_with_user(user_id = nil)
    if user_id.present?
      union = Gitlab::SQL::Union.new([all, User.unscoped.where(id: user_id)])

      # We use "unscoped" here so that any inner conditions are not repeated for
      # the outer query, which would be redundant.
      User.unscoped.from("(#{union.to_sql}) #{User.table_name}")
    else
      all
    end
  end

293
  def self.with_two_factor
294 295 296 297 298 299 300 301 302
    with_u2f_registrations = <<-SQL
      EXISTS (
        SELECT *
        FROM u2f_registrations AS u2f
        WHERE u2f.user_id = users.id
      ) OR users.otp_required_for_login = ?
    SQL

    where(with_u2f_registrations, true)
303 304 305
  end

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

310 311 312
  #
  # Class methods
  #
A
Andrey Kumanyaev 已提交
313
  class << self
314
    # Devise method overridden to allow sign in with email or username
315 316 317
    def find_for_database_authentication(warden_conditions)
      conditions = warden_conditions.dup
      if login = conditions.delete(:login)
318
        where(conditions).find_by("lower(username) = :value OR lower(email) = :value", value: login.downcase.strip)
319
      else
G
Gabriel Mazetto 已提交
320
        find_by(conditions)
321 322
      end
    end
323

324
    def sort_by_attribute(method)
325 326 327
      order_method = method || 'id_desc'

      case order_method.to_s
328 329
      when 'recent_sign_in' then order_recent_sign_in
      when 'oldest_sign_in' then order_oldest_sign_in
330
      else
331
        order_by(order_method)
V
Valery Sizov 已提交
332 333 334
      end
    end

335
    def for_github_id(id)
336
      joins(:identities).merge(Identity.with_extern_uid(:github, id))
337 338
    end

339
    # Find a User by their primary email or any associated secondary email
340 341
    def find_by_any_email(email, confirmed: false)
      by_any_email(email, confirmed: confirmed).take
Y
Yorick Peterse 已提交
342 343 344
    end

    # Returns a relation containing all the users for the given Email address
345
    def by_any_email(email, confirmed: false)
Y
Yorick Peterse 已提交
346
      users = where(email: email)
347 348
      users = users.confirmed if confirmed

Y
Yorick Peterse 已提交
349
      emails = joins(:emails).where(emails: { email: email })
350
      emails = emails.confirmed if confirmed
Y
Yorick Peterse 已提交
351 352 353
      union = Gitlab::SQL::Union.new([users, emails])

      from("(#{union.to_sql}) #{table_name}")
354
    end
355

356
    def filter(filter_name)
A
Andrey Kumanyaev 已提交
357
      case filter_name
358
      when 'admins'
359
        admins
360
      when 'blocked'
361
        blocked
362
      when 'two_factor_disabled'
363
        without_two_factor
364
      when 'two_factor_enabled'
365
        with_two_factor
366
      when 'wop'
367
        without_projects
368
      when 'external'
369
        external
A
Andrey Kumanyaev 已提交
370
      else
371
        active
A
Andrey Kumanyaev 已提交
372
      end
373 374
    end

375 376 377 378 379 380 381
    # 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.
382
    def search(query)
383 384
      return none if query.blank?

385 386
      query = query.downcase

387 388 389 390 391 392 393 394 395
      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

396
      where(
397 398
        fuzzy_arel_match(:name, query, lower_exact_match: true)
          .or(fuzzy_arel_match(:username, query, lower_exact_match: true))
399
          .or(arel_table[:email].eq(query))
400
      ).reorder(order % { query: ActiveRecord::Base.connection.quote(query) }, :name)
A
Andrey Kumanyaev 已提交
401
    end
402

Y
Yorick Peterse 已提交
403 404 405 406 407 408 409 410 411 412 413 414
    # Limits the result set to users _not_ in the given query/list of IDs.
    #
    # users - The list of users to ignore. This can be an
    #         `ActiveRecord::Relation`, or an Array.
    def where_not_in(users = nil)
      users ? where.not(id: users) : all
    end

    def reorder_by_name
      reorder(:name)
    end

415 416 417 418 419
    # 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)
420 421
      return none if query.blank?

422 423
      query = query.downcase

424
      email_table = Email.arel_table
425 426
      matched_by_emails_user_ids = email_table
        .project(email_table[:user_id])
427
        .where(email_table[:email].eq(query))
428 429

      where(
430 431
        fuzzy_arel_match(:name, query)
          .or(fuzzy_arel_match(:username, query))
432
          .or(arel_table[:email].eq(query))
433
          .or(arel_table[:id].in(matched_by_emails_user_ids))
434 435 436
      )
    end

437
    def by_login(login)
438 439 440 441 442 443 444
      return nil unless login

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

447
    def find_by_username(username)
448
      by_username(username).take
449 450
    end

R
Robert Speicher 已提交
451
    def find_by_username!(username)
452
      by_username(username).take!
R
Robert Speicher 已提交
453 454
    end

T
Timothy Andrew 已提交
455
    def find_by_personal_access_token(token_string)
456 457
      return unless token_string

458
      PersonalAccessTokensFinder.new(state: 'active').find_by(token: token_string)&.user
T
Timothy Andrew 已提交
459 460
    end

Y
Yorick Peterse 已提交
461 462
    # Returns a user for the given SSH key.
    def find_by_ssh_key_id(key_id)
463
      Key.find_by(id: key_id)&.user
Y
Yorick Peterse 已提交
464 465
    end

466
    def find_by_full_path(path, follow_redirects: false)
467 468
      namespace = Namespace.for_user.find_by_full_path(path, follow_redirects: follow_redirects)
      namespace&.owner
469 470
    end

471 472 473
    def reference_prefix
      '@'
    end
474 475 476 477

    # Pattern used to extract `@user` user references from text
    def reference_pattern
      %r{
478
        (?<!\w)
479
        #{Regexp.escape(reference_prefix)}
480
        (?<user>#{Gitlab::PathRegex::FULL_NAMESPACE_FORMAT_REGEX})
481 482
      }x
    end
483 484 485 486

    # Return (create if necessary) the ghost user. The ghost user
    # owns records previously belonging to deleted users.
    def ghost
487 488
      email = 'ghost%s@example.com'
      unique_internal(where(ghost: true), 'ghost', email) do |u|
489 490 491
        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
492
    end
V
vsizov 已提交
493
  end
R
randx 已提交
494

M
Michael Kozono 已提交
495 496 497 498
  def full_path
    username
  end

499 500 501 502
  def self.internal_attributes
    [:ghost]
  end

503
  def internal?
504 505 506 507 508 509 510 511
    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
512
    where(internal_attributes.map { |attr| "#{attr} IS NOT TRUE" }.join(" AND "))
513 514
  end

515 516 517
  #
  # Instance methods
  #
518 519 520 521 522

  def to_param
    username
  end

J
Jarka Kadlecova 已提交
523
  def to_reference(_from = nil, target_project: nil, full: nil)
524 525 526
    "#{self.class.reference_prefix}#{username}"
  end

527 528
  def skip_confirmation=(bool)
    skip_confirmation! if bool
D
Daniel Juarez 已提交
529 530 531 532
  end

  def skip_reconfirmation=(bool)
    skip_reconfirmation! if bool
R
randx 已提交
533
  end
534

535
  def generate_reset_token
M
Marin Jankovski 已提交
536
    @reset_token, enc = Devise.token_generator.generate(self.class, :reset_password_token)
537 538 539 540

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

M
Marin Jankovski 已提交
541
    @reset_token
542 543
  end

544 545 546 547
  def recently_sent_password_reset?
    reset_password_sent_at.present? && reset_password_sent_at >= 1.minute.ago
  end

T
Toon Claes 已提交
548 549 550 551 552 553 554 555
  def remember_me!
    super if ::Gitlab::Database.read_write?
  end

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

R
Robert Speicher 已提交
556
  def disable_two_factor!
557
    transaction do
L
Lin Jen-Shin 已提交
558
      update(
559 560 561 562 563 564 565
        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
      )
566
      self.u2f_registrations.destroy_all # rubocop: disable DestroyAll
567 568 569 570 571 572 573 574
    end
  end

  def two_factor_enabled?
    two_factor_otp_enabled? || two_factor_u2f_enabled?
  end

  def two_factor_otp_enabled?
575
    otp_required_for_login?
576 577 578
  end

  def two_factor_u2f_enabled?
579 580 581 582 583
    if u2f_registrations.loaded?
      u2f_registrations.any?
    else
      u2f_registrations.exists?
    end
R
Robert Speicher 已提交
584 585
  end

586 587 588 589 590 591
  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

592
  def unique_email
593 594
    if !emails.exists?(email: email) && Email.exists?(email: email)
      errors.add(:email, 'has already been taken')
595
    end
596 597
  end

598
  def owns_notification_email
599
    return if temp_oauth_email?
600

601
    errors.add(:notification_email, "is not an email you own") unless all_emails.include?(notification_email)
602 603
  end

604
  def owns_public_email
605
    return if public_email.blank?
606

607
    errors.add(:public_email, "is not an email you own") unless all_emails.include?(public_email)
608 609
  end

A
Alexandra 已提交
610 611 612 613 614
  # 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

615
  # Note: the use of the Emails services will cause `saves` on the user object, running
616
  # through the callbacks again and can have side effects, such as the `previous_changes`
617 618 619
  # 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
620
  def update_emails_with_primary_email(previous_email)
621
    primary_email_record = emails.find_by(email: email)
622
    Emails::DestroyService.new(self, user: self).execute(primary_email_record) if primary_email_record
623

624 625
    # 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
626
    Emails::CreateService.new(self, user: self, email: previous_email).execute(confirmed_at: confirmed_at)
627 628
  end

629
  def update_invalid_gpg_signatures
630
    gpg_keys.each(&:update_invalid_gpg_signatures)
631 632
  end

633
  # Returns the groups a user has access to, either through a membership or a project authorization
634
  def authorized_groups
635 636
    union = Gitlab::SQL::Union
      .new([groups.select(:id), authorized_projects.select(:namespace_id)])
637

638
    Group.where("namespaces.id IN (#{union.to_sql})") # rubocop:disable GitlabSecurity/SqlInjection
639 640
  end

641 642 643 644 645
  # 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

646 647
  # Returns a relation of groups the user has access to, including their parent
  # and child groups (recursively).
648
  def all_expanded_groups
649
    Gitlab::GroupHierarchy.new(groups).all_groups
650 651 652 653 654 655
  end

  def expanded_groups_requiring_two_factor_authentication
    all_expanded_groups.where(require_two_factor_authentication: true)
  end

656
  def refresh_authorized_projects
657 658 659 660
    Users::RefreshAuthorizedProjectsService.new(self).execute
  end

  def remove_project_authorizations(project_ids)
661
    project_authorizations.where(project_id: project_ids).delete_all
662 663
  end

664
  def authorized_projects(min_access_level = nil)
665 666
    # We're overriding an association, so explicitly call super with no
    # arguments or it would be passed as `force_reload` to the association
667
    projects = super()
668 669

    if min_access_level
670 671
      projects = projects
        .where('project_authorizations.access_level >= ?', min_access_level)
672
    end
673 674 675 676 677 678

    projects
  end

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

681 682 683 684 685 686 687 688 689
  # 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

690 691 692 693 694 695 696 697 698 699
  # 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

700
  def owned_projects
701
    @owned_projects ||= Project.from("(#{owned_projects_union.to_sql}) AS projects")
702 703
  end

704 705 706 707
  # 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 已提交
708
    authorized_projects(Gitlab::Access::REPORTER).non_archived.with_issues_enabled
709 710
  end

D
Dmitriy Zaporozhets 已提交
711
  def require_ssh_key?
Y
Yorick Peterse 已提交
712 713 714
    count = Users::KeysCountService.new(self).count

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

717 718 719 720 721 722
  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?
723 724
  end

725
  def require_personal_access_token_creation_for_git_auth?
726
    return false if allow_password_authentication_for_git? || ldap_user?
727 728

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

731 732 733 734
  def require_extra_setup_for_git_auth?
    require_password_creation_for_git? || require_personal_access_token_creation_for_git_auth?
  end

735
  def allow_password_authentication?
736 737 738 739
    allow_password_authentication_for_web? || allow_password_authentication_for_git?
  end

  def allow_password_authentication_for_web?
740
    Gitlab::CurrentSettings.password_authentication_enabled_for_web? && !ldap_user?
741 742 743
  end

  def allow_password_authentication_for_git?
744
    Gitlab::CurrentSettings.password_authentication_enabled_for_git? && !ldap_user?
745 746
  end

747
  def can_change_username?
748
    gitlab_config.username_changing_enabled
749 750
  end

D
Dmitriy Zaporozhets 已提交
751
  def can_create_project?
752
    projects_limit_left > 0
D
Dmitriy Zaporozhets 已提交
753 754 755
  end

  def can_create_group?
756
    can?(:create_group)
D
Dmitriy Zaporozhets 已提交
757 758
  end

759 760 761 762
  def can_select_namespace?
    several_namespaces? || admin
  end

763
  def can?(action, subject = :global)
H
http://jneen.net/ 已提交
764
    Ability.allowed?(self, action, subject)
D
Dmitriy Zaporozhets 已提交
765 766
  end

767 768 769 770
  def confirm_deletion_with_password?
    !password_automatically_set? && allow_password_authentication?
  end

D
Dmitriy Zaporozhets 已提交
771 772 773 774
  def first_name
    name.split.first unless name.blank?
  end

775
  def projects_limit_left
776 777 778
    projects_limit - personal_projects_count
  end

779 780
  def recent_push(project = nil)
    service = Users::LastPushEventService.new(self)
D
Dmitriy Zaporozhets 已提交
781

782 783 784 785
    if project
      service.last_event_for_project(project)
    else
      service.last_event_for_user
786
    end
D
Dmitriy Zaporozhets 已提交
787 788 789
  end

  def several_namespaces?
790
    owned_groups.any? || maintainers_groups.any?
D
Dmitriy Zaporozhets 已提交
791 792 793 794 795
  end

  def namespace_id
    namespace.try :id
  end
796

797 798 799
  def name_with_username
    "#{name} (#{username})"
  end
D
Dmitriy Zaporozhets 已提交
800

801
  def already_forked?(project)
802 803 804
    !!fork_of(project)
  end

805
  def fork_of(project)
806
    namespace.find_fork_of(project)
807
  end
808 809

  def ldap_user?
810
    if identities.loaded?
811
      identities.find { |identity| Gitlab::Auth::OAuth::Provider.ldap_provider?(identity.provider) && !identity.extern_uid.nil? }
812 813 814
    else
      identities.exists?(["provider LIKE ? AND extern_uid IS NOT NULL", "ldap%"])
    end
815 816 817 818
  end

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

821
  def project_deploy_keys
822
    DeployKey.unscoped.in_projects(authorized_projects.pluck(:id)).distinct(:id)
823 824
  end

825
  def accessible_deploy_keys
826 827 828 829 830
    @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
831
  end
832 833

  def created_by
S
skv 已提交
834
    User.find_by(id: created_by_id) if created_by_id
835
  end
836 837

  def sanitize_attrs
838 839 840
    %i[skype linkedin twitter].each do |attr|
      value = self[attr]
      self[attr] = Sanitize.clean(value) if value.present?
841 842
    end
  end
843

844
  def set_notification_email
845
    if notification_email.blank? || all_emails.exclude?(notification_email)
846
      self.notification_email = email
847 848 849
    end
  end

850
  def set_public_email
851
    if public_email.blank? || all_emails.exclude?(public_email)
852
      self.public_email = ''
853 854 855
    end
  end

856
  def update_secondary_emails!
857 858 859
    set_notification_email
    set_public_email
    save if notification_email_changed? || public_email_changed?
860 861
  end

862
  def set_projects_limit
863 864 865
    # `User.select(:id)` raises
    # `ActiveModel::MissingAttributeError: missing attribute: projects_limit`
    # without this safeguard!
866
    return unless has_attribute?(:projects_limit) && projects_limit.nil?
867

868
    self.projects_limit = Gitlab::CurrentSettings.default_projects_limit
869 870
  end

871
  def requires_ldap_check?
872 873 874
    if !Gitlab.config.ldap.enabled
      false
    elsif ldap_user?
875 876 877 878 879 880
      !last_credential_check_at || (last_credential_check_at + 1.hour) < Time.now
    else
      false
    end
  end

J
Jacob Vosmaer 已提交
881 882 883 884 885 886 887
  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

888 889 890 891 892
  def solo_owned_groups
    @solo_owned_groups ||= owned_groups.select do |group|
      group.owners == [self]
    end
  end
893 894

  def with_defaults
895
    User.defaults.each do |k, v|
896
      public_send("#{k}=", v) # rubocop:disable GitlabSecurity/PublicSend
897
    end
898 899

    self
900
  end
901

902 903 904 905
  def can_leave_project?(project)
    project.namespace != namespace &&
      project.project_member(self)
  end
906

J
Jerome Dalbert 已提交
907
  def full_website_url
908
    return "http://#{website_url}" if website_url !~ %r{\Ahttps?://}
J
Jerome Dalbert 已提交
909 910 911 912 913

    website_url
  end

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

917
  def all_ssh_keys
918
    keys.map(&:publishable_key)
919
  end
920 921

  def temp_oauth_email?
922
    email.start_with?('temp-email-for-oauth')
923 924
  end

925
  def avatar_url(size: nil, scale: 2, **args)
926
    GravatarService.new.execute(email, size, scale, username: username)
927
  end
D
Dmitriy Zaporozhets 已提交
928

929 930 931 932
  def primary_email_verified?
    confirmed? && !temp_oauth_email?
  end

933 934 935 936 937 938 939 940 941 942
  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

943
  def all_emails
944
    all_emails = []
945 946
    all_emails << email unless temp_oauth_email?
    all_emails.concat(emails.map(&:email))
947
    all_emails
948 949
  end

950
  def verified_emails
951
    verified_emails = []
952
    verified_emails << email if primary_email_verified?
953
    verified_emails.concat(emails.confirmed.pluck(:email))
954 955 956
    verified_emails
  end

957
  def verified_email?(check_email)
958
    downcased = check_email.downcase
A
Alexandra 已提交
959
    email == downcased ? primary_email_verified? : emails.confirmed.where(email: downcased).exists?
960 961
  end

K
Kirill Zaitsev 已提交
962 963 964 965
  def hook_attrs
    {
      name: name,
      username: username,
966
      avatar_url: avatar_url(only_path: false)
K
Kirill Zaitsev 已提交
967 968 969
    }
  end

D
Dmitriy Zaporozhets 已提交
970
  def ensure_namespace_correct
971 972 973 974
    if namespace
      namespace.path = namespace.name = username if username_changed?
    else
      build_namespace(path: username, name: username)
D
Dmitriy Zaporozhets 已提交
975 976 977
    end
  end

978 979 980 981 982
  def set_username_errors
    namespace_path_errors = self.errors.delete(:"namespace.path")
    self.errors[:username].concat(namespace_path_errors) if namespace_path_errors
  end

983 984 985 986
  def username_changed_hook
    system_hook_service.execute_hooks_for(self, :rename)
  end

D
Dmitriy Zaporozhets 已提交
987
  def post_destroy_hook
988
    log_info("User \"#{name}\" (#{email})  was removed")
D
Douwe Maan 已提交
989

D
Dmitriy Zaporozhets 已提交
990 991 992
    system_hook_service.execute_hooks_for(self, :destroy)
  end

Y
Yorick Peterse 已提交
993 994 995 996
  def remove_key_cache
    Users::KeysCountService.new(self).delete_cache
  end

N
Nick Thomas 已提交
997 998
  def delete_async(deleted_by:, params: {})
    block if params[:hard_delete]
999
    DeleteUserWorker.perform_async(deleted_by.id, id, params.to_h)
N
Nick Thomas 已提交
1000 1001
  end

D
Dmitriy Zaporozhets 已提交
1002
  def notification_service
D
Dmitriy Zaporozhets 已提交
1003 1004 1005
    NotificationService.new
  end

1006
  def log_info(message)
D
Dmitriy Zaporozhets 已提交
1007 1008 1009 1010 1011 1012
    Gitlab::AppLogger.info message
  end

  def system_hook_service
    SystemHooksService.new
  end
C
Ciro Santilli 已提交
1013 1014

  def starred?(project)
V
Valery Sizov 已提交
1015
    starred_projects.exists?(project.id)
C
Ciro Santilli 已提交
1016 1017 1018
  end

  def toggle_star(project)
1019
    UsersStarProject.transaction do
1020 1021
      user_star_project = users_star_projects
          .where(project: project, user: self).lock(true).first
1022 1023 1024 1025 1026 1027

      if user_star_project
        user_star_project.destroy
      else
        UsersStarProject.create!(project: project, user: self)
      end
C
Ciro Santilli 已提交
1028 1029
    end
  end
1030 1031

  def manageable_namespaces
1032 1033 1034 1035
    @manageable_namespaces ||= [namespace] + manageable_groups
  end

  def manageable_groups
1036
    Gitlab::GroupHierarchy.new(owned_or_maintainers_groups).base_and_descendants
1037
  end
D
Dmitriy Zaporozhets 已提交
1038

1039 1040 1041 1042 1043 1044
  def namespaces
    namespace_ids = groups.pluck(:id)
    namespace_ids.push(namespace.id)
    Namespace.where(id: namespace_ids)
  end

D
Dmitriy Zaporozhets 已提交
1045
  def oauth_authorized_tokens
1046
    Doorkeeper::AccessToken.where(resource_owner_id: id, revoked_at: nil)
D
Dmitriy Zaporozhets 已提交
1047
  end
1048

1049 1050 1051 1052 1053 1054 1055 1056 1057
  # 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
1058
  # ms on a database with a similar size to GitLab.com's database. On the other
1059 1060
  # hand, using a subquery means we can get the exact same data in about 40 ms.
  def contributed_projects
1061 1062 1063 1064 1065
    events = Event.select(:project_id)
      .contributions.where(author_id: self)
      .where("created_at > ?", Time.now - 1.year)
      .uniq
      .reorder(nil)
1066 1067

    Project.where(id: events)
1068
  end
1069

1070 1071 1072
  def can_be_removed?
    !solo_owned_groups.present?
  end
1073

1074 1075
  def ci_owned_runners
    @ci_owned_runners ||= begin
1076
      project_runner_ids = Ci::RunnerProject
1077
        .where(project: authorized_projects(Gitlab::Access::MAINTAINER))
1078
        .select(:runner_id)
1079 1080

      group_runner_ids = Ci::RunnerNamespace
1081
        .where(namespace_id: owned_or_maintainers_groups.select(:id))
1082 1083 1084 1085
        .select(:runner_id)

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

1086
      Ci::Runner.where("ci_runners.id IN (#{union.to_sql})") # rubocop:disable GitlabSecurity/SqlInjection
K
Kamil Trzcinski 已提交
1087
    end
1088
  end
1089

1090
  def notification_settings_for(source)
1091
    if notification_settings.loaded?
1092 1093 1094 1095
      notification_settings.find do |notification|
        notification.source_type == source.class.base_class.name &&
          notification.source_id == source.id
      end
1096 1097 1098
    else
      notification_settings.find_or_initialize_by(source: source)
    end
1099 1100
  end

1101 1102 1103
  # Lazy load global notification setting
  # Initializes User setting with Participating level if setting not persisted
  def global_notification_setting
1104 1105 1106
    return @global_notification_setting if defined?(@global_notification_setting)

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

    @global_notification_setting
1110 1111
  end

1112
  def assigned_open_merge_requests_count(force: false)
1113
    Rails.cache.fetch(['users', id, 'assigned_open_merge_requests_count'], force: force, expires_in: 20.minutes) do
1114
      MergeRequestsFinder.new(self, assignee_id: self.id, state: 'opened').execute.count
1115 1116 1117
    end
  end

J
Josh Frye 已提交
1118
  def assigned_open_issues_count(force: false)
1119
    Rails.cache.fetch(['users', id, 'assigned_open_issues_count'], force: force, expires_in: 20.minutes) do
1120
      IssuesFinder.new(self, assignee_id: self.id, state: 'opened').execute.count
1121
    end
1122 1123
  end

1124 1125 1126 1127 1128 1129 1130 1131 1132 1133 1134 1135
  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 已提交
1136 1137 1138 1139 1140 1141
  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

1142 1143 1144 1145 1146
  def update_todos_count_cache
    todos_done_count(force: true)
    todos_pending_count(force: true)
  end

1147
  def invalidate_cache_counts
1148 1149
    invalidate_issue_cache_counts
    invalidate_merge_request_cache_counts
1150 1151
    invalidate_todos_done_count
    invalidate_todos_pending_count
A
Andreas Brandl 已提交
1152
    invalidate_personal_projects_count
1153 1154 1155
  end

  def invalidate_issue_cache_counts
1156 1157 1158
    Rails.cache.delete(['users', id, 'assigned_open_issues_count'])
  end

1159 1160 1161 1162
  def invalidate_merge_request_cache_counts
    Rails.cache.delete(['users', id, 'assigned_open_merge_requests_count'])
  end

1163 1164
  def invalidate_todos_done_count
    Rails.cache.delete(['users', id, 'todos_done_count'])
P
Paco Guzman 已提交
1165 1166
  end

1167 1168
  def invalidate_todos_pending_count
    Rails.cache.delete(['users', id, 'todos_pending_count'])
P
Paco Guzman 已提交
1169 1170
  end

A
Andreas Brandl 已提交
1171 1172 1173 1174
  def invalidate_personal_projects_count
    Rails.cache.delete(['users', id, 'personal_projects_count'])
  end

1175 1176 1177 1178 1179 1180 1181
  # 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!
1182 1183
    return if ::Gitlab::Database.read_only?

1184 1185
    self.failed_attempts ||= 0
    self.failed_attempts += 1
1186

1187 1188 1189
    if attempts_exceeded?
      lock_access! unless access_locked?
    else
J
James Lopez 已提交
1190
      Users::UpdateService.new(self, user: self).execute(validate: false)
1191 1192 1193
    end
  end

1194 1195 1196 1197 1198 1199 1200 1201 1202
  def access_level
    if admin?
      :admin
    else
      :regular
    end
  end

  def access_level=(new_level)
D
Douwe Maan 已提交
1203 1204
    new_level = new_level.to_s
    return unless %w(admin regular).include?(new_level)
1205

D
Douwe Maan 已提交
1206 1207
    self.admin = (new_level == 'admin')
  end
1208

1209 1210 1211 1212 1213 1214
  # Does the user have access to all private groups & projects?
  # Overridden in EE to also check auditor?
  def full_private_access?
    admin?
  end

1215
  def update_two_factor_requirement
1216
    periods = expanded_groups_requiring_two_factor_authentication.pluck(:two_factor_grace_period)
1217

1218
    self.require_two_factor_authentication_from_group = periods.any?
1219 1220 1221 1222 1223
    self.two_factor_grace_period = periods.min || User.column_defaults['two_factor_grace_period']

    save
  end

1224
  # each existing user needs to have an `feed_token`.
A
Alexis Reigel 已提交
1225 1226
  # we do this on read since migrating all existing users is not a feasible
  # solution.
1227 1228
  def feed_token
    ensure_feed_token!
A
Alexis Reigel 已提交
1229 1230
  end

1231 1232 1233 1234 1235 1236 1237 1238 1239 1240 1241 1242 1243 1244 1245 1246
  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 已提交
1247 1248
  # override, from Devise
  def lock_access!
1249
    Gitlab::AppLogger.info("Account Locked: username=#{username}")
B
Brian Neel 已提交
1250 1251 1252
    super
  end

1253 1254 1255 1256 1257 1258 1259 1260 1261 1262 1263 1264 1265 1266 1267 1268 1269 1270 1271 1272 1273 1274 1275 1276 1277 1278 1279 1280
  # 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

1281 1282 1283 1284
  def terms_accepted?
    accepted_term_id.present?
  end

1285 1286 1287 1288 1289
  def required_terms_not_accepted?
    Gitlab::CurrentSettings.current_application_settings.enforce_terms? &&
      !terms_accepted?
  end

1290 1291 1292
  # @deprecated
  alias_method :owned_or_masters_groups, :owned_or_maintainers_groups

1293 1294 1295 1296 1297
  protected

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

1299 1300 1301
    super
  end

1302 1303
  private

1304 1305 1306 1307 1308 1309 1310 1311 1312
  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 已提交
1313 1314
  # Added according to https://github.com/plataformatec/devise/blob/7df57d5081f9884849ca15e4fde179ef164a575f/README.md#activejob-integration
  def send_devise_notification(notification, *args)
1315
    return true unless can?(:receive_notifications)
1316

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

1320 1321 1322 1323 1324 1325 1326 1327 1328
  # 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

1329 1330 1331 1332 1333
  def ensure_user_rights_and_limits
    if external?
      self.can_create_group = false
      self.projects_limit   = 0
    else
1334 1335
      # 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?
1336
      self.projects_limit = Gitlab::CurrentSettings.default_projects_limit unless projects_limit_changed?
1337
    end
Z
Zeger-Jan van de Weg 已提交
1338
  end
1339

1340 1341 1342 1343
  def signup_domain_valid?
    valid = true
    error = nil

1344 1345
    if Gitlab::CurrentSettings.domain_blacklist_enabled?
      blocked_domains = Gitlab::CurrentSettings.domain_blacklist
1346
      if domain_matches?(blocked_domains, email)
1347 1348 1349 1350 1351
        error = 'is not from an allowed domain.'
        valid = false
      end
    end

1352
    allowed_domains = Gitlab::CurrentSettings.domain_whitelist
1353
    unless allowed_domains.blank?
1354
      if domain_matches?(allowed_domains, email)
1355 1356
        valid = true
      else
D
dev-chris 已提交
1357
        error = "domain is not authorized for sign-up"
1358 1359 1360 1361
        valid = false
      end
    end

1362
    errors.add(:email, error) unless valid
1363 1364 1365

    valid
  end
1366

1367
  def domain_matches?(email_domains, email)
1368 1369 1370 1371 1372 1373 1374
    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
1375 1376 1377 1378

  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.
1379
      SecureRandom.hex.to_i(16).to_s(36)
1380 1381 1382 1383
    else
      super
    end
  end
1384

1385 1386
  def self.unique_internal(scope, username, email_pattern, &block)
    scope.first || create_unique_internal(scope, username, email_pattern, &block)
1387 1388 1389 1390
  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
1391
    # exclusive lease to ensure than this block is never run concurrently.
1392
    lease_key = "user:unique_internal:#{username}"
1393 1394 1395 1396 1397 1398 1399 1400
    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

1401
    # Recheck if the user is already present. One might have been
1402 1403
    # added between the time we last checked (first line of this method)
    # and the time we acquired the lock.
1404 1405
    existing_user = uncached { scope.first }
    return existing_user if existing_user.present?
1406 1407 1408

    uniquify = Uniquify.new

1409
    username = uniquify.string(username) { |s| User.find_by_username(s) }
1410

1411
    email = uniquify.string(-> (n) { Kernel.sprintf(email_pattern, n) }) do |s|
1412 1413 1414
      User.find_by_email(s)
    end

1415
    user = scope.build(
1416 1417 1418
      username: username,
      email: email,
      &creation_block
1419
    )
J
James Lopez 已提交
1420

J
James Lopez 已提交
1421
    Users::UpdateService.new(user, user: user).execute(validate: false)
1422
    user
1423 1424 1425
  ensure
    Gitlab::ExclusiveLease.cancel(lease_key, uuid)
  end
G
gitlabhq 已提交
1426
end