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

3 4
require 'carrierwave/orm/activerecord'

5
class User < ApplicationRecord
6
  extend Gitlab::ConfigHelper
7 8

  include Gitlab::ConfigHelper
9
  include Gitlab::SQL::Pattern
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
Jan Provaznik's avatar
Jan Provaznik committed
21
  include WithUploads
22
  include OptionallySearch
23
  include FromUnion
24

25 26
  DEFAULT_NOTIFICATION_LEVEL = :participating

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

31
  add_authentication_token_field :incoming_email_token, token_generator: -> { SecureRandom.hex.to_i(16).to_s(36) }
32
  add_authentication_token_field :feed_token
33

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

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

51
  devise :two_factor_authenticatable,
52
         otp_secret_encryption_key: Gitlab::Application.secrets.otp_key_base
53

54
  devise :two_factor_backupable, otp_number_of_backup_codes: 10
55
  serialize :otp_backup_codes, JSON # rubocop:disable Cop/ActiveRecordSerialize
56

57
  devise :lockable, :recoverable, :rememberable, :trackable,
58 59 60 61
         :validatable, :omniauthable, :confirmable, :registerable

  BLOCKED_MESSAGE = "Your account has been blocked. Please contact your GitLab " \
                    "administrator if you think this is an error.".freeze
gitlabhq's avatar
gitlabhq committed
62

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

69 70
    update_tracked_fields(request)

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

74
    Users::UpdateService.new(self, user: self).execute(validate: false)
75
  end
76
  # rubocop: enable CodeReuse/ServiceClass
77

78
  attr_accessor :force_random_password
gitlabhq's avatar
gitlabhq committed
79

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

83 84 85 86
  #
  # Relations
  #

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

  # Profile
91
  has_many :keys, -> { regular_keys }, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
92
  has_many :deploy_keys, -> { where(type: 'DeployKey') }, dependent: :nullify # rubocop:disable Cop/ActiveRecordDependent
93
  has_many :gpg_keys
94

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

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

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

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

128 129 130 131 132
  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
133
  has_many :releases,                 dependent: :nullify, foreign_key: :author_id # rubocop:disable Cop/ActiveRecordDependent
134 135 136 137 138 139 140
  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
141
  has_many :todos
142
  has_many :notification_settings
143 144
  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
145

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

150
  has_many :custom_attributes, class_name: 'UserCustomAttribute'
151
  has_many :callouts, class_name: 'UserCallout'
152 153
  has_many :term_agreements
  belongs_to :accepted_term, class_name: 'ApplicationSetting::Term'
154

155
  has_one :status, class_name: 'UserStatus'
156
  has_one :user_preference
157

158 159 160
  #
  # Validations
  #
161
  # Note: devise :validatable above adds validations for :email and :password
Cyril's avatar
Cyril committed
162
  validates :name, presence: true
Douwe Maan's avatar
Douwe Maan committed
163
  validates :email, confirmation: true
164
  validates :notification_email, presence: true
165 166 167
  validates :notification_email, devise_email: true, if: ->(user) { user.notification_email != user.email }
  validates :public_email, presence: true, uniqueness: true, devise_email: true, allow_blank: true
  validates :commit_email, devise_email: true, allow_nil: true, if: ->(user) { user.commit_email != user.email }
168
  validates :bio, length: { maximum: 255 }, allow_blank: true
169 170 171
  validates :projects_limit,
    presence: true,
    numericality: { greater_than_or_equal_to: 0, less_than_or_equal_to: Gitlab::Database::MAX_INT_VALUE }
172
  validates :username, presence: true
173

174
  validates :namespace, presence: true
175 176
  validate :namespace_move_dir_allowed, if: :username_changed?

177 178 179
  validate :unique_email, if: :email_changed?
  validate :owns_notification_email, if: :notification_email_changed?
  validate :owns_public_email, if: :public_email_changed?
180
  validate :owns_commit_email, if: :commit_email_changed?
