Read about our upcoming Code of Conduct on this issue

This instance will be upgraded to Heptapod 0.28.1 on 2022-01-26 at 16:00 UTC+1 (a few minutes of down time)

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

Steven Thonus's avatar
Steven Thonus committed
3
4
require 'carrierwave/orm/activerecord'

gitlabhq's avatar
gitlabhq committed
5
class User < ActiveRecord::Base
6
  extend Gitlab::ConfigHelper
7
8

  include Gitlab::ConfigHelper
Hiroyuki Sato's avatar
Hiroyuki Sato committed
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
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
Robert Speicher's avatar
Robert Speicher committed
53

54
  devise :two_factor_backupable, otp_number_of_backup_codes: 10
55
  serialize :otp_backup_codes, JSON # rubocop:disable Cop/ActiveRecordSerialize
Robert Speicher's avatar
Robert Speicher committed
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

James Lopez's avatar
James Lopez committed
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
92
  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
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
133
134
135
136
137
138
139
  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
140
  has_many :todos
141
  has_many :notification_settings
142
143
  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
144

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

149
  has_many :custom_attributes, class_name: 'UserCustomAttribute'
Matija Čupić's avatar
Matija Čupić committed
150
  has_many :callouts, class_name: 'UserCallout'
151
152
  has_many :term_agreements
  belongs_to :accepted_term, class_name: 'ApplicationSetting::Term'
153

Bob Van Landuyt's avatar
Bob Van Landuyt committed
154
155
  has_one :status, class_name: 'UserStatus'

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

172
  validates :namespace, presence: true
173
174
  validate :namespace_move_dir_allowed, if: :username_changed?

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

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

213
  after_initialize :set_projects_limit
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
214

215
  # User's Layout preference
216
  enum layout: [:fixed, :fluid]
217

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

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

226
  delegate :path, to: :namespace, allow_nil: true, prefix: true
227

228
229
230
  state_machine :state, initial: :active do
    event :block do
      transition active: :blocked
231
      transition ldap_blocked: :blocked
232
233
    end

234
235
236
237
    event :ldap_block do
      transition active: :ldap_blocked
    end

238
239
    event :activate do
      transition blocked: :active
240
      transition ldap_blocked: :active
241
    end
242
243
244
245
246

    state :blocked, :ldap_blocked do
      def blocked?
        true
      end
247
248
249
250
251
252

      def active_for_authentication?
        false
      end

      def inactive_message
253
        BLOCKED_MESSAGE
254
      end
255
    end
256
257
  end

Andrey Kumanyaev's avatar
Andrey Kumanyaev committed
258
  # Scopes
259
  scope :admins, -> { where(admin: true) }
260
  scope :blocked, -> { with_states(:blocked, :ldap_blocked) }
261
  scope :external, -> { where(external: true) }
James Lopez's avatar
James Lopez committed
262
  scope :active, -> { with_state(:active).non_internal }
263
  scope :without_projects, -> { joins('LEFT JOIN project_authorizations ON users.id = project_authorizations.user_id').where(project_authorizations: { user_id: nil }) }
264
265
  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')) }
266
  scope :confirmed, -> { where.not(confirmed_at: nil) }
267
  scope :by_username, -> (usernames) { iwhere(username: usernames) }
268

269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
  # 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.
292
      User.unscoped.from_union([all, User.unscoped.where(id: user_id)])
293
294
295
296
297
    else
      all
    end
  end

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

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

315
316
317
  #
  # Class methods
  #
Andrey Kumanyaev's avatar
Andrey Kumanyaev committed
318
  class << self
319
    # Devise method overridden to allow sign in with email or username
320
321
322
    def find_for_database_authentication(warden_conditions)
      conditions = warden_conditions.dup
      if login = conditions.delete(:login)
323
        where(conditions).find_by("lower(username) = :value OR lower(email) = :value", value: login.downcase.strip)
324
      else
Gabriel Mazetto's avatar
Gabriel Mazetto committed
325
        find_by(conditions)
326
327
      end
    end
328

329
    def sort_by_attribute(method)
330
331
332
      order_method = method || 'id_desc'

      case order_method.to_s
333
334
      when 'recent_sign_in' then order_recent_sign_in
      when 'oldest_sign_in' then order_oldest_sign_in
