Class: User

Constant Summary collapse

DEFAULT_NOTIFICATION_LEVEL =
:participating
INSTANCE_ACCESS_REQUEST_APPROVERS_TO_BE_NOTIFIED_LIMIT =
10
BLOCKED_PENDING_APPROVAL_STATE =
'blocked_pending_approval'.freeze
MINIMUM_INACTIVE_DAYS =
90
REQUIRES_ROLE_VALUE =
99

Constants included from HasUserType

HasUserType::BOT_USER_TYPES, HasUserType::INTERNAL_USER_TYPES, HasUserType::NON_INTERNAL_USER_TYPES, HasUserType::USER_TYPES

Constants included from UpdateHighestRole

UpdateHighestRole::HIGHEST_ROLE_JOB_DELAY, UpdateHighestRole::HIGHEST_ROLE_LEASE_TIMEOUT

Constants included from BatchDestroyDependentAssociations

BatchDestroyDependentAssociations::DEPENDENT_ASSOCIATIONS_BATCH_SIZE

Constants included from WithUploads

WithUploads::FILE_UPLOADERS

Constants included from BlocksJsonSerialization

BlocksJsonSerialization::JsonSerializationError

Constants included from Avatarable

Avatarable::ALLOWED_IMAGE_SCALER_WIDTHS, Avatarable::GROUP_AVATAR_SIZES, Avatarable::PROJECT_AVATAR_SIZES, Avatarable::USER_AVATAR_SIZES

Constants included from Gitlab::SQL::Pattern

Gitlab::SQL::Pattern::MIN_CHARS_FOR_PARTIAL_MATCHING, Gitlab::SQL::Pattern::REGEX_QUOTED_WORD

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Methods included from Gitlab::ConfigHelper

gitlab_config, gitlab_config_features

Methods included from AdminChangedPasswordNotifier

#initialize, #send_only_admin_changed_your_password_notification!

Methods included from HasUserType

#bot?, #internal?

Methods included from BatchDestroyDependentAssociations

#dependent_associations_to_destroy, #destroy_dependent_associations_in_batches

Methods included from WithUploads

#retrieve_upload

Methods included from FastDestroyAll::Helpers

#perform_fast_destroy

Methods included from AfterCommitQueue

#run_after_commit, #run_after_commit_or_now

Methods included from BlocksJsonSerialization

#to_json

Methods included from FeatureGate

#flipper_id

Methods included from Referable

#referable_inspect, #reference_link_text, #to_reference_base

Methods included from Avatarable

#avatar_path, #avatar_type, #uncached_avatar_path, #upload_paths

Methods inherited from ApplicationRecord

at_most, id_in, id_not_in, iid_in, pluck_primary_key, primary_key_in, safe_ensure_unique, safe_find_or_create_by, safe_find_or_create_by!, underscore, where_exists, with_fast_statement_timeout, without_order

Instance Attribute Details

#force_random_passwordObject

rubocop: enable CodeReuse/ServiceClass


88
89
90
# File 'app/models/user.rb', line 88

def force_random_password
  @force_random_password
end

#impersonatorObject

Virtual attribute for impersonator


94
95
96
# File 'app/models/user.rb', line 94

def impersonator
  @impersonator
end

#loginObject

Virtual attribute for authenticating by either username or email


91
92
93
# File 'app/models/user.rb', line 91

def 
  @login
end

Class Method Details

.alert_botObject


701
702
703
704
705
706
707
708
709
# File 'app/models/user.rb', line 701

def alert_bot
  email_pattern = "alert%[email protected]#{Settings.gitlab.host}"

  unique_internal(where(user_type: :alert_bot), 'alert-bot', email_pattern) do |u|
    u.bio = 'The GitLab alert bot'
    u.name = 'GitLab Alert Bot'
    u.avatar = bot_avatar(image: 'alert-bot.png')
  end
end

.by_any_email(emails, confirmed: false) ⇒ Object

Returns a relation containing all the users for the given email addresses

Parameters:

  • emails (String, Array<String>)

    email addresses to check

  • confirmed (Boolean) (defaults to: false)

    Only return users where the email is confirmed


525
526
527
528
529
530
531
532
533
534
535
536
537
538
# File 'app/models/user.rb', line 525

def by_any_email(emails, confirmed: false)
  from_users = by_user_email(emails)
  from_users = from_users.confirmed if confirmed

  from_emails = by_emails(emails)
  from_emails = from_emails.confirmed.merge(Email.confirmed) if confirmed

  items = [from_users, from_emails]

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

  from_union(items)
end

.by_login(login) ⇒ Object


649
650
651
652
653
654
655
656
657
# File 'app/models/user.rb', line 649

def ()
  return unless 

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

.filter_items(filter_name) ⇒ Object


546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
# File 'app/models/user.rb', line 546

def filter_items(filter_name)
  case filter_name
  when 'admins'
    admins
  when 'blocked'
    blocked
  when 'blocked_pending_approval'
    blocked_pending_approval
  when 'two_factor_disabled'
    without_two_factor
  when 'two_factor_enabled'
    with_two_factor
  when 'wop'
    without_projects
  when 'external'
    external
  when 'deactivated'
    deactivated
  else
    active_without_ghosts
  end
end

.find_by_any_email(email, confirmed: false) ⇒ Object

Find a User by their primary email or any associated secondary email


515
516
517
518
519
# File 'app/models/user.rb', line 515

def find_by_any_email(email, confirmed: false)
  return unless email

  by_any_email(email, confirmed: confirmed).take
end

.find_by_full_path(path, follow_redirects: false) ⇒ Object


672
673
674
675
# File 'app/models/user.rb', line 672

def find_by_full_path(path, follow_redirects: false)
  namespace = Namespace.for_user.find_by_full_path(path, follow_redirects: follow_redirects)
  namespace&.owner
end

.find_by_private_commit_email(email) ⇒ Object


540
541
542
543
544
# File 'app/models/user.rb', line 540

def find_by_private_commit_email(email)
  user_id = Gitlab::PrivateCommitEmail.user_id_for_email(email)

  find_by(id: user_id)
end

.find_by_ssh_key_id(key_id) ⇒ Object

Returns a user for the given SSH key.


668
669
670
# File 'app/models/user.rb', line 668

def find_by_ssh_key_id(key_id)
  find_by('EXISTS (?)', Key.select(1).where('keys.user_id = users.id').where(id: key_id))
end

.find_by_username(username) ⇒ Object


659
660
661
# File 'app/models/user.rb', line 659

def find_by_username(username)
  by_username(username).take
end

.find_by_username!(username) ⇒ Object


663
664
665
# File 'app/models/user.rb', line 663

def find_by_username!(username)
  by_username(username).take!
end

.find_for_database_authentication(warden_conditions) ⇒ Object

Devise method overridden to allow sign in with email or username


488
489
490
491
492
493
494
495
# File 'app/models/user.rb', line 488

def find_for_database_authentication(warden_conditions)
  conditions = warden_conditions.dup
  if  = conditions.delete(:login)
    where(conditions).find_by("lower(username) = :value OR lower(email) = :value", value: .downcase.strip)
  else
    find_by(conditions)
  end
end

.for_github_id(id) ⇒ Object


510
511
512
# File 'app/models/user.rb', line 510

def for_github_id(id)
  joins(:identities).merge(Identity.with_extern_uid(:github, id))
end

.ghostObject

Return (create if necessary) the ghost user. The ghost user owns records previously belonging to deleted users.


693
694
695
696
697
698
699
# File 'app/models/user.rb', line 693