181
  validate :signup_domain_valid?, on: :create, if: ->(user) { !user.created_by_id }
182

183
  before_validation :sanitize_attrs
184
  before_validation :set_notification_email, if: :new_record?
185
  before_validation :set_public_email, if: :public_email_changed?
186
  before_validation :set_commit_email, if: :commit_email_changed?
187
  before_save :set_public_email, if: :public_email_changed? # in case validation is skipped
188
  before_save :set_commit_email, if: :commit_email_changed? # in case validation is skipped
Douwe Maan's avatar
Douwe Maan committed
189
  before_save :ensure_incoming_email_token
190
  before_save :ensure_user_rights_and_limits, if: ->(user) { user.new_record? || user.external_changed? }
191
  before_save :skip_reconfirmation!, if: ->(user) { user.email_changed? && user.read_only_attribute?(:email) }
192
  before_save :check_for_verified_email, if: ->(user) { user.email_changed? && !user.new_record? }
193
  before_validation :ensure_namespace_correct
194
  before_save :ensure_namespace_correct # in case validation is skipped
195
  after_validation :set_username_errors
196
  after_update :username_changed_hook, if: :username_changed?
197
  after_destroy :post_destroy_hook
198
  after_destroy :remove_key_cache
199 200 201 202 203 204 205 206 207 208 209 210 211 212 213
  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
214

215
  after_initialize :set_projects_limit
216

217
  # User's Layout preference
218
  enum layout: [:fixed, :fluid]
219

220 221
  # User's Dashboard preference
  # Note: When adding an option, it MUST go on the end of the array.
222
  enum dashboard: [:projects, :stars, :project_activity, :starred_project_activity, :groups, :todos, :issues, :merge_requests, :operations]
223

224
  # User's Project preference
225 226 227
  # Note: When adding an option, it MUST go on the end of the array.
  enum project_view: [:readme, :activity, :files]

228
  delegate :path, to: :namespace, allow_nil: true, prefix: true
229 230
  delegate :notes_filter_for, to: :user_preference
  delegate :set_notes_filter, to: :user_preference
231 232 233
  delegate :first_day_of_week, :first_day_of_week=, to: :user_preference

  accepts_nested_attributes_for :user_preference, update_only: true
234

235 236 237
  state_machine :state, initial: :active do
    event :block do
      transition active: :blocked
238
      transition ldap_blocked: :blocked
239 240
    end

241 242 243 244
    event :ldap_block do
      transition active: :ldap_blocked
    end

245 246
    event :activate do
      transition blocked: :active
247
      transition ldap_blocked: :active
248
    end
249 250 251 252 253

    state :blocked, :ldap_blocked do
      def blocked?
        true
      end
254 255 256 257 258 259

      def active_for_authentication?
        false
      end

      def inactive_message
260
        BLOCKED_MESSAGE
261
      end
262
    end
263 264
  end

Andrey Kumanyaev's avatar
Andrey Kumanyaev committed
265
  # Scopes
266
  scope :admins, -> { where(admin: true) }
267
  scope :blocked, -> { with_states(:blocked, :ldap_blocked) }
268
  scope :external, -> { where(external: true) }
James Lopez's avatar
James Lopez committed
269
  scope :active, -> { with_state(:active).non_internal }
270
  scope :without_projects, -> { joins('LEFT JOIN project_authorizations ON users.id = project_authorizations.user_id').where(project_authorizations: { user_id: nil }) }
271 272
  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')) }
273 274
  scope :order_recent_last_activity, -> { reorder(Gitlab::Database.nulls_last_order('last_activity_on', 'DESC')) }
  scope :order_oldest_last_activity, -> { reorder(Gitlab::Database.nulls_first_order('last_activity_on', 'ASC')) }
275
  scope :confirmed, -> { where.not(confirmed_at: nil) }
276
  scope :by_username, -> (usernames) { iwhere(username: Array(usernames).map(&:to_s)) }