335
      else
336
        order_by(order_method)
Valery Sizov's avatar
Valery Sizov committed
337
338
339
      end
    end

340
    def for_github_id(id)
341
      joins(:identities).merge(Identity.with_extern_uid(:github, id))
342
343
    end

344
    # Find a User by their primary email or any associated secondary email
345
346
    def find_by_any_email(email, confirmed: false)
      by_any_email(email, confirmed: confirmed).take
347
348
349
    end

    # Returns a relation containing all the users for the given Email address
350
    def by_any_email(email, confirmed: false)
351
      users = where(email: email)
352
353
      users = users.confirmed if confirmed

354
      emails = joins(:emails).where(emails: { email: email })
355
      emails = emails.confirmed if confirmed
356

357
      from_union([users, emails])
358
    end
359

360
    def filter(filter_name)
Andrey Kumanyaev's avatar
Andrey Kumanyaev committed
361
      case filter_name
362
      when 'admins'
363
        admins
364
      when 'blocked'
365
        blocked
366
      when 'two_factor_disabled'
367
        without_two_factor
368
      when 'two_factor_enabled'
369
        with_two_factor
370
      when 'wop'
371
        without_projects
372
      when 'external'
373
        external
Andrey Kumanyaev's avatar
Andrey Kumanyaev committed
374
      else
375
        active
Andrey Kumanyaev's avatar
Andrey Kumanyaev committed
376
      end
377
378
    end

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

389
390
      query = query.downcase

391
392
393
394
395
396
397
398
399
      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

400
      where(
401
402
        fuzzy_arel_match(:name, query, lower_exact_match: true)
          .or(fuzzy_arel_match(:username, query, lower_exact_match: true))
403
          .or(arel_table[:email].eq(query))
404
      ).reorder(order % { query: ActiveRecord::Base.connection.quote(query) }, :name)
Andrey Kumanyaev's avatar
Andrey Kumanyaev committed
405
    end
406

407
408
409
410
411
412
413
414
415
416
417
418
    # 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

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

426
427
      query = query.downcase

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

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

441
    def by_login(login)
442
443
444
445
446
447
448
      return nil unless login

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

451
    def find_by_username(username)
452
      by_username(username).take
453
454
    end

Robert Speicher's avatar
Robert Speicher committed
455
    def find_by_username!(username)
456
      by_username(username).take!
Robert Speicher's avatar
Robert Speicher committed
457
458
    end

Timothy Andrew's avatar
Timothy Andrew committed
459
    def find_by_personal_access_token(token_string)
460
461
      return unless token_string

462
      PersonalAccessTokensFinder.new(state: 'active').find_by(token: token_string)&.user # rubocop: disable CodeReuse/Finder
Timothy Andrew's avatar
Timothy Andrew committed
463
464
    end

Yorick Peterse's avatar
Yorick Peterse committed
465
466
    # Returns a user for the given SSH key.
    def find_by_ssh_key_id(key_id)
467
      Key.find_by(id: key_id)&.user
Yorick Peterse's avatar
Yorick Peterse committed
468
469
    end

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

475
476
477
    def reference_prefix
      '@'
    end
478
479
480
481

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

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

    # 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
507
  end
randx's avatar
randx committed
508

Michael Kozono's avatar
Michael Kozono committed
509
510
511
512
  def full_path
    username
  end

513
514
515
516
  def self.internal_attributes
    [:ghost]
  end

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

529
530
531
  #
  # Instance methods
  #
532
533
534
535
536

  def to_param
    username
  end

537
  def to_reference(_from = nil, target_project: nil, full: nil)
538
539
540
    "#{self.class.reference_prefix}#{username}"
  end

541
542
  def skip_confirmation=(bool)
    skip_confirmation! if bool
Daniel Juarez's avatar
Daniel Juarez committed
543
544
545
546
  end

  def skip_reconfirmation=(bool)
    skip_reconfirmation! if bool
randx's avatar
randx committed
547
  end
548

549
  def generate_reset_token
Marin Jankovski's avatar
Marin Jankovski committed
550
    @reset_token, enc = Devise.token_generator.generate(self.class, :reset_password_token)
551
552
553
554

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

Marin Jankovski's avatar
Marin Jankovski committed
555
    @reset_token
