user.rb 45.6 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
  # rubocop: disable CodeReuse/ServiceClass
65
  def update_tracked_fields!(request)
66 67
    return if Gitlab::Database.read_only?

68 69
    update_tracked_fields(request)

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

J
James Lopez 已提交
73
    Users::UpdateService.new(self, user: self).execute(validate: false)
74
  end
75
  # rubocop: enable CodeReuse/ServiceClass
76

77
  attr_accessor :force_random_password
G
gitlabhq 已提交
78

79 80 81
  # Virtual attribute for authenticating by either username or email
  attr_accessor :login

82 83 84 85
  #
  # Relations
  #

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

  # Profile
90 91
  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
92
  has_many :gpg_keys
93

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

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

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

124
  has_many :user_interacted_projects
125
  has_many :project_interactions, through: :user_interacted_projects, source: :project, class_name: 'Project'
126

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

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

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

B
Bob Van Landuyt 已提交
153 154
  has_one :status, class_name: 'UserStatus'

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

170
  validates :namespace, presence: true
171 172
  validate :namespace_move_dir_allowed, if: :username_changed?

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

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

208
  after_initialize :set_projects_limit
D
Dmitriy Zaporozhets 已提交
209

210
  # User's Layout preference
211
  enum layout: [:fixed, :fluid]
212

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

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

221
  delegate :path, to: :namespace, allow_nil: true, prefix: true
222

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

229 230 231 232
    event :ldap_block do
      transition active: :ldap_blocked
    end

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

    state :blocked, :ldap_blocked do
      def blocked?
        true
      end
242 243 244 245 246 247

      def active_for_authentication?
        false
      end

      def inactive_message
248
        BLOCKED_MESSAGE
249
      end
250
    end
251 252
  end

A
Andrey Kumanyaev 已提交
253
  # Scopes
254
  scope :admins, -> { where(admin: true) }
255
  scope :blocked, -> { with_states(:blocked, :ldap_blocked) }
256
  scope :external, -> { where(external: true) }
J
James Lopez 已提交
257
  scope :active, -> { with_state(:active).non_internal }
258
  scope :without_projects, -> { joins('LEFT JOIN project_authorizations ON users.id = project_authorizations.user_id').where(project_authorizations: { user_id: nil }) }
259 260
  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')) }
261
  scope :confirmed, -> { where.not(confirmed_at: nil) }
262
  scope :by_username, -> (usernames) { iwhere(username: usernames) }
263

Y
Yorick Peterse 已提交
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 293 294
  # 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

295
  def self.with_two_factor
296 297 298 299 300 301 302 303 304
    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)
305 306 307
  end

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

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

326
    def sort_by_attribute(method)
327 328 329
      order_method = method || 'id_desc'

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

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

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

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

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

      from("(#{union.to_sql}) #{table_name}")
356
    end
357

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

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

387 388
      query = query.downcase

389 390 391 392 393 394 395 396 397
      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

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

Y
Yorick Peterse 已提交
405 406 407 408 409 410 411 412 413 414 415 416
    # 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

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

424 425
      query = query.downcase

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

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

439
    def by_login(login)
440 441 442 443 444 445 446
      return nil unless login

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

449
    def find_by_username(username)
450
      by_username(username).take
451 452
    end

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

T
Timothy Andrew 已提交
457
    def find_by_personal_access_token(token_string)
458 459
      return unless token_string

460
      PersonalAccessTokensFinder.new(state: 'active').find_by(token: token_string)&.user # rubocop: disable CodeReuse/Finder
T
Timothy Andrew 已提交
461 462
    end

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

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

473 474 475
    def reference_prefix
      '@'
    end
476 477 478 479

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

    # Return (create if necessary) the ghost user. The ghost user
    # owns records previously belonging to deleted users.
    def ghost
489 490
      email = 'ghost%s@example.com'
      unique_internal(where(ghost: true), 'ghost', email) do |u|
491 492 493
        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
494
    end