def ghost
  email = 'ghost%[email protected]'
  unique_internal(where(user_type: :ghost), 'ghost', email) do |u|
    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
end

.limit_to_todo_authors(user: nil, with_todos: false, todo_state: nil) ⇒ Object

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.


428
429
430
431
432
433
434
# File 'app/models/user.rb', line 428

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

.migration_botObject


711
712
713
714
715
716
717
718
719
# File 'app/models/user.rb', line 711

def migration_bot
  email_pattern = "noreply+gitlab-migration-bot%[email protected]#{Settings.gitlab.host}"

  unique_internal(where(user_type: :migration_bot), 'migration-bot', email_pattern) do |u|
    u.bio = 'The GitLab migration bot'
    u.name = 'GitLab Migration Bot'
    u.confirmed_at = Time.zone.now
  end
end

.password_lengthObject

Devise method overridden to allow support for dynamic password lengths


478
479
480
# File 'app/models/user.rb', line 478

def password_length
  Gitlab::CurrentSettings.minimum_password_length..Devise.password_length.max
end

.random_passwordObject

Generate a random password that conforms to the current password length settings


483
484
485
# File 'app/models/user.rb', line 483

def random_password
  Devise.friendly_token(password_length.max)
end

.reference_patternObject

Pattern used to extract `@user` user references from text


682
683
684
685
686
687
688
689
# File 'app/models/user.rb', line 682