277
  scope :for_todos, -> (todos) { where(id: todos.select(:user_id)) }
278
  scope :with_emails, -> { preload(:emails) }
279

280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302
  # 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?
      # We use "unscoped" here so that any inner conditions are not repeated for
      # the outer query, which would be redundant.
303
      User.unscoped.from_union([all, User.unscoped.where(id: user_id)])
304 305 306 307 308
    else
      all
    end
  end

309
  def self.with_two_factor
310 311 312 313 314 315 316 317 318
    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)
319 320 321
  end

  def self.without_two_factor
322
    joins("LEFT OUTER JOIN u2f_registrations AS u2f ON u2f.user_id = users.id")
323
      .where("u2f.id IS NULL AND users.otp_required_for_login = ?", false)
324
  end
Andrey Kumanyaev's avatar
Andrey Kumanyaev committed
325

326 327 328
  #
  # Class methods
  #
Andrey Kumanyaev's avatar
Andrey Kumanyaev committed
329
  class << self
330
    # Devise method overridden to allow sign in with email or username
331 332 333
    def find_for_database_authentication(warden_conditions)
      conditions = warden_conditions.dup
      if login = conditions.delete(:login)
334
        where(conditions).find_by("lower(username) = :value OR lower(email) = :value", value: login.downcase.strip)
335
      else
Gabriel Mazetto's avatar
Gabriel Mazetto committed
336
        find_by(conditions)
337 338
      end
    end
339

340
    def sort_by_attribute(method)
341 342 343
      order_method = method || 'id_desc'

      case order_method.to_s
344 345
      when 'recent_sign_in' then order_recent_sign_in
      when 'oldest_sign_in' then order_oldest_sign_in
346 347
      when 'last_activity_on_desc' then order_recent_last_activity
      when 'last_activity_on_asc' then order_oldest_last_activity
348
      else
349
        order_by(order_method)
Valery Sizov's avatar
Valery Sizov committed
350 351 352
      end
    end

353
    def for_github_id(id)
354
      joins(:identities).merge(Identity.with_extern_uid(:github, id))
355 356
    end

357
    # Find a User by their primary email or any associated secondary email
358
    def find_by_any_email(email, confirmed: false)
359 360
      return unless email

361
      by_any_email(email, confirmed: confirmed).take
362 363
    end

364 365 366 367 368 369 370 371 372
    # Returns a relation containing all the users for the given email addresses
    #
    # @param emails [String, Array<String>] email addresses to check
    # @param confirmed [Boolean] Only return users where the email is confirmed
    def by_any_email(emails, confirmed: false)
      emails = Array(emails).map(&:downcase)

      from_users = where(email: emails)
      from_users = from_users.confirmed if confirmed
373

374
      from_emails = joins(:emails).where(emails: { email: emails })
375
      from_emails = from_emails.confirmed.merge(Email.confirmed) if confirmed
376

377 378 379 380 381 382
      items = [from_users, from_emails]

      user_ids = Gitlab::PrivateCommitEmail.user_ids_for_emails(emails)
      items << where(id: user_ids) if user_ids.present?

      from_union(items)
383
    end
384

385 386 387 388 389 390
    def find_by_private_commit_email(email)
      user_id = Gitlab::PrivateCommitEmail.user_id_for_email(email)

      find_by(id: user_id)
    end

391
    def filter_items(filter_name)
Andrey Kumanyaev's avatar
Andrey Kumanyaev committed
392
      case filter_name
393
      when 'admins'
394
        admins
395
      when 'blocked'
396
        blocked
397
      when 'two_factor_disabled'
398
        without_two_factor
399
      when 'two_factor_enabled'
400
        with_two_factor
401
      when 'wop'
402
        without_projects
403
      when 'external'
404
        external
Andrey Kumanyaev's avatar
Andrey Kumanyaev committed
405
      else
406
        active