495 496 497 498 499 500 501 502 503 504

    # Return true if there is only single non-internal user in the deployment,
    # ghost user is ignored.
    def single_user?
      User.non_internal.limit(2).count == 1
    end

    def single_user
      User.non_internal.first if single_user?
    end
V
vsizov 已提交
505
  end
R
randx 已提交
506

M
Michael Kozono 已提交
507 508 509 510
  def full_path
    username
  end

511 512 513 514
  def self.internal_attributes
    [:ghost]
  end

515
  def internal?
516 517 518 519 520 521 522 523
    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
524
    where(internal_attributes.map { |attr| "#{attr} IS NOT TRUE" }.join(" AND "))
525 526
  end

527 528 529
  #
  # Instance methods
  #
530 531 532 533 534

  def to_param
    username
  end

J
Jarka Kadlecova 已提交
535
  def to_reference(_from = nil, target_project: nil, full: nil)
536 537 538
    "#{self.class.reference_prefix}#{username}"
  end

539 540
  def skip_confirmation=(bool)
    skip_confirmation! if bool
D
Daniel Juarez 已提交
541 542 543 544
  end

  def skip_reconfirmation=(bool)
    skip_reconfirmation! if bool
R
randx 已提交
545
  end
546

547
  def generate_reset_token
M
Marin Jankovski 已提交
548
    @reset_token, enc = Devise.token_generator.generate(self.class, :reset_password_token)
549 550 551 552

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

M
Marin Jankovski 已提交
553
    @reset_token
554 555
  end

556 557 558 559
  def recently_sent_password_reset?
    reset_password_sent_at.present? && reset_password_sent_at >= 1.minute.ago
  end

T
Toon Claes 已提交
560 561 562 563 564 565 566 567
  def remember_me!
    super if ::Gitlab::Database.read_write?
  end

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

R
Robert Speicher 已提交
568
  def disable_two_factor!
569
    transaction do
L
Lin Jen-Shin 已提交
570
      update(
571 572 573 574 575 576 577
        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
      )
578
      self.u2f_registrations.destroy_all # rubocop: disable DestroyAll
579 580 581 582 583 584 585 586
    end
  end

  def two_factor_enabled?
    two_factor_otp_enabled? || two_factor_u2f_enabled?
  end

  def two_factor_otp_enabled?
587
    otp_required_for_login?
588 589 590
  end

  def two_factor_u2f_enabled?
591 592 593 594 595
    if u2f_registrations.loaded?
      u2f_registrations.any?
    else
      u2f_registrations.exists?
    end
R
Robert Speicher 已提交
596 597
  end

598 599 600 601 602 603
  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

604
  def unique_email
605 606
    if !emails.exists?(email: email) && Email.exists?(email: email)
      errors.add(:email, 'has already been taken')
607
    end
608 609
  end

610
  def owns_notification_email
611
    return if temp_oauth_email?
612

613
    errors.add(:notification_email, "is not an email you own") unless all_emails.include?(notification_email)
614 615
  end

616
  def owns_public_email
617
    return if public_email.blank?
618

619
    errors.add(:public_email, "is not an email you own") unless all_emails.include?(public_email)
620 621
  end

A
Alexandra 已提交
622 623 624 625 626
  # 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

627
  # Note: the use of the Emails services will cause `saves` on the user object, running
628
  # through the callbacks again and can have side effects, such as the `previous_changes`
629 630 631
  # 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
632
  # rubocop: disable CodeReuse/ServiceClass
633
  def update_emails_with_primary_email(previous_email)
634
    primary_email_record = emails.find_by(email: email)
635
    Emails::DestroyService.new(self, user: self).execute(primary_email_record) if primary_email_record
636

637 638
    # 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
639
    Emails::CreateService.new(self, user: self, email: previous_email).execute(confirmed_at: confirmed_at)
640
  end
641
  # rubocop: enable CodeReuse/ServiceClass
642

643
  def update_invalid_gpg_signatures
644
    gpg_keys.each(&:update_invalid_gpg_signatures)
645 646
  end

647
  # Returns the groups a user has access to, either through a membership or a project authorization
648
  def authorized_groups