556
557
  end

558
559
560
561
  def recently_sent_password_reset?
    reset_password_sent_at.present? && reset_password_sent_at >= 1.minute.ago
  end

Toon Claes's avatar
Toon Claes committed
562
563
564
565
566
567
568
569
  def remember_me!
    super if ::Gitlab::Database.read_write?
  end

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

Robert Speicher's avatar
Robert Speicher committed
570
  def disable_two_factor!
571
    transaction do
Lin Jen-Shin's avatar
Lin Jen-Shin committed
572
      update(
573
574
575
576
577
578
579
        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
      )
580
      self.u2f_registrations.destroy_all # rubocop: disable DestroyAll
581
582
583
584
585
586
587
588
    end
  end

  def two_factor_enabled?
    two_factor_otp_enabled? || two_factor_u2f_enabled?
  end

  def two_factor_otp_enabled?
589
    otp_required_for_login?
590
591
592
  end

  def two_factor_u2f_enabled?
593
594
595
596
597
    if u2f_registrations.loaded?
      u2f_registrations.any?
    else
      u2f_registrations.exists?
    end
Robert Speicher's avatar
Robert Speicher committed
598
599
  end

600
601
602
603
604
605
  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

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

612
  def owns_notification_email
613
    return if temp_oauth_email?
614

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

618
  def owns_public_email
619
    return if public_email.blank?
620

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

624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
  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
    return unless has_attribute?(:commit_email)

    # 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

Alexandra's avatar
Alexandra committed
650
651
652
653
654
  # 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

655
  # Note: the use of the Emails services will cause `saves` on the user object, running
656
  # through the callbacks again and can have side effects, such as the `previous_changes`
657
658
659
  # 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
660
  # rubocop: disable CodeReuse/ServiceClass
661
  def update_emails_with_primary_email(previous_email)
662
    primary_email_record = emails.find_by(email: email)
663
    Emails::DestroyService.new(self, user: self).execute(primary_email_record) if primary_email_record
664

665
666
    # 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
667
    Emails::CreateService.new(self, user: self, email: previous_email).execute(confirmed_at: confirmed_at)
668
  end
669
  # rubocop: enable CodeReuse/ServiceClass
670

671
  def update_invalid_gpg_signatures
672
    gpg_keys.each(&:update_invalid_gpg_signatures)
673
674
  end

675
  # Returns the groups a user has access to, either through a membership or a project authorization
676
  def authorized_groups
677
678
679
680
    Group.from_union([
      groups,
      authorized_projects.joins(:namespace).select('namespaces.*')
    ])
681
682
  end

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

688
689
  # Returns a relation of groups the user has access to, including their parent
  # and child groups (recursively).
690
  def all_expanded_groups
691
    Gitlab::GroupHierarchy.new(groups).all_groups
692
693
694
695
696
697
  end

  def expanded_groups_requiring_two_factor_authentication
    all_expanded_groups.where(require_two_factor_authentication: true)
  end

698
  # rubocop: disable CodeReuse/ServiceClass
699
  def refresh_authorized_projects
700
701
    Users::RefreshAuthorizedProjectsService.new(self).execute
  end
702
  # rubocop: enable CodeReuse/ServiceClass
703
704

  def remove_project_authorizations(project_ids)
705
    project_authorizations.where(project_id: project_ids).delete_all
706
707
  end

708
  def authorized_projects(min_access_level = nil)
709
710
    # We're overriding an association, so explicitly call super with no
    # arguments or it would be passed as `force_reload` to the association
711
    projects = super()
712
713

    if min_access_level
714
715
      projects = projects
        .where('project_authorizations.access_level >= ?', min_access_level)
716
    end
717
718
719
720
721
722

    projects
  end

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

725
726
727
728
729
730
731
732
733
  # 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

734
735
736
737
738
739
740
741
742
743
  # 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

744
  def owned_projects
745
746
747
748
749
750
751
752
753
    @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
    )
754
755
  end

756
757
758
759
  # 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
Felipe Artur's avatar
Felipe Artur committed
760
    authorized_projects(Gitlab::Access::REPORTER).non_archived.with_issues_enabled
761
762
  end

763
  # rubocop: disable CodeReuse/ServiceClass
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
764
  def require_ssh_key?