Andrey Kumanyaev's avatar
Andrey Kumanyaev committed
407
      end
408 409
    end

410 411 412 413 414 415 416
    # 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.
417
    def search(query)
418 419
      return none if query.blank?

420 421
      query = query.downcase

422 423 424 425 426 427 428 429 430
      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

431
      where(
432 433
        fuzzy_arel_match(:name, query, lower_exact_match: true)
          .or(fuzzy_arel_match(:username, query, lower_exact_match: true))
434
          .or(arel_table[:email].eq(query))
435
      ).reorder(order % { query: ActiveRecord::Base.connection.quote(query) }, :name)
Andrey Kumanyaev's avatar
Andrey Kumanyaev committed
436
    end
437

438 439 440 441 442 443 444 445 446 447 448 449
    # 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

450 451 452 453 454
    # 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)
455 456
      return none if query.blank?

457 458
      query = query.downcase

459
      email_table = Email.arel_table
460 461
      matched_by_emails_user_ids = email_table
        .project(email_table[:user_id])
462
        .where(email_table[:email].eq(query))
463 464

      where(
465 466
        fuzzy_arel_match(:name, query)
          .or(fuzzy_arel_match(:username, query))
467
          .or(arel_table[:email].eq(query))
468
          .or(arel_table[:id].in(matched_by_emails_user_ids))
469 470 471
      )
    end

472
    def by_login(login)
473
      return unless login
474 475 476 477 478 479

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

482
    def find_by_username(username)
483
      by_username(username).take
484 485
    end

486
    def find_by_username!(username)
487
      by_username(username).take!
488 489
    end

490 491
    # Returns a user for the given SSH key.
    def find_by_ssh_key_id(key_id)
492
      Key.find_by(id: key_id)&.user
493 494
    end

495
    def find_by_full_path(path, follow_redirects: false)
496 497
      namespace = Namespace.for_user.find_by_full_path(path, follow_redirects: follow_redirects)
      namespace&.owner
498 499
    end

500 501 502
    def reference_prefix
      '@'
    end