649 650
    union = Gitlab::SQL::Union
      .new([groups.select(:id), authorized_projects.select(:namespace_id)])
651

652
    Group.where("namespaces.id IN (#{union.to_sql})") # rubocop:disable GitlabSecurity/SqlInjection
653 654
  end

655 656 657 658 659
  # 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

660 661
  # Returns a relation of groups the user has access to, including their parent
  # and child groups (recursively).
662
  def all_expanded_groups
663
    Gitlab::GroupHierarchy.new(groups).all_groups
664 665 666 667 668 669
  end

  def expanded_groups_requiring_two_factor_authentication
    all_expanded_groups.where(require_two_factor_authentication: true)
  end

670
  # rubocop: disable CodeReuse/ServiceClass
671
  def refresh_authorized_projects
672 673
    Users::RefreshAuthorizedProjectsService.new(self).execute
  end
674
  # rubocop: enable CodeReuse/ServiceClass
675 676

  def remove_project_authorizations(project_ids)
677
    project_authorizations.where(project_id: project_ids).delete_all
678 679
  end

680
  def authorized_projects(min_access_level = nil)
681 682
    # We're overriding an association, so explicitly call super with no
    # arguments or it would be passed as `force_reload` to the association
683
    projects = super()
684 685

    if min_access_level
686 687
      projects = projects
        .where('project_authorizations.access_level >= ?', min_access_level)
688
    end
689 690 691 692 693 694

    projects
  end

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

697 698 699 700 701 702 703 704 705
  # 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

706 707 708 709 710 711 712 713 714 715
  # 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

716
  def owned_projects
717
    @owned_projects ||= Project.from("(#{owned_projects_union.to_sql}) AS projects")
718 719
  end

720 721 722 723
  # 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 已提交
724
    authorized_projects(Gitlab::Access::REPORTER).non_archived.with_issues_enabled
725 726
  end

727
  # rubocop: disable CodeReuse/ServiceClass
D
Dmitriy Zaporozhets 已提交
728
  def require_ssh_key?
Y
Yorick Peterse 已提交
729 730 731
    count = Users::KeysCountService.new(self).count

    count.zero? && Gitlab::ProtocolAccess.allowed?('ssh')
D
Dmitriy Zaporozhets 已提交
732
  end
733
  # rubocop: enable CodeReuse/ServiceClass
D
Dmitriy Zaporozhets 已提交
734

735 736 737 738 739 740
  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?
741 742
  end

743
  def require_personal_access_token_creation_for_git_auth?
744
    return false if allow_password_authentication_for_git? || ldap_user?
745 746

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

749 750 751 752
  def require_extra_setup_for_git_auth?
    require_password_creation_for_git? || require_personal_access_token_creation_for_git_auth?
  end

753
  def allow_password_authentication?
754 755 756 757
    allow_password_authentication_for_web? || allow_password_authentication_for_git?
  end

  def allow_password_authentication_for_web?
758
    Gitlab::CurrentSettings.password_authentication_enabled_for_web? && !ldap_user?
759 760 761
  end

  def allow_password_authentication_for_git?
762
    Gitlab::CurrentSettings.password_authentication_enabled_for_git? && !ldap_user?
763 764
  end

765
  def can_change_username?
766
    gitlab_config.username_changing_enabled
767 768
  end

D
Dmitriy Zaporozhets 已提交
769
  def can_create_project?
770
    projects_limit_left > 0
D
Dmitriy Zaporozhets 已提交
771 772 773
  end

  def can_create_group?
774
    can?(:create_group)
D
Dmitriy Zaporozhets 已提交
775 776
  end

777 778 779 780
  def can_select_namespace?
    several_namespaces? || admin
  end

781
  def can?(action, subject = :global)
H
http://jneen.net/ 已提交
782
    Ability.allowed?(self, action, subject)
D
Dmitriy Zaporozhets 已提交
783 784
  end

785 786 787 788
  def confirm_deletion_with_password?
    !password_automatically_set? && allow_password_authentication?
  end

D
Dmitriy Zaporozhets 已提交
789 790 791 792
  def first_name
    name.split.first unless name.blank?
  end