765
766
767
    count = Users::KeysCountService.new(self).count

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

771
772
773
774
775
776
  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?
777
778
  end

779
  def require_personal_access_token_creation_for_git_auth?
780
    return false if allow_password_authentication_for_git? || ldap_user?
781
782

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

785
786
787
788
  def require_extra_setup_for_git_auth?
    require_password_creation_for_git? || require_personal_access_token_creation_for_git_auth?
  end

789
  def allow_password_authentication?
790
791
792
793
    allow_password_authentication_for_web? || allow_password_authentication_for_git?
  end

  def allow_password_authentication_for_web?
794
    Gitlab::CurrentSettings.password_authentication_enabled_for_web? && !ldap_user?
795
796
797
  end

  def allow_password_authentication_for_git?
798
    Gitlab::CurrentSettings.password_authentication_enabled_for_git? && !ldap_user?
799
800
  end

801
  def can_change_username?
802
    gitlab_config.username_changing_enabled
803
804
  end

Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
805
  def can_create_project?
806
    projects_limit_left > 0
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
807
808
809
  end

  def can_create_group?
810
    can?(:create_group)
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
811
812
  end

813
814
815
816
  def can_select_namespace?
    several_namespaces? || admin
  end

817
  def can?(action, subject = :global)
http://jneen.net/'s avatar
http://jneen.net/ committed
818
    Ability.allowed?(self, action, subject)
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
819
820
  end

821
822
823
824
  def confirm_deletion_with_password?
    !password_automatically_set? && allow_password_authentication?
  end

Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
825
826
827
828
  def first_name
    name.split.first unless name.blank?
  end

829
  def projects_limit_left
830
831
832
    projects_limit - personal_projects_count
  end

833
  # rubocop: disable CodeReuse/ServiceClass
834
835
  def recent_push(project = nil)
    service = Users::LastPushEventService.new(self)
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
836

837
838
839
840
    if project
      service.last_event_for_project(project)
    else
      service.last_event_for_user
841
    end
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
842
  end
843
  # rubocop: enable CodeReuse/ServiceClass
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
844
845

  def several_namespaces?
846
    owned_groups.any? || maintainers_groups.any?
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
847
848
849
850
851
  end

  def namespace_id
    namespace.try :id
  end
852

853
854
855
  def name_with_username
    "#{name} (#{username})"
  end
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
856

857
  def already_forked?(project)
858
859
860
    !!fork_of(project)
  end

861
  def fork_of(project)
862
    namespace.find_fork_of(project)
863
  end
864
865

  def ldap_user?
866
    if identities.loaded?
867
      identities.find { |identity| Gitlab::Auth::OAuth::Provider.ldap_provider?(identity.provider) && !identity.extern_uid.nil? }
868
869
870
    else
      identities.exists?(["provider LIKE ? AND extern_uid IS NOT NULL", "ldap%"])
    end
871
872
873
874
  end

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

877
  def project_deploy_keys
878
    DeployKey.unscoped.in_projects(authorized_projects.pluck(:id)).distinct(:id)
879
880
  end

881
  def accessible_deploy_keys
882
883
884
885
886
    @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
887
  end
888
889

  def created_by
skv's avatar
skv committed
890
    User.find_by(id: created_by_id) if created_by_id
891
  end
892
893

  def sanitize_attrs
894
895
896
    %i[skype linkedin twitter].each do |attr|
      value = self[attr]
      self[attr] = Sanitize.clean(value) if value.present?
897
898
    end
  end
899

900
  def set_notification_email
901
    if notification_email.blank? || all_emails.exclude?(notification_email)
902
      self.notification_email = email
903
904
905
    end
  end

906
  def set_public_email
907
    if public_email.blank? || all_emails.exclude?(public_email)
908
      self.public_email = ''
909
910
911
    end
  end

912
913
914
915
916
917
  def set_commit_email
    if commit_email.blank? || verified_emails.exclude?(commit_email)
      self.commit_email = nil
    end
  end

918
  def update_secondary_emails!
919
920
    set_notification_email
    set_public_email
921
922
    set_commit_email
    save if notification_email_changed? || public_email_changed? || commit_email_changed?
923
924
  end

925
  def set_projects_limit