503 504 505 506

    # Pattern used to extract `@user` user references from text
    def reference_pattern
      %r{
507
        (?<!\w)
508
        #{Regexp.escape(reference_prefix)}
509
        (?<user>#{Gitlab::PathRegex::FULL_NAMESPACE_FORMAT_REGEX})
510 511
      }x
    end
512 513 514 515

    # Return (create if necessary) the ghost user. The ghost user
    # owns records previously belonging to deleted users.
    def ghost
516 517
      email = 'ghost%s@example.com'
      unique_internal(where(ghost: true), 'ghost', email) do |u|
518 519 520
        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
521
    end
522 523 524 525 526 527 528 529 530 531

    # 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
vsizov's avatar
vsizov committed
532
  end
randx's avatar
randx committed
533

Michael Kozono's avatar
Michael Kozono committed
534 535 536 537
  def full_path
    username
  end

538 539 540 541
  def self.internal_attributes
    [:ghost]
  end

542
  def internal?
543 544 545 546 547 548 549 550
    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
551
    where(internal_attributes.map { |attr| "#{attr} IS NOT TRUE" }.join(" AND "))
552 553
  end

554 555 556
  #
  # Instance methods
  #
557 558 559 560 561

  def to_param
    username
  end

562
  def to_reference(_from = nil, target_project: nil, full: nil)
563 564 565
    "#{self.class.reference_prefix}#{username}"
  end

566 567
  def skip_confirmation=(bool)
    skip_confirmation! if bool
568 569 570 571
  end

  def skip_reconfirmation=(bool)
    skip_reconfirmation! if bool
randx's avatar
randx committed
572
  end
573

574
  def generate_reset_token
575
    @reset_token, enc = Devise.token_generator.generate(self.class, :reset_password_token)
576 577 578 579

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

580
    @reset_token
581 582
  end

583 584 585 586
  def recently_sent_password_reset?
    reset_password_sent_at.present? && reset_password_sent_at >= 1.minute.ago
  end

587 588 589 590 591 592 593 594
  def remember_me!
    super if ::Gitlab::Database.read_write?
  end

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

595
  def disable_two_factor!
596
    transaction do
Lin Jen-Shin's avatar
Lin Jen-Shin committed
597
      update(
598 599 600 601 602 603 604
        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
      )
605
      self.u2f_registrations.destroy_all # rubocop: disable DestroyAll
606 607 608 609 610 611 612 613
    end
  end

  def two_factor_enabled?
    two_factor_otp_enabled? || two_factor_u2f_enabled?
  end

  def two_factor_otp_enabled?
614
    otp_required_for_login?
615 616 617
  end

  def two_factor_u2f_enabled?
618 619 620 621 622
    if u2f_registrations.loaded?
      u2f_registrations.any?
    else
      u2f_registrations.exists?
    end
623 624
  end

625 626 627 628 629 630
  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

631
  def unique_email
632 633
    if !emails.exists?(email: email) && Email.exists?(email: email)
      errors.add(:email, 'has already been taken')
634
    end
635 636
  end

637
  def owns_notification_email
638
    return if temp_oauth_email?
639

640
    errors.add(:notification_email, "is not an email you own") unless all_emails.include?(notification_email)
641 642
  end

643
  def owns_public_email
644
    return if public_email.blank?
645

646
    errors.add(:public_email, "is not an email you own") unless all_emails.include?(public_email)
647 648
  end

649 650 651 652 653 654 655 656 657 658 659 660
  def owns_commit_email
    return if read_attribute(:commit_email).blank?

    errors.add(:commit_email, "is not an email you own") unless verified_emails.include?(commit_email)
  end

  # Define commit_email-related attribute methods explicitly instead of relying
  # on ActiveRecord to provide them. Some of the specs use the current state of
  # the model code but an older database schema, so we need to guard against the
  # possibility of the commit_email column not existing.

  def commit_email
661
    return self.email unless has_attribute?(:commit_email)
662

663 664 665 666
    if super == Gitlab::PrivateCommitEmail::TOKEN
      return private_commit_email
    end

667 668 669 670 671 672 673 674 675 676 677 678
    # The commit email is the same as the primary email if undefined
    super.presence || self.email
  end

  def commit_email=(email)
    super if has_attribute?(:commit_email)
  end

  def commit_email_changed?
    has_attribute?(:commit_email) && super
  end

679 680 681 682
  def private_commit_email
    Gitlab::PrivateCommitEmail.for_user(self)
  end

683 684 685 686 687
  # 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

688
  # Note: the use of the Emails services will cause `saves` on the user object, running
689
  # through the callbacks again and can have side effects, such as the `previous_changes`
690 691 692
  # 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
693
  # rubocop: disable CodeReuse/ServiceClass
694
  def update_emails_with_primary_email(previous_email)
695
    primary_email_record = emails.find_by(email: email)
696
    Emails::DestroyService.new(self, user: self).execute(primary_email_record) if primary_email_record
697

698 699
    # 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
700
    Emails::CreateService.new(self, user: self, email: previous_email).execute(confirmed_at: confirmed_at)
701
  end
702
  # rubocop: enable CodeReuse/ServiceClass
703

704
  def update_invalid_gpg_signatures
705
    gpg_keys.each(&:update_invalid_gpg_signatures)
706 707
  end

708
  # Returns the groups a user has access to, either through a membership or a project authorization
709
  def authorized_groups
710 711 712 713 714 715
    Group.unscoped do
      Group.from_union([
        groups,
        authorized_projects.joins(:namespace).select('namespaces.*')
      ])
    end
716 717
  end

718 719
  # Returns the groups a user is a member of, either directly or through a parent group
  def membership_groups
720
    Gitlab::ObjectHierarchy.new(groups).base_and_descendants
721 722
  end

723 724
  # Returns a relation of groups the user has access to, including their parent
  # and child groups (recursively).
725
  def all_expanded_groups
726
    Gitlab::ObjectHierarchy.new(groups).all_objects
727 728 729 730 731 732
  end

  def expanded_groups_requiring_two_factor_authentication
    all_expanded_groups.where(require_two_factor_authentication: true)
  end

733
  # rubocop: disable CodeReuse/ServiceClass
734
  def refresh_authorized_projects
735 736
    Users::RefreshAuthorizedProjectsService.new(self).execute
  end
737
  # rubocop: enable CodeReuse/ServiceClass
738 739

  def remove_project_authorizations(project_ids)
740
    project_authorizations.where(project_id: project_ids).delete_all
741 742
  end

743
  def authorized_projects(min_access_level = nil)
744 745
    # We're overriding an association, so explicitly call super with no
    # arguments or it would be passed as `force_reload` to the association
746
    projects = super()
747 748

    if min_access_level
749 750
      projects = projects
        .where('project_authorizations.access_level >= ?', min_access_level)
751
    end
752 753 754 755 756 757

    projects
  end

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

760 761 762 763 764
  # 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)`
765 766 767 768 769 770
  def authorizations_for_projects(min_access_level: nil)
    authorizations = project_authorizations.select(1).where('project_authorizations.project_id = projects.id')

    return authorizations unless min_access_level.present?

    authorizations.where('project_authorizations.access_level >= ?', min_access_level)
771 772
  end

773 774 775 776 777 778 779 780 781 782
  # 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

783
  def owned_projects
784 785 786 787 788 789 790 791 792
    @owned_projects ||= Project.from_union(
      [
        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
    )
793 794
  end

795 796 797 798
  # 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
799
    authorized_projects(Gitlab::Access::REPORTER).non_archived.with_issues_enabled
800 801
  end

802
  # rubocop: disable CodeReuse/ServiceClass
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
803
  def require_ssh_key?
804 805 806
    count = Users::KeysCountService.new(self).count

    count.zero? && Gitlab::ProtocolAccess.allowed?('ssh')
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
807
  end
808
  # rubocop: enable CodeReuse/ServiceClass
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
809

810 811 812 813 814 815
  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?
816 817
  end

818
  def require_personal_access_token_creation_for_git_auth?
819
    return false if allow_password_authentication_for_git? || ldap_user?
820 821

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

824 825 826 827
  def require_extra_setup_for_git_auth?
    require_password_creation_for_git? || require_personal_access_token_creation_for_git_auth?
  end

828
  def allow_password_authentication?
829 830 831 832
    allow_password_authentication_for_web? || allow_password_authentication_for_git?
  end

  def allow_password_authentication_for_web?
833
    Gitlab::CurrentSettings.password_authentication_enabled_for_web? && !ldap_user?
834 835 836
  end

  def allow_password_authentication_for_git?
837
    Gitlab::CurrentSettings.password_authentication_enabled_for_git? && !ldap_user?
838 839
  end

840
  def can_change_username?
841
    gitlab_config.username_changing_enabled
842 843
  end

Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
844
  def can_create_project?
845
    projects_limit_left > 0
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
846 847 848
  end

  def can_create_group?
849
    can?(:create_group)
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
850 851
  end

852 853 854 855
  def can_select_namespace?
    several_namespaces? || admin
  end

856
  def can?(action, subject = :global)
857
    Ability.allowed?(self, action, subject)
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
858 859
  end

860 861 862 863
  def confirm_deletion_with_password?
    !password_automatically_set? && allow_password_authentication?
  end

Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
864 865 866 867
  def first_name
    name.split.first unless name.blank?
  end

868
  def projects_limit_left
869 870 871
    projects_limit - personal_projects_count
  end

872
  # rubocop: disable CodeReuse/ServiceClass
873 874
  def recent_push(project = nil)
    service = Users::LastPushEventService.new(self)
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
875

876 877 878 879
    if project
      service.last_event_for_project(project)
    else
      service.last_event_for_user
880
    end
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
881
  end
882
  # rubocop: enable CodeReuse/ServiceClass
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
883 884

  def several_namespaces?
885
    owned_groups.any? || maintainers_groups.any?
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
886 887 888 889 890
  end

  def namespace_id
    namespace.try :id
  end
891

892 893 894
  def name_with_username
    "#{name} (#{username})"
  end
895

896
  def already_forked?(project)
897 898 899
    !!fork_of(project)
  end

900
  def fork_of(project)
901
    namespace.find_fork_of(project)
902
  end
903 904

  def ldap_user?
905
    if identities.loaded?
906
      identities.find { |identity| Gitlab::Auth::OAuth::Provider.ldap_provider?(identity.provider) && !identity.extern_uid.nil? }
907 908 909
    else
      identities.exists?(["provider LIKE ? AND extern_uid IS NOT NULL", "ldap%"])
    end
910 911 912 913
  end

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

916
  def project_deploy_keys
917
    DeployKey.unscoped.in_projects(authorized_projects.pluck(:id)).distinct(:id)
918 919
  end

920 921 922 923
  def highest_role
    members.maximum(:access_level) || Gitlab::Access::NO_ACCESS
  end

924
  def accessible_deploy_keys
925 926 927 928 929
    @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
930
  end
931 932

  def created_by
skv's avatar
skv committed
933
    User.find_by(id: created_by_id) if created_by_id
934
  end
935 936

  def sanitize_attrs
937 938 939
    %i[skype linkedin twitter].each do |attr|
      value = self[attr]
      self[attr] = Sanitize.clean(value) if value.present?
940 941
    end
  end
942

943
  def set_notification_email
944
    if notification_email.blank? || all_emails.exclude?(notification_email)
945
      self.notification_email = email
946 947 948
    end
  end

949
  def set_public_email
950
    if public_email.blank? || all_emails.exclude?(public_email)
951
      self.public_email = ''
952 953 954
    end
  end

955 956 957 958 959 960
  def set_commit_email
    if commit_email.blank? || verified_emails.exclude?(commit_email)
      self.commit_email = nil
    end
  end

961
  def update_secondary_emails!
962 963
    set_notification_email
    set_public_email
964 965
    set_commit_email
    save if notification_email_changed? || public_email_changed? || commit_email_changed?
966 967
  end

968
  def set_projects_limit
969 970 971
    # `User.select(:id)` raises
    # `ActiveModel::MissingAttributeError: missing attribute: projects_limit`
    # without this safeguard!
972
    return unless has_attribute?(:projects_limit) && projects_limit.nil?
973

974
    self.projects_limit = Gitlab::CurrentSettings.default_projects_limit
975 976
  end

977
  def requires_ldap_check?
978 979 980
    if !Gitlab.config.ldap.enabled
      false
    elsif ldap_user?
981
      !last_credential_check_at || (last_credential_check_at + ldap_sync_time) < Time.now
982 983 984 985 986
    else
      false
    end
  end

987 988 989 990 991
  def ldap_sync_time
    # This number resides in this method so it can be redefined in EE.
    1.hour
  end

Jacob Vosmaer's avatar
Jacob Vosmaer committed
992 993 994 995 996 997 998
  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

999 1000 1001 1002 1003
  def solo_owned_groups
    @solo_owned_groups ||= owned_groups.select do |group|
      group.owners == [self]
    end
  end
1004 1005

  def with_defaults
1006
    User.defaults.each do |k, v|
1007
      public_send("#{k}=", v) # rubocop:disable GitlabSecurity/PublicSend
1008
    end
1009 1010

    self
1011
  end
1012

1013 1014 1015 1016
  def can_leave_project?(project)
    project.namespace != namespace &&
      project.project_member(self)
  end
1017

Jerome Dalbert's avatar
Jerome Dalbert committed
1018
  def full_website_url
1019
    return "http://#{website_url}" if website_url !~ %r{\Ahttps?://}
Jerome Dalbert's avatar
Jerome Dalbert committed
1020 1021 1022 1023 1024

    website_url
  end

  def short_website_url
1025
    website_url.sub(%r{\Ahttps?://}, '')
Jerome Dalbert's avatar
Jerome Dalbert committed
1026
  end
GitLab's avatar
GitLab committed
1027

1028
  def all_ssh_keys
1029
    keys.map(&:publishable_key)
1030
  end
1031 1032

  def temp_oauth_email?
1033
    email.start_with?('temp-email-for-oauth')
1034 1035
  end

1036
  # rubocop: disable CodeReuse/ServiceClass
1037
  def avatar_url(size: nil, scale: 2, **args)
1038
    GravatarService.new.execute(email, size, scale, username: username)
1039
  end
1040
  # rubocop: enable CodeReuse/ServiceClass
1041

1042 1043 1044 1045
  def primary_email_verified?
    confirmed? && !temp_oauth_email?
  end

1046 1047 1048 1049 1050 1051 1052 1053 1054 1055
  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

1056
  def all_emails
1057
    all_emails = []
1058
    all_emails << email unless temp_oauth_email?
1059
    all_emails << private_commit_email
1060
    all_emails.concat(emails.map(&:email))
1061
    all_emails
1062 1063
  end

1064
  def verified_emails
1065
    verified_emails = []
1066
    verified_emails << email if primary_email_verified?
1067
    verified_emails << private_commit_email
1068
    verified_emails.concat(emails.confirmed.pluck(:email))
1069 1070 1071
    verified_emails
  end

1072 1073 1074 1075 1076 1077 1078 1079 1080 1081
  def any_email?(check_email)
    downcased = check_email.downcase

    # handle the outdated private commit email case
    return true if persisted? &&
        id == Gitlab::PrivateCommitEmail.user_id_for_email(downcased)

    all_emails.include?(check_email.downcase)
  end

1082
  def verified_email?(check_email)
1083
    downcased = check_email.downcase
1084

1085 1086 1087
    # handle the outdated private commit email case
    return true if persisted? &&
        id == Gitlab::PrivateCommitEmail.user_id_for_email(downcased)
1088

1089
    verified_emails.include?(check_email.downcase)
1090 1091
  end

Kirill Zaitsev's avatar
Kirill Zaitsev committed
1092 1093 1094 1095
  def hook_attrs
    {
      name: name,
      username: username,
1096
      avatar_url: avatar_url(only_path: false)
Kirill Zaitsev's avatar
Kirill Zaitsev committed
1097 1098 1099
    }
  end

1100
  def ensure_namespace_correct
1101 1102 1103 1104
    if namespace
      namespace.path = namespace.name = username if username_changed?
    else
      build_namespace(path: username, name: username)
1105 1106 1107
    end
  end

1108 1109 1110 1111 1112
  def set_username_errors
    namespace_path_errors = self.errors.delete(:"namespace.path")
    self.errors[:username].concat(namespace_path_errors) if namespace_path_errors
  end

1113 1114 1115 1116
  def username_changed_hook
    system_hook_service.execute_hooks_for(self, :rename)
  end

1117
  def post_destroy_hook
1118
    log_info("User \"#{name}\" (#{email})  was removed")
1119

1120 1121 1122
    system_hook_service.execute_hooks_for(self, :destroy)
  end

1123
  # rubocop: disable CodeReuse/ServiceClass
1124 1125 1126
  def remove_key_cache
    Users::KeysCountService.new(self).delete_cache
  end
1127
  # rubocop: enable CodeReuse/ServiceClass
1128

1129 1130
  def delete_async(deleted_by:, params: {})
    block if params[:hard_delete]
1131