793
  def projects_limit_left
794 795 796
    projects_limit - personal_projects_count
  end

797
  # rubocop: disable CodeReuse/ServiceClass
798 799
  def recent_push(project = nil)
    service = Users::LastPushEventService.new(self)
D
Dmitriy Zaporozhets 已提交
800

801 802 803 804
    if project
      service.last_event_for_project(project)
    else
      service.last_event_for_user
805
    end
D
Dmitriy Zaporozhets 已提交
806
  end
807
  # rubocop: enable CodeReuse/ServiceClass
D
Dmitriy Zaporozhets 已提交
808 809

  def several_namespaces?
810
    owned_groups.any? || maintainers_groups.any?
D
Dmitriy Zaporozhets 已提交
811 812 813 814 815
  end

  def namespace_id
    namespace.try :id
  end
816

817 818 819
  def name_with_username
    "#{name} (#{username})"
  end
D
Dmitriy Zaporozhets 已提交
820

821
  def already_forked?(project)
822 823 824
    !!fork_of(project)
  end

825
  def fork_of(project)
826
    namespace.find_fork_of(project)
827
  end
828 829

  def ldap_user?
830
    if identities.loaded?
831
      identities.find { |identity| Gitlab::Auth::OAuth::Provider.ldap_provider?(identity.provider) && !identity.extern_uid.nil? }
832 833 834
    else
      identities.exists?(["provider LIKE ? AND extern_uid IS NOT NULL", "ldap%"])
    end
835 836 837 838
  end

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

841
  def project_deploy_keys
842
    DeployKey.unscoped.in_projects(authorized_projects.pluck(:id)).distinct(:id)
843 844
  end

845
  def accessible_deploy_keys
846 847 848 849 850
    @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
851
  end
852 853

  def created_by
S
skv 已提交
854
    User.find_by(id: created_by_id) if created_by_id
855
  end
856 857

  def sanitize_attrs
858 859 860
    %i[skype linkedin twitter].each do |attr|
      value = self[attr]
      self[attr] = Sanitize.clean(value) if value.present?
861 862
    end
  end
863

864
  def set_notification_email
865
    if notification_email.blank? || all_emails.exclude?(notification_email)
866
      self.notification_email = email
867 868 869
    end
  end

870
  def set_public_email
871
    if public_email.blank? || all_emails.exclude?(public_email)
872
      self.public_email = ''
873 874 875
    end
  end

876
  def update_secondary_emails!
877 878 879
    set_notification_email
    set_public_email
    save if notification_email_changed? || public_email_changed?
880 881
  end

882
  def set_projects_limit
883 884 885
    # `User.select(:id)` raises
    # `ActiveModel::MissingAttributeError: missing attribute: projects_limit`
    # without this safeguard!
886
    return unless has_attribute?(:projects_limit) && projects_limit.nil?
887

888
    self.projects_limit = Gitlab::CurrentSettings.default_projects_limit
889 890
  end

891
  def requires_ldap_check?
892 893 894
    if !Gitlab.config.ldap.enabled
      false
    elsif ldap_user?
895 896 897 898 899 900
      !last_credential_check_at || (last_credential_check_at + 1.hour) < Time.now
    else
      false
    end
  end

J
Jacob Vosmaer 已提交
901 902 903 904 905 906 907
  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

908 909 910 911 912
  def solo_owned_groups
    @solo_owned_groups ||= owned_groups.select do |group|
      group.owners == [self]
    end
  end
913 914

  def with_defaults
915
    User.defaults.each do |k, v|
916
      public_send("#{k}=", v) # rubocop:disable GitlabSecurity/PublicSend
917
    end
918 919

    self
920
  end
921

922 923 924 925
  def can_leave_project?(project)
    project.namespace != namespace &&
      project.project_member(self)
  end
926

J
Jerome Dalbert 已提交
927
  def full_website_url
