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

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

Mark Chao's avatar
Mark Chao committed
5
class User < ApplicationRecord
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, 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
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
  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
Gosia Ksionek's avatar
Gosia Ksionek committed
108
  has_many :developer_groups, -> { where(members: { access_level: ::Gitlab::Access::DEVELOPER }) }, through: :group_members, source: :group
109
110
111
112
  has_many :owned_or_maintainers_groups,
           -> { where(members: { access_level: [Gitlab::Access::MAINTAINER, Gitlab::Access::OWNER] }) },
           through: :group_members,
           source: :group
113
  alias_attribute :masters_groups, :maintainers_groups
114

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

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

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

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

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

Bob Van Landuyt's avatar
Bob Van Landuyt committed
156
  has_one :status, class_name: 'UserStatus'
157
  has_one :user_preference
Bob Van Landuyt's avatar
Bob Van Landuyt committed
158

159
160
161
  #
  # Validations
  #
162
  # Note: devise :validatable above adds validations for :email and :password
163
  validates :name, presence: true, length: { maximum: 128 }
Douwe Maan's avatar
Douwe Maan committed
164
  validates :email, confirmation: true
165
  validates :notification_email, presence: true
166
167
168
  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 }
169
  validates :bio, length: { maximum: 255 }, allow_blank: true
170
171
172
  validates :projects_limit,
    presence: true,
    numericality: { greater_than_or_equal_to: 0, less_than_or_equal_to: Gitlab::Database::MAX_INT_VALUE }
173
  validates :username, presence: true
174

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

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

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

216
  after_initialize :set_projects_limit
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
217

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

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

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

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

  accepts_nested_attributes_for :user_preference, update_only: true
235

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

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

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

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

      def active_for_authentication?
        false
      end

      def inactive_message
261
        BLOCKED_MESSAGE
262
      end
263
    end
264
265
  end

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

282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
  # 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.
305
      User.unscoped.from_union([all, User.unscoped.where(id: user_id)])
306
307
308
309
310
    else
      all
    end
  end

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

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

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

342
    def sort_by_attribute(method)
343
344
345
      order_method = method || 'id_desc'

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

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

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

363
      by_any_email(email, confirmed: confirmed).take
364
365
    end

366
367
368
369
370
371
372
373
374
    # 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
375

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

379
380
381
382
383
384
      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)
385
    end
386

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

      find_by(id: user_id)
    end

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

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

422
423
      query = query.downcase

424
425
426
427
428
429
430
431
432
      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

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

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

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

459
460
      query = query.downcase

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

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

474
    def by_login(login)
475
      return unless login
476
477
478
479
480
481

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

484
    def find_by_username(username)
485
      by_username(username).take
486
487
    end

Robert Speicher's avatar
Robert Speicher committed
488
    def find_by_username!(username)
489
      by_username(username).take!
Robert Speicher's avatar
Robert Speicher committed
490
491
    end

Yorick Peterse's avatar
Yorick Peterse committed
492
493
    # Returns a user for the given SSH key.
    def find_by_ssh_key_id(key_id)
494
      Key.find_by(id: key_id)&.user
Yorick Peterse's avatar
Yorick Peterse committed
495
496
    end

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

502
503
504
    def reference_prefix
      '@'
    end
505
506
507
508

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

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

    # 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
534
  end
randx's avatar
randx committed
535

Michael Kozono's avatar
Michael Kozono committed
536
537
538
539
  def full_path
    username
  end

540
  def internal?
541
    ghost?
542
543
544
  end

  def self.internal
545
    where(ghost: true)
546
547
548
  end

  def self.non_internal
549
    where('ghost IS NOT TRUE')
550
551
  end

552
553
554
  #
  # Instance methods
  #
555
556
557
558
559

  def to_param
    username
  end

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

564
565
  def skip_confirmation=(bool)
    skip_confirmation! if bool
Daniel Juarez's avatar
Daniel Juarez committed
566
567
568
569
  end

  def skip_reconfirmation=(bool)
    skip_reconfirmation! if bool
randx's avatar
randx committed
570
  end
571

572
  def generate_reset_token
Marin Jankovski's avatar
Marin Jankovski committed
573
    @reset_token, enc = Devise.token_generator.generate(self.class, :reset_password_token)
574
575
576
577

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