def reference_pattern
  @reference_pattern ||=
    %r{
      (?<!\w)
      #{Regexp.escape(reference_prefix)}
      (?<user>#{Gitlab::PathRegex::FULL_NAMESPACE_FORMAT_REGEX})
    }x
end

.reference_prefixObject


677
678
679
# File 'app/models/user.rb', line 677

def reference_prefix
  '@'
end

.reorder_by_nameObject


610
611
612
# File 'app/models/user.rb', line 610

def reorder_by_name
  reorder(:name)
end

.search(query, **options) ⇒ Object

Searches users matching the given query.

This method uses ILIKE on PostgreSQL.

query - The search query as a String

Returns an ActiveRecord::Relation.


576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
# File 'app/models/user.rb', line 576

def search(query, **options)
  query = query&.delete_prefix('@')
  return none if query.blank?

  query = query.downcase

  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

  sanitized_order_sql = Arel.sql(sanitize_sql_array([order, query: query]))

  search_query = if Feature.enabled?(:user_search_secondary_email)
                   search_with_secondary_emails(query)
                 else
                   search_without_secondary_emails(query)
                 end

  search_query.reorder(sanitized_order_sql, :name)
end

.search_with_secondary_emails(query) ⇒ Object

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.


630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
# File 'app/models/user.rb', line 630

def search_with_secondary_emails(query)
  return none if query.blank?

  query = query.downcase

  email_table = Email.arel_table
  matched_by_email_user_id = email_table
    .project(email_table[:user_id])
    .where(email_table[:email].eq(query))
    .take(1) # at most 1 record as there is a unique constraint

  where(
    fuzzy_arel_match(:name, query)
      .or(fuzzy_arel_match(:username, query))
      .or(arel_table[:email].eq(query))
      .or(arel_table[:id].eq(matched_by_email_user_id))
  )
end

.search_without_secondary_emails(query) ⇒ Object


614
615
616
617
618
619
620
621
622
623
624
# File 'app/models/user.rb', line 614

def search_without_secondary_emails(query)
  return none if query.blank?

  query = query.downcase

  where(
    fuzzy_arel_match(:name, query, lower_exact_match: true)
      .or(fuzzy_arel_match(:username, query, lower_exact_match: true))
      .or(arel_table[:email].eq(query))
  )
end

.security_botObject


721
722
723
724
725
726
727
728
729
730
731
# File 'app/models/user.rb', line 721

def security_bot
  email_pattern = "security-bot%[email protected]#{Settings.gitlab.host}"

  unique_internal(where(user_type: :security_bot), 'GitLab-Security-Bot', email_pattern) do |u|
    u.bio = 'System bot that monitors detected vulnerabilities for solutions and creates merge requests with the fixes.'
    u.name = 'GitLab Security Bot'
    u.website_url = Gitlab::Routing.url_helpers.help_page_url('user/application_security/security_bot/index.md')
    u.avatar = bot_avatar(image: 'security-bot.png')
    u.confirmed_at = Time.zone.now
  end
end

.single_userObject


749
750
751
# File 'app/models/user.rb', line 749

def single_user
  User.non_internal.first if single_user?
end

.single_user?Boolean

Return true if there is only single non-internal user in the deployment, ghost user is ignored.

Returns:

  • (Boolean)

745
746
747
# File 'app/models/user.rb', line 745

def single_user?
  User.non_internal.limit(2).count == 1
end

.sort_by_attribute(method) ⇒ Object


497
498
499
500
501
502
503
504
505
506
507
508
# File 'app/models/user.rb', line 497

def sort_by_attribute(method)
  order_method = method || 'id_desc'

  case order_method.to_s
  when 'recent_sign_in' then 
  when 'oldest_sign_in' then 
  when 'last_activity_on_desc' then order_recent_last_activity
  when 'last_activity_on_asc' then order_oldest_last_activity
  else
    order_by(order_method)
  end
end

.support_botObject


733
734
735
736
737
738
739
740
741
# File 'app/models/user.rb', line 733

def support_bot
  email_pattern = "support%[email protected]#{Settings.gitlab.host}"

  unique_internal(where(user_type: :support_bot), 'support-bot', email_pattern) do |u|
    u.bio = 'The GitLab support bot used for Service Desk'
    u.name = 'GitLab Support Bot'
    u.avatar = bot_avatar(image: 'support-bot.png')
  end
end

.union_with_user(user_id = nil) ⇒ Object

Returns a relation that optionally includes the given user.

user_id - The ID of the user to include.


439
440
441
442
443
444
445
446
447
# File 'app/models/user.rb', line 439

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.
    User.unscoped.from_union([all, User.unscoped.where(id: user_id)])
  else
    all
  end
end

.where_not_in(users = nil) ⇒ Object

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.

606
607
608
# File 'app/models/user.rb', line 606

def where_not_in(users = nil)
  users ? where.not(id: users) : all
end

.with_two_factorObject


449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
# File 'app/models/user.rb', line 449

def self.with_two_factor
  with_u2f_registrations = <<-SQL
    EXISTS (
      SELECT *
      FROM u2f_registrations AS u2f
      WHERE u2f.user_id = users.id
    ) OR users.otp_required_for_login = ?
    OR
    EXISTS (
      SELECT *
      FROM webauthn_registrations AS webauthn
      WHERE webauthn.user_id = users.id
    )
  SQL

  where(with_u2f_registrations, true)
end

.with_visible_profile(user) ⇒ Object


410
411
412
413
414
415
416
417
418
# File 'app/models/user.rb', line 410

def self.with_visible_profile(user)
  return with_public_profile if user.nil?

  if user.admin?
    all
  else
    with_public_profile.or(where(id: user.id))
  end
end

.without_two_factorObject


467
468
469
470
471
# File 'app/models/user.rb', line 467

def self.without_two_factor
  joins("LEFT OUTER JOIN u2f_registrations AS u2f ON u2f.user_id = users.id
         LEFT OUTER JOIN webauthn_registrations AS webauthn ON webauthn.user_id = users.id")
    .where("u2f.id IS NULL AND webauthn.id IS NULL AND users.otp_required_for_login = ?", false)
end

Instance Method Details

#accept_pending_invitations!Object


1297
1298
1299
1300
1301
# File 'app/models/user.rb', line 1297

def accept_pending_invitations!
  pending_invitations.select do |member|
    member.accept_invite!(self)
  end
end

#access_levelObject

rubocop: enable CodeReuse/ServiceClass


1630
1631
1632
1633
1634
1635
1636
# File 'app/models/user.rb', line 1630

def access_level
  if admin?
    :admin
  else
    :regular
  end
end

#access_level=(new_level) ⇒ Object


1638
1639
1640
1641
1642
1643
# File 'app/models/user.rb', line 1638

def access_level=(new_level)
  new_level = new_level.to_s
  return unless %w(admin regular).include?(new_level)

  self.admin = (new_level == 'admin')
end

#accessible_deploy_keysObject


1176
1177
1178
1179
1180
1181
# File 'app/models/user.rb', line 1176

def accessible_deploy_keys
  DeployKey.from_union([
    DeployKey.where(id: project_deploy_keys.select(:deploy_key_id)),
    DeployKey.are_public
  ])
end

#active_for_authentication?Boolean

Returns:

  • (Boolean)

393
394
395
# File 'app/models/user.rb', line 393

def active_for_authentication?
  super && can?(:log_in)
end

#all_emails(include_private_email: true) ⇒ Object


1307
1308
1309
1310
1311
1312
1313
# File 'app/models/user.rb', line 1307

def all_emails(include_private_email: true)
  all_emails = []
  all_emails << email unless temp_oauth_email?
  all_emails << private_commit_email if include_private_email
  all_emails.concat(emails.map(&:email))
  all_emails
end

#all_expanded_groupsObject

Returns a relation of groups the user has access to, including their parent and child groups (recursively).


950
951
952
# File 'app/models/user.rb', line 950

def all_expanded_groups
  Gitlab::ObjectHierarchy.new(groups).all_objects
end

#all_ssh_keysObject


1279
1280
1281
# File 'app/models/user.rb', line 1279

def all_ssh_keys
  keys.map(&:publishable_key)
end

#allow_password_authentication?Boolean

Returns:

  • (Boolean)

1063
1064
1065
# File 'app/models/user.rb', line 1063

def allow_password_authentication?
  allow_password_authentication_for_web? || allow_password_authentication_for_git?
end

#allow_password_authentication_for_git?Boolean

Returns:

  • (Boolean)

1071
1072
1073
# File 'app/models/user.rb', line 1071

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

#allow_password_authentication_for_web?Boolean

Returns:

  • (Boolean)

1067
1068
1069
# File 'app/models/user.rb', line 1067

def allow_password_authentication_for_web?
  Gitlab::CurrentSettings.password_authentication_enabled_for_web? && !ldap_user?
end

#already_forked?(project) ⇒ Boolean

Returns:

  • (Boolean)

1144
1145
1146
# File 'app/models/user.rb', line 1144

def already_forked?(project)
  !!fork_of(project)
end

#any_email?(check_email) ⇒ Boolean

Returns:

  • (Boolean)

1329
1330
1331
1332
1333
1334
1335
1336
1337
# File 'app/models/user.rb', line 1329

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

#assigned_open_issues_count(force: false) ⇒ Object


1553
1554
1555
1556
1557
# File 'app/models/user.rb', line 1553

def assigned_open_issues_count(force: false)
  Rails.cache.fetch(['users', id, 'assigned_open_issues_count'], force: force, expires_in: 20.minutes) do
    IssuesFinder.new(self, assignee_id: self.id, state: 'opened', non_archived: true).execute.count
  end
end

#assigned_open_merge_requests_count(force: false) ⇒ Object


1547
1548
1549
1550
1551
# File 'app/models/user.rb', line 1547

def assigned_open_merge_requests_count(force: false)
  Rails.cache.fetch(['users', id, 'assigned_open_merge_requests_count'], force: force, expires_in: 20.minutes) do
    MergeRequestsFinder.new(self, assignee_id: self.id, state: 'opened', non_archived: true).execute.count
  end
end

#authorizations_for_projects(min_access_level: nil, related_project_column: 'projects.id') ⇒ Object

Typically used in conjunction with projects table to get projects a user has been given access to. The param `related_project_column` is the column to compare to the project_authorizations. By default is projects.id

Example use: `Project.where('EXISTS(?)', user.authorizations_for_projects)`


998
999
1000
1001
1002
1003
1004
1005
1006
# File 'app/models/user.rb', line 998

def authorizations_for_projects(min_access_level: nil, related_project_column: 'projects.id')
  authorizations = project_authorizations
                    .select(1)
                    .where("project_authorizations.project_id = #{related_project_column}")

  return authorizations unless min_access_level.present?

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

#authorized_groupsObject

Returns the groups a user has access to, either through a membership or a project authorization


933
934
935
936
937
938
939
940
941
# File 'app/models/user.rb', line 933

def authorized_groups
  Group.unscoped do
    if Feature.enabled?(:shared_group_membership_auth, self)
      authorized_groups_with_shared_membership
    else
      authorized_groups_without_shared_membership
    end
  end
end

#authorized_project?(project, min_access_level = nil) ⇒ Boolean

Returns:

  • (Boolean)

987
988
989
# File 'app/models/user.rb', line 987

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

#authorized_projects(min_access_level = nil) ⇒ Object


974
975
976
977
978
979
980
981
982
983
984
985
# File 'app/models/user.rb', line 974

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

  if min_access_level
    projects = projects
      .where('project_authorizations.access_level >= ?', min_access_level)
  end

  projects
end

#avatar_url(size: nil, scale: 2, **args) ⇒ Object

rubocop: disable CodeReuse/ServiceClass


1288
1289
1290
# File 'app/models/user.rb', line 1288

def avatar_url(size: nil, scale: 2, **args)
  GravatarService.new.execute(email, size, scale, username: username)
end

#can?(action, subject = :global) ⇒ Boolean

Returns:

  • (Boolean)

1091
1092
1093
# File 'app/models/user.rb', line 1091

def can?(action, subject = :global)
  Ability.allowed?(self, action, subject)
end

#can_be_deactivated?Boolean

Returns:

  • (Boolean)

1754
1755
1756
# File 'app/models/user.rb', line 1754

def can_be_deactivated?
  active? && no_recent_activity? && !internal?
end

#can_be_removed?Boolean

Returns:

  • (Boolean)

1482
1483
1484
# File 'app/models/user.rb', line 1482

def can_be_removed?
  !solo_owned_groups.present?
end

#can_change_username?Boolean

Returns:

  • (Boolean)

1075
1076
1077
# File 'app/models/user.rb', line 1075

def can_change_username?
  gitlab_config.username_changing_enabled
end

#can_create_group?Boolean

Returns:

  • (Boolean)

1083
1084
1085
# File 'app/models/user.rb', line 1083

def can_create_group?
  can?(:create_group)
end

#can_create_project?Boolean

Returns:

  • (Boolean)

1079
1080
1081
# File 'app/models/user.rb', line 1079

def can_create_project?
  projects_limit_left > 0
end

#can_leave_project?(project) ⇒ Boolean

Returns:

  • (Boolean)

1264
1265
1266
1267
# File 'app/models/user.rb', line 1264

def can_leave_project?(project)
  project.namespace != namespace &&
    project.project_member(self)
end

#can_read_all_resources?Boolean

Returns:

  • (Boolean)

1645
1646
1647
# File 'app/models/user.rb', line 1645

def can_read_all_resources?
  can?(:read_all_resources)
end

#can_select_namespace?Boolean

Returns:

  • (Boolean)

1087
1088
1089
# File 'app/models/user.rb', line 1087

def can_select_namespace?
  several_namespaces? || admin
end

#check_for_verified_emailObject

see if the new email is already a verified secondary email


906
907
908
# File 'app/models/user.rb', line 906

def check_for_verified_email
  skip_reconfirmation! if emails.confirmed.where(email: self.email).any?
end

#ci_owned_runnersObject


1486
1487
1488
1489
1490
1491
1492
1493
1494
1495
1496
1497
1498
1499
1500
# File 'app/models/user.rb', line 1486

def ci_owned_runners
  @ci_owned_runners ||= begin
    project_runners = Ci::RunnerProject
      .where(project: authorized_projects(Gitlab::Access::MAINTAINER))
      .joins(:runner)
      .select('ci_runners.*')

    group_runners = Ci::RunnerNamespace
      .where(namespace_id: Gitlab::ObjectHierarchy.new(owned_groups).base_and_descendants.select(:id))
      .joins(:runner)
      .select('ci_runners.*')

    Ci::Runner.from_union([project_runners, group_runners])
  end
end

#commit_emailObject

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.


882
883
884
885
886
887
888
889
890
891
# File 'app/models/user.rb', line 882

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

  if super == Gitlab::PrivateCommitEmail::TOKEN
    return private_commit_email
  end

  # The commit email is the same as the primary email if undefined
  super.presence || self.email
end

#commit_email=(email) ⇒ Object


893
894
895
# File 'app/models/user.rb', line 893

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

#commit_email_changed?Boolean

Returns:

  • (Boolean)

897
898
899
# File 'app/models/user.rb', line 897

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

#confirm_deletion_with_password?Boolean

Returns:

  • (Boolean)

1095
1096
1097
# File 'app/models/user.rb', line 1095

def confirm_deletion_with_password?
  !password_automatically_set? && allow_password_authentication?
end

#confirmation_required_on_sign_in?Boolean

Returns:

  • (Boolean)

1787
1788
1789
# File 'app/models/user.rb', line 1787

def confirmation_required_on_sign_in?
  !confirmed? && !confirmation_period_valid?
end

#contributed_projectsObject

Returns the projects a user contributed to in the last year.

This method relies on a subquery as this performs significantly better compared to a JOIN when coupled with, for example, `Project.visible_to_user`. That is, consider the following code:

some_user.contributed_projects.visible_to_user(other_user)

If this method were to use a JOIN the resulting query would take roughly 200 ms on a database with a similar size to GitLab.com's database. On the other hand, using a subquery means we can get the exact same data in about 40 ms.


1472
1473
1474
1475
1476
1477
1478
1479
1480
# File 'app/models/user.rb', line 1472

def contributed_projects
  events = Event.select(:project_id)
    .contributions.where(author_id: self)
    .where("created_at > ?", Time.current - 1.year)
    .distinct
    .reorder(nil)

  Project.where(id: events)
end

#created_byObject


1183
1184
1185
# File 'app/models/user.rb', line 1183

def created_by
  User.find_by(id: created_by_id) if created_by_id
end

#created_recently?Boolean

Returns:

  • (Boolean)

1795
1796
1797
# File 'app/models/user.rb', line 1795

def created_recently?
  created_at > Devise.confirm_within.ago
end

#current_highest_access_levelObject

Load the current highest access by looking directly at the user's memberships


1783
1784
1785
# File 'app/models/user.rb', line 1783

def current_highest_access_level
  members.non_request.maximum(:access_level)
end

#delete_async(deleted_by:, params: {}) ⇒ Object

rubocop: enable CodeReuse/ServiceClass


1389
1390
1391
1392
# File 'app/models/user.rb', line 1389

def delete_async(deleted_by:, params: {})
  block if params[:hard_delete]
  DeleteUserWorker.perform_async(deleted_by.id, id, params.to_h)
end

#disable_two_factor!Object


799
800
801
802
803
804
805
806
807
808
809
810
811
812
# File 'app/models/user.rb', line 799

def disable_two_factor!
  transaction do
    update(
      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
    )
    self.u2f_registrations.destroy_all # rubocop: disable Cop/DestroyAll
    self.webauthn_registrations.destroy_all # rubocop: disable Cop/DestroyAll
  end
end

#dismissed_callout?(feature_name:, ignore_dismissal_earlier_than: nil) ⇒ Boolean

Returns:

  • (Boolean)

1775
1776
1777
1778
1779
1780
# File 'app/models/user.rb', line 1775

def dismissed_callout?(feature_name:, ignore_dismissal_earlier_than: nil)
  callouts = self.callouts.with_feature_name(feature_name)
  callouts = callouts.with_dismissed_after(ignore_dismissal_earlier_than) if ignore_dismissal_earlier_than

  callouts.any?
end

#ensure_namespace_correctObject


1358
1359
1360
1361
1362
1363
1364
1365
1366
# File 'app/models/user.rb', line 1358

def ensure_namespace_correct
  if namespace
    namespace.path = username if username_changed?
    namespace.name = name if name_changed?
  else
    namespace = build_namespace(path: username, name: name)
    namespace.build_namespace_settings
  end
end

#expanded_groups_requiring_two_factor_authenticationObject


954
955
956
# File 'app/models/user.rb', line 954

def expanded_groups_requiring_two_factor_authentication
  all_expanded_groups.where(require_two_factor_authentication: true)
end

#feed_tokenObject

each existing user needs to have an `feed_token`. we do this on read since migrating all existing users is not a feasible solution.


1661
1662
1663
# File 'app/models/user.rb', line 1661

def feed_token
  ensure_feed_token!
end

#first_nameObject


1099
1100
1101
1102
1103
# File 'app/models/user.rb', line 1099

def first_name
  read_attribute(:first_name) || begin
    name.split(' ').first unless name.blank?
  end
end

#forget_me!Object


795
796
797
# File 'app/models/user.rb', line 795

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

#fork_of(project) ⇒ Object


1148
1149
1150
# File 'app/models/user.rb', line 1148

def fork_of(project)
  namespace.find_fork_of(project)
end

#full_pathObject

Instance methods


758
759
760
# File 'app/models/user.rb', line 758

def full_path
  username
end

#full_website_urlObject


1269
1270
1271
1272
1273
# File 'app/models/user.rb', line 1269

def full_website_url
  return "http://#{website_url}" if website_url !~ %r{\Ahttps?://}

  website_url
end

#generate_reset_tokenObject


778
779
780
781
782
783
784
785
# File 'app/models/user.rb', line 778

def generate_reset_token
  @reset_token, enc = Devise.token_generator.generate(self.class, :reset_password_token)

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

  @reset_token
end

#global_notification_settingObject

Lazy load global notification setting Initializes User setting with Participating level if setting not persisted


1538
1539
1540
1541
1542
1543
1544
1545
# File 'app/models/user.rb', line 1538

def global_notification_setting
  return @global_notification_setting if defined?(@global_notification_setting)

  @global_notification_setting = notification_settings.find_or_initialize_by(source: nil)
  @global_notification_setting.update(level: NotificationSetting.levels[DEFAULT_NOTIFICATION_LEVEL]) unless @global_notification_setting.persisted?

  @global_notification_setting
end

#highest_roleObject


1172
1173
1174
# File 'app/models/user.rb', line 1172

def highest_role
  user_highest_role&.highest_access_level || Gitlab::Access::NO_ACCESS
end

#hook_attrsObject


1349
1350
1351
1352
1353
1354
1355
1356
# File 'app/models/user.rb', line 1349

def hook_attrs
  {
    name: name,
    username: username,
    avatar_url: avatar_url(only_path: false),
    email: email
  }
end

#impersonated?Boolean

Returns:

  • (Boolean)

1791
1792
1793
# File 'app/models/user.rb', line 1791

def impersonated?
  impersonator.present?
end

#inactive_messageObject

The messages for these keys are defined in `devise.en.yml`


398
399
400
401
402
403
404
405
406
407
408
# File 'app/models/user.rb', line 398

def inactive_message
  if blocked_pending_approval?
    :blocked_pending_approval
  elsif blocked?
    :blocked
  elsif internal?
    :forbidden
  else
    super
  end
end

#increment_failed_attempts!Object

This is copied from Devise::Models::Lockable#valid_for_authentication?, as our auth flow means we don't call that automatically (and can't conveniently do so).

See:

<https://github.com/plataformatec/devise/blob/v4.7.1/lib/devise/models/lockable.rb#L104>

rubocop: disable CodeReuse/ServiceClass


1617
1618
1619
1620
1621
1622
1623
1624
1625
1626
1627
# File 'app/models/user.rb', line 1617

def increment_failed_attempts!
  return if ::Gitlab::Database.read_only?

  increment_failed_attempts

  if attempts_exceeded?
    lock_access! unless access_locked?
  else
    Users::UpdateService.new(self, user: self).execute(validate: false)
  end
end

#invalidate_cache_countsObject


1582
1583
1584
1585
1586
1587
1588
# File 'app/models/user.rb', line 1582

def invalidate_cache_counts
  invalidate_issue_cache_counts
  invalidate_merge_request_cache_counts
  invalidate_todos_done_count
  invalidate_todos_pending_count
  invalidate_personal_projects_count
end

#invalidate_issue_cache_countsObject


1590
1591
1592
# File 'app/models/user.rb', line 1590

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

#invalidate_merge_request_cache_countsObject


1594
1595
1596
# File 'app/models/user.rb', line 1594

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

#invalidate_personal_projects_countObject


1606
1607
1608
# File 'app/models/user.rb', line 1606

def invalidate_personal_projects_count
  Rails.cache.delete(['users', id, 'personal_projects_count'])
end

#invalidate_todos_done_countObject


1598
1599
1600
# File 'app/models/user.rb', line 1598

def invalidate_todos_done_count
  Rails.cache.delete(['users', id, 'todos_done_count'])
end

#invalidate_todos_pending_countObject


1602
1603
1604
# File 'app/models/user.rb', line 1602

def invalidate_todos_pending_count
  Rails.cache.delete(['users', id, 'todos_pending_count'])
end

#last_active_atObject


1758
1759
1760
1761
1762
1763
# File 'app/models/user.rb', line 1758

def last_active_at
  last_activity = last_activity_on&.to_time&.in_time_zone
   = 

  [last_activity, ].compact.max
end

#last_nameObject


1105
1106
1107
1108
1109
# File 'app/models/user.rb', line 1105

def last_name
  read_attribute(:last_name) || begin
    name.split(' ').drop(1).join(' ') unless name.blank?
  end
end

#ldap_identityObject


1160
1161
1162
# File 'app/models/user.rb', line 1160

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

#ldap_sync_timeObject


1238
1239
1240
1241
# File 'app/models/user.rb', line 1238

def ldap_sync_time
  # This number resides in this method so it can be redefined in EE.
  1.hour
end

#ldap_user?Boolean

Returns:

  • (Boolean)

1152
1153
1154
1155
1156
1157
1158
# File 'app/models/user.rb', line 1152

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

#lock_access!Object

override, from Devise


1689
1690
1691
1692
# File 'app/models/user.rb', line 1689

def lock_access!
  Gitlab::AppLogger.info("Account Locked: username=#{username}")
  super
end

#log_info(message) ⇒ Object

rubocop: enable CodeReuse/ServiceClass


1400
1401
1402
# File 'app/models/user.rb', line 1400

def log_info(message)
  Gitlab::AppLogger.info message
end

#manageable_groups(include_groups_with_developer_maintainer_access: false) ⇒ Object


1431
1432
1433
1434
1435
1436
1437
1438
1439
1440
1441
1442
1443
# File 'app/models/user.rb', line 1431

def manageable_groups(include_groups_with_developer_maintainer_access: false)
  owned_and_maintainer_group_hierarchy = Gitlab::ObjectHierarchy.new(owned_or_maintainers_groups).base_and_descendants

  if include_groups_with_developer_maintainer_access
    union_sql = ::Gitlab::SQL::Union.new(
      [owned_and_maintainer_group_hierarchy,
       groups_with_developer_maintainer_project_access]).to_sql

    ::Group.from("(#{union_sql}) #{::Group.table_name}")
  else
    owned_and_maintainer_group_hierarchy
  end
end

#manageable_groups_with_routes(include_groups_with_developer_maintainer_access: false) ⇒ Object


1445
1446
1447
1448
1449
# File 'app/models/user.rb', line 1445

def manageable_groups_with_routes(include_groups_with_developer_maintainer_access: false)
  manageable_groups(include_groups_with_developer_maintainer_access: include_groups_with_developer_maintainer_access)
    .eager_load(:route)
    .order('routes.path')
end

#manageable_namespacesObject


1427
1428
1429
# File 'app/models/user.rb', line 1427

def manageable_namespaces
  @manageable_namespaces ||= [namespace] + manageable_groups
end

#matches_identity?(provider, extern_uid) ⇒ Boolean

Returns:

  • (Boolean)

1164
1165
1166
# File 'app/models/user.rb', line 1164

def matches_identity?(provider, extern_uid)
  identities.where(provider: provider, extern_uid: extern_uid).exists?
end

#max_member_access_for_group(group_id) ⇒ Object


1718
1719
1720
# File 'app/models/user.rb', line 1718

def max_member_access_for_group(group_id)
  max_member_access_for_group_ids([group_id])[group_id]
end

#max_member_access_for_group_ids(group_ids) ⇒ Object

Determine the maximum access level for a group of groups in bulk.

Returns a Hash mapping project ID -> maximum access level.


1712
1713
1714
1715
1716
# File 'app/models/user.rb', line 1712

def max_member_access_for_group_ids(group_ids)
  max_member_access_for_resource_ids(Group, group_ids) do |group_ids|
    group_members.where(source: group_ids).group(:source_id).maximum(:access_level)
  end
end

#max_member_access_for_project(project_id) ⇒ Object


1705
1706
1707
# File 'app/models/user.rb', line 1705

def max_member_access_for_project(project_id)
  max_member_access_for_project_ids([project_id])[project_id]
end

#max_member_access_for_project_ids(project_ids) ⇒ Object

Determine the maximum access level for a group of projects in bulk.

Returns a Hash mapping project ID -> maximum access level.


1697
1698
1699
1700
1701
1702
1703
# File 'app/models/user.rb', line 1697

def max_member_access_for_project_ids(project_ids)
  max_member_access_for_resource_ids(Project, project_ids) do |project_ids|
    project_authorizations.where(project: project_ids)
                          .group(:project_id)
                          .maximum(:access_level)
  end
end

#membership_groupsObject

Returns the groups a user is a member of, either directly or through a parent group


944
945
946
# File 'app/models/user.rb', line 944

def membership_groups
  Gitlab::ObjectHierarchy.new(groups).base_and_descendants
end

#name_with_usernameObject


1140
1141
1142
# File 'app/models/user.rb', line 1140

def name_with_username
  "#{name} (#{username})"
end

#namespace_idObject


1136
1137
1138
# File 'app/models/user.rb', line 1136

def namespace_id
  namespace.try :id
end

#namespace_move_dir_allowedObject


840
841
842
843
844
# File 'app/models/user.rb', line 840

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

#namespacesObject


1451
1452
1453
1454
1455
# File 'app/models/user.rb', line 1451

def namespaces
  namespace_ids = groups.pluck(:id)
  namespace_ids.push(namespace.id)
  Namespace.where(id: namespace_ids)
end

#notification_email_for(notification_group) ⇒ Object


1502
1503
1504
1505
# File 'app/models/user.rb', line 1502

def notification_email_for(notification_group)
  # Return group-specific email address if present, otherwise return global notification email address
  notification_group&.notification_email_for(self) || notification_email
end

#notification_serviceObject

rubocop: disable CodeReuse/ServiceClass


1395
1396
1397
# File 'app/models/user.rb', line 1395

def notification_service
  NotificationService.new
end

#notification_settings_for(source, inherit: false) ⇒ Object


1507
1508
1509
1510
1511
1512
1513
1514
1515
1516
1517
1518
1519
1520
1521
1522
1523
1524
1525
1526
1527
1528
1529
# File 'app/models/user.rb', line 1507

def notification_settings_for(source, inherit: false)
  if notification_settings.loaded?
    notification_settings.find do |notification|
      notification.source_type == source.class.base_class.name &&
        notification.source_id == source.id
    end
  else
    notification_settings.find_or_initialize_by(source: source) do |ns|
      next unless source.is_a?(Group) && inherit

      # If we're here it means we're trying to create a NotificationSetting for a group that doesn't have one.
      # Find the closest parent with a notification_setting that's not Global level, or that has an email set.
      ancestor_ns = source
                      .notification_settings(hierarchy_order: :asc)
                      .where(user: self)
                      .find_by('level != ? OR notification_email IS NOT NULL', NotificationSetting.levels[:global])
      # Use it to seed the settings
      ns.assign_attributes(ancestor_ns&.slice(*NotificationSetting.allowed_fields))
      ns.source = source
      ns.user = self
    end
  end
end

#notification_settings_for_groups(groups) ⇒ Object


1531
1532
1533
1534
# File 'app/models/user.rb', line 1531

def notification_settings_for_groups(groups)
  ids = groups.is_a?(ActiveRecord::Relation) ? groups.select(:id) : groups.map(&:id)
  notification_settings.for_groups.where(source_id: ids)
end

#oauth_authorized_tokensObject


1457
1458
1459
# File 'app/models/user.rb', line 1457

def oauth_authorized_tokens
  Doorkeeper::AccessToken.where(resource_owner_id: id, revoked_at: nil)
end

#owned_projectsObject


1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
# File 'app/models/user.rb', line 1018

def owned_projects
  @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
  )
end

#owns_commit_emailObject


871
872
873
874
875
# File 'app/models/user.rb', line 871

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

#owns_notification_emailObject


859
860
861
862
863
# File 'app/models/user.rb', line 859

def owns_notification_email
  return if new_record? || temp_oauth_email?

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

#owns_public_emailObject


865
866
867
868
869
# File 'app/models/user.rb', line 865

def owns_public_email
  return if public_email.blank?

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

#password_expired?Boolean

Returns:

  • (Boolean)

1750
1751
1752
# File 'app/models/user.rb', line 1750

def password_expired?
  !!(password_expires_at && password_expires_at < Time.current)
end

#pending_invitationsObject


1303
1304
1305
# File 'app/models/user.rb', line 1303

def pending_invitations
  Member.where(invite_email: verified_emails).invite
end

#pending_todo_for(target) ⇒ Object


1746
1747
1748
# File 'app/models/user.rb', line 1746

def pending_todo_for(target)
  todos.find_by(target: target, state: :pending)
end

#personal_projects_count(force: false) ⇒ Object


1571
1572
1573
1574
1575
# File 'app/models/user.rb', line 1571

def personal_projects_count(force: false)
  Rails.cache.fetch(['users', id, 'personal_projects_count'], force: force, expires_in: 24.hours, raw: true) do
    personal_projects.count
  end.to_i
end

#post_destroy_hookObject


1377
1378
1379
1380
1381
# File 'app/models/user.rb', line 1377

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

  system_hook_service.execute_hooks_for(self, :destroy)
end

#preferred_languageObject


387
388
389
390
391
# File 'app/models/user.rb', line 387

def preferred_language
  read_attribute('preferred_language') ||
    I18n.default_locale.to_s.presence_in(Gitlab::I18n::AVAILABLE_LANGUAGES.keys) ||
    'en'
end

#primary_email_verified?Boolean

rubocop: enable CodeReuse/ServiceClass

Returns:

  • (Boolean)

1293
1294
1295
# File 'app/models/user.rb', line 1293

def primary_email_verified?
  confirmed? && !temp_oauth_email?
end

#private_commit_emailObject


901
902
903
# File 'app/models/user.rb', line 901

def private_commit_email
  Gitlab::PrivateCommitEmail.for_user(self)
end

#project_deploy_keysObject


1168
1169
1170
# File 'app/models/user.rb', line 1168

def project_deploy_keys
  @project_deploy_keys ||= DeployKey.in_projects(authorized_projects.select(:id)).distinct(:id)
end

#projects_limit_leftObject


1111
1112
1113
# File 'app/models/user.rb', line 1111

def projects_limit_left
  projects_limit - personal_projects_count
end

#projects_where_can_admin_issuesObject

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.


1033
1034
1035
# File 'app/models/user.rb', line 1033

def projects_where_can_admin_issues
  authorized_projects(Gitlab::Access::REPORTER).non_archived.with_issues_enabled
end

#projects_with_reporter_access_limited_to(projects) ⇒ Object

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.


1014
1015
1016
# File 'app/models/user.rb', line 1014

def projects_with_reporter_access_limited_to(projects)
  authorized_projects(Gitlab::Access::REPORTER).where(id: projects)
end

#public_verified_emailsObject


1323
1324
1325
1326
1327
# File 'app/models/user.rb', line 1323

def public_verified_emails
  emails = verified_emails(include_private_email: false)
  emails << email unless temp_oauth_email?
  emails.uniq
end

#read_only_attribute?(attribute) ⇒ Boolean

Returns:

  • (Boolean)

1684
1685
1686
# File 'app/models/user.rb', line 1684

def read_only_attribute?(attribute)
  &.read_only?(attribute)
end

#recent_push(project = nil) ⇒ Object

rubocop: disable CodeReuse/ServiceClass


1116
1117
1118
1119
1120
1121
1122
1123
1124
# File 'app/models/user.rb', line 1116

def recent_push(project = nil)
  service = Users::LastPushEventService.new(self)

  if project
    service.last_event_for_project(project)
  else
    service.last_event_for_user
  end
end

#recently_sent_password_reset?Boolean

Returns:

  • (Boolean)

787
788
789
# File 'app/models/user.rb', line 787

def recently_sent_password_reset?
  reset_password_sent_at.present? && reset_password_sent_at >= 1.minute.ago
end

#refresh_authorized_projectsObject

rubocop: disable CodeReuse/ServiceClass


965
966
967
# File 'app/models/user.rb', line 965

def refresh_authorized_projects
  Users::RefreshAuthorizedProjectsService.new(self).execute
end

#remember_me!Object


791
792
793
# File 'app/models/user.rb', line 791

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

#remove_key_cacheObject

rubocop: disable CodeReuse/ServiceClass


1384
1385
1386
# File 'app/models/user.rb', line 1384

def remove_key_cache
  Users::KeysCountService.new(self).delete_cache
end

#remove_project_authorizations(project_ids) ⇒ Object

rubocop: enable CodeReuse/ServiceClass


970
971
972
# File 'app/models/user.rb', line 970

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

#require_extra_setup_for_git_auth?Boolean

Returns:

  • (Boolean)

1059
1060
1061
# File 'app/models/user.rb', line 1059

def require_extra_setup_for_git_auth?
  require_password_creation_for_git? || require_personal_access_token_creation_for_git_auth?
end

#require_password_creation_for_git?Boolean

Returns:

  • (Boolean)

1049
1050
1051
# File 'app/models/user.rb', line 1049

def require_password_creation_for_git?
  allow_password_authentication_for_git? && password_automatically_set?
end

#require_password_creation_for_web?Boolean

rubocop: enable CodeReuse/ServiceClass

Returns:

  • (Boolean)

1045
1046
1047
# File 'app/models/user.rb', line 1045

def require_password_creation_for_web?
  allow_password_authentication_for_web? && password_automatically_set?
end

#require_personal_access_token_creation_for_git_auth?Boolean

Returns:

  • (Boolean)

1053
1054
1055
1056
1057
# File 'app/models/user.rb', line 1053

def require_personal_access_token_creation_for_git_auth?
  return false if allow_password_authentication_for_git? || ldap_user?

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

#require_ssh_key?Boolean

rubocop: disable CodeReuse/ServiceClass

Returns:

  • (Boolean)

1038
1039
1040
1041
1042
# File 'app/models/user.rb', line 1038

def require_ssh_key?
  count = Users::KeysCountService.new(self).count

  count == 0 && Gitlab::ProtocolAccess.allowed?('ssh')
end

#required_terms_not_accepted?Boolean

Returns:

  • (Boolean)

1728
1729
1730
1731
# File 'app/models/user.rb', line 1728

def required_terms_not_accepted?
  Gitlab::CurrentSettings.current_application_settings.enforce_terms? &&
    !terms_accepted?
end

#requires_ldap_check?Boolean

Returns:

  • (Boolean)

1228
1229
1230
1231
1232
1233
1234
1235
1236
# File 'app/models/user.rb', line 1228

def requires_ldap_check?
  if !Gitlab.config.ldap.enabled
    false
  elsif ldap_user?
    !last_credential_check_at || (last_credential_check_at + ldap_sync_time) < Time.current
  else
    false
  end
end

#requires_usage_stats_consent?Boolean

Returns:

  • (Boolean)

1733
1734
1735
# File 'app/models/user.rb', line 1733

def requires_usage_stats_consent?
  self.admin? && 7.days.ago > self.created_at && !has_current_license? && User.single_user? && !consented_usage_stats?
end

#role_required?Boolean

Returns:

  • (Boolean)

1767
1768
1769
# File 'app/models/user.rb', line 1767

def role_required?
  role_before_type_cast == REQUIRES_ROLE_VALUE
end

#sanitize_attrsObject


1187
1188
1189
1190
1191
1192
# File 'app/models/user.rb', line 1187

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

#set_commit_emailObject


1206
1207
1208
1209
1210
# File 'app/models/user.rb', line 1206

def set_commit_email
  if commit_email.blank? || verified_emails.exclude?(commit_email)
    self.commit_email = nil
  end
end

#set_notification_emailObject


1194
1195
1196
1197
1198
# File 'app/models/user.rb', line 1194

def set_notification_email
  if notification_email.blank? || all_emails.exclude?(notification_email)
    self.notification_email = email
  end
end

#set_projects_limitObject


1219
1220
1221
1222
1223
1224
1225
1226
# File 'app/models/user.rb', line 1219

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

  self.projects_limit = Gitlab::CurrentSettings.default_projects_limit
end

#set_public_emailObject


1200
1201
1202
1203
1204
# File 'app/models/user.rb', line 1200

def set_public_email
  if public_email.blank? || all_emails.exclude?(public_email)
    self.public_email = ''
  end
end

#set_role_required!Object


1771
1772
1773
# File 'app/models/user.rb', line 1771

def set_role_required!
  update_column(:role, REQUIRES_ROLE_VALUE)
end

#set_username_errorsObject


1368
1369
1370
1371
# File 'app/models/user.rb', line 1368

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

#several_namespaces?Boolean

rubocop: enable CodeReuse/ServiceClass

Returns:

  • (Boolean)

1127
1128
1129
1130
1131
1132
1133
1134
# File 'app/models/user.rb', line 1127

def several_namespaces?
  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?
end

#short_website_urlObject


1275
1276
1277
# File 'app/models/user.rb', line 1275

def short_website_url
  website_url.sub(%r{\Ahttps?://}, '')
end

#skip_confirmation=(bool) ⇒ Object


770
771
772
# File 'app/models/user.rb', line 770

def skip_confirmation=(bool)
  skip_confirmation! if bool
end

#skip_reconfirmation=(bool) ⇒ Object


774
775
776
# File 'app/models/user.rb', line 774

def skip_reconfirmation=(bool)
  skip_reconfirmation! if bool
end

#solo_owned_groupsObject


1250
1251
1252
1253
1254
# File 'app/models/user.rb', line 1250

def solo_owned_groups
  @solo_owned_groups ||= owned_groups.includes(:owners).select do |group|
    group.owners == [self]
  end
end

#source_groups_of_two_factor_authentication_requirementObject


958
959
960
961
962
# File 'app/models/user.rb', line 958

def source_groups_of_two_factor_authentication_requirement
  Gitlab::ObjectHierarchy.new(expanded_groups_requiring_two_factor_authentication)
    .all_objects
    .where(id: groups)
end

#starred?(project) ⇒ Boolean

rubocop: enable CodeReuse/ServiceClass

Returns:

  • (Boolean)

1410
1411
1412
# File 'app/models/user.rb', line 1410

def starred?(project)
  starred_projects.exists?(project.id)
end

#static_object_tokenObject

Each existing user needs to have a `static_object_token`. We do this on read since migrating all existing users is not a feasible solution.


1668
1669
1670
# File 'app/models/user.rb', line 1668

def static_object_token
  ensure_static_object_token!
end

#sync_attribute?(attribute) ⇒ Boolean

Returns:

  • (Boolean)

1672
1673
1674
1675
1676
1677
1678
1679
1680
1681
1682
# File 'app/models/user.rb', line 1672

def sync_attribute?(attribute)
  return true if ldap_user? && attribute == :email

  attributes = Gitlab.config.omniauth.sync_profile_attributes

  if attributes.is_a?(Array)
    attributes.include?(attribute.to_s)
  else
    attributes
  end
end

#system_hook_serviceObject

rubocop: disable CodeReuse/ServiceClass


1405
1406
1407
# File 'app/models/user.rb', line 1405

def system_hook_service
  SystemHooksService.new
end

#temp_oauth_email?Boolean

Returns:

  • (Boolean)

1283
1284
1285
# File 'app/models/user.rb', line 1283

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

#terms_accepted?Boolean

Returns:

  • (Boolean)

1722
1723
1724
1725
1726
# File 'app/models/user.rb', line 1722

def terms_accepted?
  return true if project_bot?

  accepted_term_id.present?
end

#to_paramObject


762
763
764
# File 'app/models/user.rb', line 762

def to_param
  username
end

#to_reference(_from = nil, target_project: nil, full: nil) ⇒ Object


766
767
768
# File 'app/models/user.rb', line 766

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

#todos_done_count(force: false) ⇒ Object


1559
1560
1561
1562
1563
# File 'app/models/user.rb', line 1559

def todos_done_count(force: false)
  Rails.cache.fetch(['users', id, 'todos_done_count'], force: force, expires_in: 20.minutes) do
    TodosFinder.new(self, state: :done).execute.count
  end
end

#todos_pending_count(force: false) ⇒ Object


1565
1566
1567
1568
1569
# File 'app/models/user.rb', line 1565

def todos_pending_count(force: false)
  Rails.cache.fetch(['users', id, 'todos_pending_count'], force: force, expires_in: 20.minutes) do
    TodosFinder.new(self, state: :pending).execute.count
  end
end

#toggle_star(project) ⇒ Object


1414
1415
1416
1417
1418
1419
1420
1421
1422
1423
1424
1425
# File 'app/models/user.rb', line 1414

def toggle_star(project)
  UsersStarProject.transaction do
    user_star_project = users_star_projects
        .where(project: project, user: self).lock(true).first

    if user_star_project
      user_star_project.destroy
    else
      UsersStarProject.create!(project: project, user: self)
    end
  end
end

#try_obtain_ldap_leaseObject


1243
1244
1245
1246
1247
1248
# File 'app/models/user.rb', line 1243

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

#two_factor_enabled?Boolean

Returns:

  • (Boolean)

814
815
816
# File 'app/models/user.rb', line 814

def two_factor_enabled?
  two_factor_otp_enabled? || two_factor_webauthn_u2f_enabled?
end

#two_factor_otp_enabled?Boolean

Returns:

  • (Boolean)

818
819
820
# File 'app/models/user.rb', line 818

def two_factor_otp_enabled?
  otp_required_for_login? || Feature.enabled?(:forti_authenticator, self)
end

#two_factor_u2f_enabled?Boolean

Returns:

  • (Boolean)

822
823
824
825
826
827
828
# File 'app/models/user.rb', line 822

def two_factor_u2f_enabled?
  if u2f_registrations.loaded?
    u2f_registrations.any?
  else
    u2f_registrations.exists?
  end
end

#two_factor_webauthn_enabled?Boolean

Returns:

  • (Boolean)

834
835
836
837
838
# File 'app/models/user.rb', line 834

def two_factor_webauthn_enabled?
  return false unless Feature.enabled?(:webauthn)

  (webauthn_registrations.loaded? && webauthn_registrations.any?) || (!webauthn_registrations.loaded? && webauthn_registrations.exists?)
end

#two_factor_webauthn_u2f_enabled?Boolean

Returns:

  • (Boolean)

830
831
832
# File 'app/models/user.rb', line 830

def two_factor_webauthn_u2f_enabled?
  two_factor_u2f_enabled? || two_factor_webauthn_enabled?
end

#unique_emailObject


853
854
855
856
857
# File 'app/models/user.rb', line 853

def unique_email
  if !emails.exists?(email: email) && Email.exists?(email: email)
    errors.add(:email, _('has already been taken'))
  end
end

#update_emails_with_primary_email(previous_confirmed_at, previous_email) ⇒ Object

Note: the use of the Emails services will cause `saves` on the user object, running through the callbacks again and can have side effects, such as the `previous_changes` 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 rubocop: disable CodeReuse/ServiceClass


916
917
918
919
920
921
922
923
924
925
# File 'app/models/user.rb', line 916

def update_emails_with_primary_email(previous_confirmed_at, previous_email)
  primary_email_record = emails.find_by(email: email)
  Emails::DestroyService.new(self, user: self).execute(primary_email_record) if primary_email_record

  # 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
  Emails::CreateService.new(self, user: self, email: previous_email).execute(confirmed_at: previous_confirmed_at)

  update_columns(confirmed_at: primary_email_record.confirmed_at) if primary_email_record&.confirmed_at
end

#update_invalid_gpg_signaturesObject

rubocop: enable CodeReuse/ServiceClass


928
929
930
# File 'app/models/user.rb', line 928

def update_invalid_gpg_signatures
  gpg_keys.each(&:update_invalid_gpg_signatures)
end

#update_secondary_emails!Object


1212
1213
1214
1215
1216
1217
# File 'app/models/user.rb', line 1212

def update_secondary_emails!
  set_notification_email
  set_public_email
  set_commit_email
  save if notification_email_changed? || public_email_changed? || commit_email_changed?
end

#update_todos_count_cacheObject


1577
1578
1579
1580
# File 'app/models/user.rb', line 1577

def update_todos_count_cache
  todos_done_count(force: true)
  todos_pending_count(force: true)
end

#update_tracked_fields!(request) ⇒ Object

Override Devise::Models::Trackable#update_tracked_fields! to limit database writes to at most once every hour rubocop: disable CodeReuse/ServiceClass


76
77
78
79
80
81
82
83
84
85
# File 'app/models/user.rb', line 76

def update_tracked_fields!(request)
  return if Gitlab::Database.read_only?

  update_tracked_fields(request)

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

  Users::UpdateService.new(self, user: self).execute(validate: false)
end

#update_two_factor_requirementObject


1649
1650
1651
1652
1653
1654
1655
1656
# File 'app/models/user.rb', line 1649

def update_two_factor_requirement
  periods = expanded_groups_requiring_two_factor_authentication.pluck(:two_factor_grace_period)

  self.require_two_factor_authentication_from_group = periods.any?
  self.two_factor_grace_period = periods.min || User.column_defaults['two_factor_grace_period']

  save
end

#user_detailObject


1742
1743
1744
# File 'app/models/user.rb', line 1742

def user_detail
  super.presence || build_user_detail
end

#user_preferenceObject

Avoid migrations only building user preference object when needed.


1738
1739
1740
# File 'app/models/user.rb', line 1738

def user_preference
  super.presence || build_user_preference
end

#username_changed_hookObject


1373
1374
1375
# File 'app/models/user.rb', line 1373

def username_changed_hook
  system_hook_service.execute_hooks_for(self, :rename)
end

#verified_email?(check_email) ⇒ Boolean

Returns:

  • (Boolean)

1339
1340
1341
1342
1343
1344
1345
1346
1347
# File 'app/models/user.rb', line 1339

def verified_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)

  verified_emails.include?(check_email.downcase)
end

#verified_emails(include_private_email: true) ⇒ Object


1315
1316
1317
1318
1319
1320
1321
# File 'app/models/user.rb', line 1315

def verified_emails(include_private_email: true)
  verified_emails = []
  verified_emails << email if primary_email_verified?
  verified_emails << private_commit_email if include_private_email
  verified_emails.concat(emails.confirmed.pluck(:email))
  verified_emails
end

#will_save_change_to_login?Boolean

will_save_change_to_attribute? is used by Devise to check if it is necessary to clear any existing reset_password_tokens before updating an authentication_key and login in our case is a virtual attribute to allow login by username or email.

Returns:

  • (Boolean)

849
850
851
# File 'app/models/user.rb', line 849

def will_save_change_to_login?
  will_save_change_to_username? || will_save_change_to_email?
end

#with_defaultsObject


1256
1257
1258
1259
1260
1261
1262
# File 'app/models/user.rb', line 1256

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

  self
end