928
    return "http://#{website_url}" if website_url !~ %r{\Ahttps?://}
J
Jerome Dalbert 已提交
929 930 931 932 933

    website_url
  end

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

937
  def all_ssh_keys
938
    keys.map(&:publishable_key)
939
  end
940 941

  def temp_oauth_email?
942
    email.start_with?('temp-email-for-oauth')
943 944
  end

945
  # rubocop: disable CodeReuse/ServiceClass
946
  def avatar_url(size: nil, scale: 2, **args)
947
    GravatarService.new.execute(email, size, scale, username: username)
948
  end
949
  # rubocop: enable CodeReuse/ServiceClass
D
Dmitriy Zaporozhets 已提交
950

951 952 953 954
  def primary_email_verified?
    confirmed? && !temp_oauth_email?
  end

955 956 957 958 959 960 961 962 963 964
  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

965
  def all_emails
966
    all_emails = []
967 968
    all_emails << email unless temp_oauth_email?
    all_emails.concat(emails.map(&:email))
969
    all_emails
970 971
  end

972
  def verified_emails
973
    verified_emails = []
974
    verified_emails << email if primary_email_verified?
975
    verified_emails.concat(emails.confirmed.pluck(:email))
976 977 978
    verified_emails
  end

979
  def verified_email?(check_email)
980
    downcased = check_email.downcase
A
Alexandra 已提交
981
    email == downcased ? primary_email_verified? : emails.confirmed.where(email: downcased).exists?
982 983
  end

K
Kirill Zaitsev 已提交
984 985 986 987
  def hook_attrs
    {
      name: name,
      username: username,
988
      avatar_url: avatar_url(only_path: false)
K
Kirill Zaitsev 已提交
989 990 991
    }
  end

D
Dmitriy Zaporozhets 已提交
992
  def ensure_namespace_correct
993 994 995 996
    if namespace
      namespace.path = namespace.name = username if username_changed?
    else
      build_namespace(path: username, name: username)
D
Dmitriy Zaporozhets 已提交
997 998 999
    end
  end

1000 1001 1002 1003 1004
  def set_username_errors
    namespace_path_errors = self.errors.delete(:"namespace.path")
    self.errors[:username].concat(namespace_path_errors) if namespace_path_errors
  end

1005 1006 1007 1008
  def username_changed_hook
    system_hook_service.execute_hooks_for(self, :rename)
  end

D
Dmitriy Zaporozhets 已提交
1009
  def post_destroy_hook
1010
    log_info("User \"#{name}\" (#{email})  was removed")
D
Douwe Maan 已提交
1011

D
Dmitriy Zaporozhets 已提交
1012 1013 1014
    system_hook_service.execute_hooks_for(self, :destroy)
  end

1015
  # rubocop: disable CodeReuse/ServiceClass
Y
Yorick Peterse 已提交
1016 1017 1018
  def remove_key_cache
    Users::KeysCountService.new(self).delete_cache
  end
1019
  # rubocop: enable CodeReuse/ServiceClass
Y
Yorick Peterse 已提交
1020

N
Nick Thomas 已提交
1021 1022
  def delete_async(deleted_by:, params: {})
    block if params[:hard_delete]
1023
    DeleteUserWorker.perform_async(deleted_by.id, id, params.to_h)
N
Nick Thomas 已提交
1024 1025
  end

1026
  # rubocop: disable CodeReuse/ServiceClass
D
Dmitriy Zaporozhets 已提交
1027
  def notification_service
D
Dmitriy Zaporozhets 已提交
1028 1029
    NotificationService.new
  end
1030
  # rubocop: enable CodeReuse/ServiceClass
D
Dmitriy Zaporozhets 已提交
1031

1032
  def log_info(message)
D
Dmitriy Zaporozhets 已提交
1033 1034 1035
    Gitlab::AppLogger.info message
  end

1036
  # rubocop: disable CodeReuse/ServiceClass
D
Dmitriy Zaporozhets 已提交
1037 1038 1039
  def system_hook_service
    SystemHooksService.new
  end
1040
  # rubocop: enable CodeReuse/ServiceClass
C
Ciro Santilli 已提交
1041 1042

  def starred?(project)
V
Valery Sizov 已提交
1043
    starred_projects.exists?(project.id)
C
Ciro Santilli 已提交
1044 1045 1046
  end

  def toggle_star(project)
1047
    UsersStarProject.transaction do
1048 1049
      user_star_project = users_star_projects
          .where(project: project, user: self).lock(true).first
1050 1051 1052 1053 1054 1055

      if user_star_project
        user_star_project.destroy
      else
        UsersStarProject.create!(project: project, user: self)
      end
C
Ciro Santilli 已提交
1056 1057
    end
  end
1058 1059

  def manageable_namespaces
1060 1061 1062 1063
    @manageable_namespaces ||= [namespace] + manageable_groups
  end

  def manageable_groups
1064
    Gitlab::GroupHierarchy.new(owned_or_maintainers_groups).base_and_descendants
1065
  end
D
Dmitriy Zaporozhets 已提交
1066

1067 1068 1069 1070 1071 1072
  def namespaces
    namespace_ids = groups.pluck(:id)
    namespace_ids.push(namespace.id)
    Namespace.where(id: namespace_ids)
  end

D
Dmitriy Zaporozhets 已提交
1073
  def oauth_authorized_tokens
1074
    Doorkeeper::AccessToken.where(resource_owner_id: id, revoked_at: nil)
D
Dmitriy Zaporozhets 已提交
1075
  end
1076

1077 1078 1079 1080 1081 1082 1083 1084 1085
  # 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
1086
  # ms on a database with a similar size to GitLab.com's database. On the other
1087 1088
  # hand, using a subquery means we can get the exact same data in about 40 ms.
  def contributed_projects
1089 1090 1091 1092 1093
    events = Event.select(:project_id)
      .contributions.where(author_id: self)
      .where("created_at > ?", Time.now - 1.year)
      .uniq
      .reorder(nil)
1094 1095

    Project.where(id: events)
1096
  end
1097

1098 1099 1100
  def can_be_removed?
    !solo_owned_groups.present?
  end
1101

1102 1103
  def ci_owned_runners
    @ci_owned_runners ||= begin
1104
      project_runner_ids = Ci::RunnerProject
1105
        .where(project: authorized_projects(Gitlab::Access::MAINTAINER))
1106
        .select(:runner_id)
1107 1108

      group_runner_ids = Ci::RunnerNamespace
1109
        .where(namespace_id: owned_or_maintainers_groups.select(:id))
1110 1111 1112 1113
        .select(:runner_id)

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

1114
      Ci::Runner.where("ci_runners.id IN (#{union.to_sql})") # rubocop:disable GitlabSecurity/SqlInjection
K
Kamil Trzcinski 已提交
1115
    end
1116
  end
1117

1118
  def notification_settings_for(source)
1119
    if notification_settings.loaded?
1120 1121 1122 1123
      notification_settings.find do |notification|
        notification.source_type == source.class.base_class.name &&
          notification.source_id == source.id
      end
1124 1125 1126
    else
      notification_settings.find_or_initialize_by(source: source)
    end
1127 1128
  end

1129 1130 1131
  # Lazy load global notification setting
  # Initializes User setting with Participating level if setting not persisted
  def global_notification_setting
1132 1133 1134
    return @global_notification_setting if defined?(@global_notification_setting)

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

    @global_notification_setting
1138 1139
  end

1140
  def assigned_open_merge_requests_count(force: false)
1141
    Rails.cache.fetch(['users', id, 'assigned_open_merge_requests_count'], force: force, expires_in: 20.minutes) do
1142
      MergeRequestsFinder.new(self, assignee_id: self.id, state: 'opened').execute.count
1143 1144 1145
    end
  end

J
Josh Frye 已提交
1146
  def assigned_open_issues_count(force: false)
1147
    Rails.cache.fetch(['users', id, 'assigned_open_issues_count'], force: force, expires_in: 20.minutes) do
1148
      IssuesFinder.new(self, assignee_id: self.id, state: 'opened').execute.count
1149
    end
1150 1151
  end

1152 1153 1154 1155 1156 1157 1158 1159 1160 1161 1162 1163
  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 已提交
1164 1165 1166 1167 1168 1169
  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

1170 1171 1172 1173 1174
  def update_todos_count_cache
    todos_done_count(force: true)
    todos_pending_count(force: true)
  end

1175
  def invalidate_cache_counts
1176 1177
    invalidate_issue_cache_counts
    invalidate_merge_request_cache_counts
1178 1179
    invalidate_todos_done_count
    invalidate_todos_pending_count
A
Andreas Brandl 已提交
1180
    invalidate_personal_projects_count
1181 1182 1183
  end

  def invalidate_issue_cache_counts
1184 1185 1186
    Rails.cache.delete(['users', id, 'assigned_open_issues_count'])
  end

1187 1188 1189 1190
  def invalidate_merge_request_cache_counts
    Rails.cache.delete(['users', id, 'assigned_open_merge_requests_count'])
  end

1191 1192
  def invalidate_todos_done_count
    Rails.cache.delete(['users', id, 'todos_done_count'])
P
Paco Guzman 已提交
1193 1194
  end

1195 1196
  def invalidate_todos_pending_count
    Rails.cache.delete(['users', id, 'todos_pending_count'])
P
Paco Guzman 已提交
1197 1198
  end

A
Andreas Brandl 已提交
1199 1200 1201 1202
  def invalidate_personal_projects_count
    Rails.cache.delete(['users', id, 'personal_projects_count'])
  end

1203 1204 1205 1206 1207 1208
  # 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>
  #
1209
  # rubocop: disable CodeReuse/ServiceClass
1210
  def increment_failed_attempts!
1211 1212
    return if ::Gitlab::Database.read_only?

1213 1214
    self.failed_attempts ||= 0
    self.failed_attempts += 1
1215

1216 1217 1218
    if attempts_exceeded?
      lock_access! unless access_locked?
    else
J
James Lopez 已提交
1219
      Users::UpdateService.new(self, user: self).execute(validate: false)
1220 1221
    end
  end
1222
  # rubocop: enable CodeReuse/ServiceClass
1223

1224 1225 1226 1227 1228 1229 1230 1231 1232
  def access_level
    if admin?
      :admin
    else
      :regular
    end
  end

  def access_level=(new_level)
D
Douwe Maan 已提交
1233 1234
    new_level = new_level.to_s
    return unless %w(admin regular).include?(new_level)
1235

D
Douwe Maan 已提交
1236 1237
    self.admin = (new_level == 'admin')
  end
1238

1239 1240 1241 1242 1243 1244
  # Does the user have access to all private groups & projects?
  # Overridden in EE to also check auditor?
  def full_private_access?
    admin?
  end

1245
  def update_two_factor_requirement
1246
    periods = expanded_groups_requiring_two_factor_authentication.pluck(:two_factor_grace_period)
1247

1248
    self.require_two_factor_authentication_from_group = periods.any?
1249 1250 1251 1252 1253
    self.two_factor_grace_period = periods.min || User.column_defaults['two_factor_grace_period']

    save
  end

1254
  # each existing user needs to have an `feed_token`.
A
Alexis Reigel 已提交
1255 1256
  # we do this on read since migrating all existing users is not a feasible
  # solution.
1257 1258
  def feed_token
    ensure_feed_token!
A
Alexis Reigel 已提交
1259 1260
  end

1261 1262 1263 1264 1265 1266 1267 1268 1269 1270 1271 1272 1273 1274 1275 1276
  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 已提交
1277 1278
  # override, from Devise
  def lock_access!
1279
    Gitlab::AppLogger.info("Account Locked: username=#{username}")
B
Brian Neel 已提交
1280 1281 1282
    super
  end

1283 1284 1285 1286 1287 1288 1289 1290 1291 1292 1293 1294 1295 1296 1297 1298 1299 1300 1301 1302 1303 1304 1305 1306 1307 1308 1309 1310
  # 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

1311 1312 1313 1314
  def terms_accepted?
    accepted_term_id.present?
  end

1315 1316 1317 1318 1319
  def required_terms_not_accepted?
    Gitlab::CurrentSettings.current_application_settings.enforce_terms? &&
      !terms_accepted?
  end

1320 1321 1322 1323
  def requires_usage_stats_consent?
    !consented_usage_stats? && 7.days.ago > self.created_at && !has_current_license? && User.single_user?
  end

1324 1325 1326
  # @deprecated
  alias_method :owned_or_masters_groups, :owned_or_maintainers_groups

1327 1328 1329 1330 1331
  protected

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

1333 1334 1335
    super
  end

1336 1337
  private

1338 1339 1340 1341 1342 1343 1344 1345
  def has_current_license?
    false
  end

  def consented_usage_stats?
    Gitlab::CurrentSettings.usage_stats_set_by_user_id == self.id
  end

1346 1347 1348 1349 1350 1351 1352 1353 1354
  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 已提交
1355 1356
  # Added according to https://github.com/plataformatec/devise/blob/7df57d5081f9884849ca15e4fde179ef164a575f/README.md#activejob-integration
  def send_devise_notification(notification, *args)
1357
    return true unless can?(:receive_notifications)
1358

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

1362 1363 1364 1365 1366 1367 1368 1369 1370
  # 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

1371 1372 1373 1374 1375
  def ensure_user_rights_and_limits
    if external?
      self.can_create_group = false
      self.projects_limit   = 0
    else
1376 1377
      # 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?
1378
      self.projects_limit = Gitlab::CurrentSettings.default_projects_limit unless projects_limit_changed?
1379
    end
Z
Zeger-Jan van de Weg 已提交
1380
  end
1381

1382 1383 1384 1385
  def signup_domain_valid?
    valid = true
    error = nil

1386 1387
    if Gitlab::CurrentSettings.domain_blacklist_enabled?
      blocked_domains = Gitlab::CurrentSettings.domain_blacklist
1388
      if domain_matches?(blocked_domains, email)
1389 1390 1391 1392 1393
        error = 'is not from an allowed domain.'
        valid = false
      end
    end

1394
    allowed_domains = Gitlab::CurrentSettings.domain_whitelist
1395
    unless allowed_domains.blank?
1396
      if domain_matches?(allowed_domains, email)
1397 1398
        valid = true
      else
D
dev-chris 已提交
1399
        error = "domain is not authorized for sign-up"
1400 1401 1402 1403
        valid = false
      end
    end

1404
    errors.add(:email, error) unless valid
1405 1406 1407

    valid
  end
1408

1409
  def domain_matches?(email_domains, email)
1410 1411 1412 1413 1414 1415 1416
    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
1417 1418 1419 1420

  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.
1421
      SecureRandom.hex.to_i(16).to_s(36)
1422 1423 1424 1425
    else
      super
    end
  end
1426

1427 1428
  def self.unique_internal(scope, username, email_pattern, &block)
    scope.first || create_unique_internal(scope, username, email_pattern, &block)
1429 1430 1431 1432
  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
1433
    # exclusive lease to ensure than this block is never run concurrently.
1434
    lease_key = "user:unique_internal:#{username}"
1435 1436 1437 1438 1439 1440 1441 1442
    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

1443
    # Recheck if the user is already present. One might have been
1444 1445
    # added between the time we last checked (first line of this method)
    # and the time we acquired the lock.
1446 1447
    existing_user = uncached { scope.first }
    return existing_user if existing_user.present?
1448 1449 1450

    uniquify = Uniquify.new

1451
    username = uniquify.string(username) { |s| User.find_by_username(s) }
1452

1453
    email = uniquify.string(-> (n) { Kernel.sprintf(email_pattern, n) }) do |s|
1454 1455 1456
      User.find_by_email(s)
    end

1457
    user = scope.build(
1458 1459 1460
      username: username,
      email: email,
      &creation_block
1461
    )
J
James Lopez 已提交
1462

1463
    Users::UpdateService.new(user, user: user).execute(validate: false) # rubocop: disable CodeReuse/ServiceClass
1464
    user
1465 1466 1467
  ensure
    Gitlab::ExclusiveLease.cancel(lease_key, uuid)
  end
G
gitlabhq 已提交
1468
end