Marin Jankovski's avatar
Marin Jankovski committed
578
    @reset_token
579
580
  end

581
582
583
584
  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
585
586
587
588
589
590
591
592
  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
593
  def disable_two_factor!
594
    transaction do
Lin Jen-Shin's avatar
Lin Jen-Shin committed
595
      update(
596
597
598
599
600
601
602
        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
      )
603
      self.u2f_registrations.destroy_all # rubocop: disable DestroyAll
604
605
606
607
608
609
610
611
    end
  end

  def two_factor_enabled?
    two_factor_otp_enabled? || two_factor_u2f_enabled?
  end

  def two_factor_otp_enabled?
612
    otp_required_for_login?
613
614
615
  end

  def two_factor_u2f_enabled?
616
617
618
619
620
    if u2f_registrations.loaded?
      u2f_registrations.any?
    else
      u2f_registrations.exists?
    end
Robert Speicher's avatar
Robert Speicher committed
621
622
  end

623
624
625
626
627
628
  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

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

635
  def owns_notification_email
636
    return if temp_oauth_email?
637

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

641
  def owns_public_email
642
    return if public_email.blank?
643

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

647
648
649
650
651
652
653
654
655
656
657
658
  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
659
    return self.email unless has_attribute?(:commit_email)
660

661
662
663
664
    if super == Gitlab::PrivateCommitEmail::TOKEN
      return private_commit_email
    end

665
666
667
668
669
670
671
672
673
674
675
676
    # 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

677
678
679
680
  def private_commit_email
    Gitlab::PrivateCommitEmail.for_user(self)
  end

Alexandra's avatar
Alexandra committed
681
682
683
684
685
  # 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

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

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

702
  def update_invalid_gpg_signatures
703
    gpg_keys.each(&:update_invalid_gpg_signatures)
704
705
  end

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

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

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

  def expanded_groups_requiring_two_factor_authentication
    all_expanded_groups.where(require_two_factor_authentication: true)
  end

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

  def remove_project_authorizations(project_ids)
738
    project_authorizations.where(project_id: project_ids).delete_all
739
740
  end

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

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

    projects
  end

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

758
759
760
761
762
  # 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)`
763
764
765
766
767
768
  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)
769
770
  end

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

781
  def owned_projects
782
783
784
785
786
787
788
789
790
    @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
    )
791
792
  end

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

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

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

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

816
  def require_personal_access_token_creation_for_git_auth?
817
    return false if allow_password_authentication_for_git? || ldap_user?
818
819

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

822
823
824
825
  def require_extra_setup_for_git_auth?
    require_password_creation_for_git? || require_personal_access_token_creation_for_git_auth?
  end

826
  def allow_password_authentication?
827
828
829
830
    allow_password_authentication_for_web? || allow_password_authentication_for_git?
  end

  def allow_password_authentication_for_web?
831
    Gitlab::CurrentSettings.password_authentication_enabled_for_web? && !ldap_user?
832
833
834
  end

  def allow_password_authentication_for_git?
835
    Gitlab::CurrentSettings.password_authentication_enabled_for_git? && !ldap_user?
836
837
  end

838
  def can_change_username?
839
    gitlab_config.username_changing_enabled
840
841
  end

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

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

850
851
852
853
  def can_select_namespace?
    several_namespaces? || admin
  end

854
  def can?(action, subject = :global)
http://jneen.net/'s avatar
http://jneen.net/ committed
855
    Ability.allowed?(self, action, subject)
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
856
857
  end

858
859
860
861
  def confirm_deletion_with_password?
    !password_automatically_set? && allow_password_authentication?
  end

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

866
  def projects_limit_left
867
868
869
    projects_limit - personal_projects_count
  end

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

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

  def several_namespaces?
Gosia Ksionek's avatar
Gosia Ksionek committed
883
884
885
886
887
888
    union_sql = ::Gitlab::SQL::Union.new(
      [owned_groups,
       maintainers_groups,
       groups_with_developer_maintainer_project_access]).to_sql

    ::Group.from("(#{union_sql}) #{::Group.table_name}").any?
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
889
890
891
892
893
  end

  def namespace_id
    namespace.try :id
  end
894

895
896
897
  def name_with_username
    "#{name} (#{username})"
  end
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
898

899
  def already_forked?(project)