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'
COUNT_CACHE_VALIDITY_PERIOD =
24.hours
OTP_SECRET_LENGTH =
32
OTP_SECRET_TTL =
2.minutes
MAX_USERNAME_LENGTH =
255
MIN_USERNAME_LENGTH =
2
MAX_LIMIT_FOR_ASSIGNEED_ISSUES_COUNT =
100
SECONDARY_EMAIL_ATTRIBUTES =
[
  :commit_email,
  :notification_email,
  :public_email
].freeze
FORBIDDEN_SEARCH_STATES =
%w[blocked banned ldap_blocked].freeze
INCOMING_MAIL_TOKEN_PREFIX =
'glimt-'
FEED_TOKEN_PREFIX =
'glft-'
FIRST_GROUP_PATHS_LIMIT =
200
MINIMUM_DAYS_CREATED =
7
DISALLOWED_PASSWORDS =
%w[123qweQWE!@#000000000].freeze
DELETION_DELAY_IN_DAYS =

rubocop: enable CodeReuse/ServiceClass

7.days

Constants included from RequireEmailVerification

RequireEmailVerification::MAXIMUM_ATTEMPTS, RequireEmailVerification::UNLOCK_IN

Constants included from EncryptedUserPassword

EncryptedUserPassword::BCRYPT_PREFIX, EncryptedUserPassword::BCRYPT_STRATEGY, EncryptedUserPassword::PBKDF2_SHA512_PREFIX, EncryptedUserPassword::PBKDF2_SHA512_STRATEGY

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 BlocksUnsafeSerialization

BlocksUnsafeSerialization::UnsafeSerializationError

Constants included from Avatarable

Avatarable::ALLOWED_IMAGE_SCALER_WIDTHS, Avatarable::COMBINED_AVATAR_SIZES, Avatarable::COMBINED_AVATAR_SIZES_RETINA, Avatarable::GROUP_AVATAR_SIZES, Avatarable::MAXIMUM_FILE_SIZE, 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_TERM

Constants inherited from ApplicationRecord

ApplicationRecord::MAX_PLUCK

Constants included from HasCheckConstraints

HasCheckConstraints::NOT_NULL_CHECK_PATTERN

Constants included from ResetOnColumnErrors

ResetOnColumnErrors::MAX_RESET_PERIOD

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 ForcedEmailConfirmation

#force_confirm

Methods included from AfterCommitQueue

#run_after_commit, #run_after_commit_or_now

Methods included from AdminChangedPasswordNotifier

#send_only_admin_changed_your_password_notification!

Methods included from RecoverableByAnyEmail

#send_reset_password_instructions

Methods included from EncryptedUserPassword

#authenticatable_salt, #password=

Methods included from StripAttribute

#strip_attributes!

Methods included from Gitlab::Auth::Otp::DuoAuth

#duo_auth_enabled?

Methods included from HasUserType

#bot?, #internal?, #redacted_name, #resource_bot_owners_and_maintainers, #resource_bot_resource

Methods included from BatchNullifyDependentAssociations

#nullify_dependent_associations_in_batches

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 BlocksUnsafeSerialization

#serializable_hash

Methods included from Gitlab::Utils::Override

#extended, extensions, #included, #method_added, #override, #prepended, #queue_verification, verify!

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 included from Gitlab::SQL::Pattern

split_query_to_search_terms

Methods inherited from ApplicationRecord

===, cached_column_list, #create_or_load_association, declarative_enum, default_select_columns, id_in, id_not_in, iid_in, nullable_column?, pluck_primary_key, primary_key_in, #readable_by?, safe_ensure_unique, safe_find_or_create_by, safe_find_or_create_by!, #to_ability_name, underscore, where_exists, where_not_exists, with_fast_read_statement_timeout, without_order

Methods included from ResetOnColumnErrors

#reset_on_union_error, #reset_on_unknown_attribute_error

Methods included from Gitlab::SensitiveSerializableHash

#serializable_hash

Instance Attribute Details

#force_random_passwordObject

rubocop: enable CodeReuse/ServiceClass



146
147
148
# File 'app/models/user.rb', line 146

def force_random_password
  @force_random_password
end

#impersonatorObject

Virtual attribute for impersonator



152
153
154
# File 'app/models/user.rb', line 152

def impersonator
  @impersonator
end

#loginObject

Virtual attribute for authenticating by either username or email



149
150
151
# File 'app/models/user.rb', line 149

def 
  @login
end

Class Method Details

.by_any_email(emails, confirmed: false) ⇒ Object

Returns a relation containing all found users by their primary email or any associated confirmed secondary email

Parameters:

  • emails (String, Array<String>)

    email addresses to check

  • confirmed (Boolean) (defaults to: false)

    Only return users where the primary email is confirmed



828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
# File 'app/models/user.rb', line 828

def by_any_email(emails, confirmed: false)
  return none if Array(emails).all?(&:nil?)

  from_users = by_user_email(emails)
  from_users = from_users.confirmed if confirmed

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

  items = [from_users, from_emails]

  # TODO: https://gitlab.com/gitlab-org/gitlab/-/issues/461885
  # What about private commit emails with capitalized username, we'd never find them and
  # since the private_commit_email derives from the username, it can
  # be uppercase in parts. So we'll never find an existing user during the invite
  # process by email if that is true as we are case sensitive in this case.
  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

.ends_with_reserved_file_extension?(username) ⇒ Boolean

Returns:

  • (Boolean)


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

def ends_with_reserved_file_extension?(username)
  Mime::EXTENSION_LOOKUP.keys.any? { |type| username.end_with?(".#{type}") }
end

.filter_items(filter_name) ⇒ Object



856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
# File 'app/models/user.rb', line 856

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

.find_by_any_email(email, confirmed: false) ⇒ Object

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



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

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



1042
1043
1044
1045
# File 'app/models/user.rb', line 1042

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

.find_by_login(login) ⇒ Object



1025
1026
1027
# File 'app/models/user.rb', line 1025

def ()
  ().take
end

.find_by_private_commit_email(email) ⇒ Object



850
851
852
853
854
# File 'app/models/user.rb', line 850

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. Deploy keys are excluded.



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

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

.find_by_username(username) ⇒ Object



1029
1030
1031
# File 'app/models/user.rb', line 1029

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

.find_by_username!(username) ⇒ Object



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

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



794
795
796
797
798
799
800
801
# File 'app/models/user.rb', line 794

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

.generate_incoming_mail_tokenObject



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

def generate_incoming_mail_token
  "#{INCOMING_MAIL_TOKEN_PREFIX}#{SecureRandom.hex.to_i(16).to_s(36)}"
end

.get_ids_by_ids_or_usernames(ids, usernames) ⇒ Object



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

def get_ids_by_ids_or_usernames(ids, usernames)
  by_ids_or_usernames(ids, usernames).pluck(:id)
end

.gfm_autocomplete_search(query) ⇒ Object



952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
# File 'app/models/user.rb', line 952

def gfm_autocomplete_search(query)
  where(
    "REPLACE(users.name, ' ', '') ILIKE :pattern OR users.username ILIKE :pattern",
    pattern: "%#{sanitize_sql_like(query)}%"
  ).order(
    Arel.sql(sanitize_sql(
      [
        "CASE WHEN REPLACE(users.name, ' ', '') ILIKE :prefix_pattern OR users.username ILIKE :prefix_pattern THEN 1 ELSE 2 END",
        { prefix_pattern: "#{sanitize_sql_like(query)}%" }
      ]
    )),
    :username,
    :id
  )
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.



747
748
749
750
751
752
753
# File 'app/models/user.rb', line 747

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

.password_lengthObject

Devise method overridden to allow support for dynamic password lengths



784
785
786
# File 'app/models/user.rb', line 784

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



789
790
791
# File 'app/models/user.rb', line 789

def random_password
  Devise.friendly_token(password_length.max)
end

.reference_patternObject

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



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

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

.reference_prefixObject



1047
1048
1049
# File 'app/models/user.rb', line 1047

def reference_prefix
  '@'
end

.reorder_by_nameObject



976
977
978
# File 'app/models/user.rb', line 976

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 with_private_emails - include private emails in search partial_email_search - only for admins to preserve email privacy. Only for self-managed instances.

Returns an ActiveRecord::Relation.



894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
# File 'app/models/user.rb', line 894

def search(query, **options)
  return none unless query.is_a?(String)

  query = query&.delete_prefix('@')
  return none if query.blank?

  query = query.downcase

  order = <<~SQL
    CASE
      WHEN LOWER(users.public_email) = :query THEN 0
      WHEN LOWER(users.username) = :query THEN 1
      WHEN LOWER(users.name) = :query THEN 2
      ELSE 3
    END
  SQL

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

  use_minimum_char_limit = options[:use_minimum_char_limit]

  scope =
    if options[:with_private_emails]
      with_primary_or_secondary_email(
        query, use_minimum_char_limit: use_minimum_char_limit, partial_email_search: options[:partial_email_search]
      )
    else
      with_public_email(query)
    end

  scope = scope.or(search_by_name_or_username(query, use_minimum_char_limit: use_minimum_char_limit))

  order = Gitlab::Pagination::Keyset::Order.build(
    [
      Gitlab::Pagination::Keyset::ColumnOrderDefinition.new(
        attribute_name: 'users_match_priority',
        order_expression: sanitized_order_sql.asc,
        add_to_projections: true
      ),
      Gitlab::Pagination::Keyset::ColumnOrderDefinition.new(
        attribute_name: 'users_name',
        order_expression: arel_table[:name].asc,
        add_to_projections: true,
        nullable: :not_nullable
      ),
      Gitlab::Pagination::Keyset::ColumnOrderDefinition.new(
        attribute_name: 'users_id',
        order_expression: arel_table[:id].asc,
        add_to_projections: true,
        nullable: :not_nullable
      )
    ])
  scope.reorder(order)
end

.search_by_name_or_username(query, use_minimum_char_limit: nil) ⇒ Object

searches user by given pattern it compares name and username fields with given pattern This method uses ILIKE on PostgreSQL.



983
984
985
986
987
988
989
990
# File 'app/models/user.rb', line 983

def search_by_name_or_username(query, use_minimum_char_limit: nil)
  use_minimum_char_limit = user_search_minimum_char_limit if use_minimum_char_limit.nil?

  where(
    fuzzy_arel_match(:name, query, use_minimum_char_limit: use_minimum_char_limit)
      .or(fuzzy_arel_match(:username, query, use_minimum_char_limit: use_minimum_char_limit))
  )
end

.single_userObject



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

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)


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

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

.sort_by_attribute(method) ⇒ Object



803
804
805
806
807
808
809
810
811
812
813
814
# File 'app/models/user.rb', line 803

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

.supported_keyset_orderingsObject



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

def self.supported_keyset_orderings
  {
    id: [:asc, :desc],
    name: [:asc, :desc],
    username: [:asc, :desc],
    created_at: [:asc, :desc],
    updated_at: [:asc, :desc]
  }
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.



758
759
760
761
762
763
764
765
766
# File 'app/models/user.rb', line 758

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

.user_search_minimum_char_limitObject

This method is overridden in JiHu. gitlab.com/gitlab-org/gitlab/-/issues/348509



1021
1022
1023
# File 'app/models/user.rb', line 1021

def user_search_minimum_char_limit
  true
end

.username_exists?(username) ⇒ Boolean

Returns:

  • (Boolean)


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

def username_exists?(username)
  exists?(username: username)
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.


972
973
974
# File 'app/models/user.rb', line 972

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

.with_primary_or_secondary_email(query, use_minimum_char_limit: true, partial_email_search: false) ⇒ Object



996
997
998
999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
# File 'app/models/user.rb', line 996

def with_primary_or_secondary_email(query, use_minimum_char_limit: true, partial_email_search: false)
  email_table = Email.arel_table

  if partial_email_search
    email_table_matched_by_email = Email.fuzzy_arel_match(:email, query, use_minimum_char_limit: use_minimum_char_limit)
    matched_by_email = User.fuzzy_arel_match(:email, query, use_minimum_char_limit: use_minimum_char_limit)
  else
    email_table_matched_by_email = email_table[:email].eq(query)
    matched_by_email = arel_table[:email].eq(query)
  end

  matched_by_email_user_id = email_table
    .project(email_table[:user_id])
    .where(email_table_matched_by_email)
    .where(email_table[:confirmed_at].not_eq(nil))
    .take(1) # at most 1 record as there is a unique constraint

  where(
    matched_by_email
    .or(arel_table[:id].eq(matched_by_email_user_id))
  )
end

.with_public_email(email_address) ⇒ Object



992
993
994
# File 'app/models/user.rb', line 992

def with_public_email(email_address)
  where(public_email: email_address)
end

.with_two_factorObject



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

def self.with_two_factor
  where(otp_required_for_login: true)
    .or(where_exists(WebauthnRegistration.where(WebauthnRegistration.arel_table[:user_id].eq(arel_table[:id]))))
end

.with_visible_profile(user) ⇒ Object



729
730
731
732
733
734
735
736
737
# File 'app/models/user.rb', line 729

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



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

def self.without_two_factor
  where
    .missing(:webauthn_registrations)
    .where(otp_required_for_login: false)
end

Instance Method Details

#abuse_metadataObject



2520
2521
2522
2523
2524
2525
# File 'app/models/user.rb', line 2520

def 
  {
    account_age: ,
    two_factor_enabled: two_factor_enabled? ? 1 : 0
  }
end

#accept_pending_invitations!Object



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

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

#access_levelObject

rubocop: enable CodeReuse/ServiceClass



2222
2223
2224
2225
2226
2227
2228
# File 'app/models/user.rb', line 2222

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

#access_level=(new_level) ⇒ Object



2230
2231
2232
2233
2234
2235
# File 'app/models/user.rb', line 2230

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



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

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

#account_age_in_daysObject



2505
2506
2507
# File 'app/models/user.rb', line 2505

def 
  (Date.current - created_at.to_date).to_i
end

#active_for_authentication?Boolean

Returns:

  • (Boolean)


708
709
710
711
712
713
714
# File 'app/models/user.rb', line 708

def active_for_authentication?
  return false unless super

  check_ldap_if_ldap_blocked!

  can?(:log_in)
end

#admin_unsubscribe!Object



1657
1658
1659
# File 'app/models/user.rb', line 1657

def admin_unsubscribe!
  update_column :admin_email_unsubscribed_at, Time.current
end

#all_emails(include_private_email: true) ⇒ Object



1769
1770
1771
1772
1773
1774
1775
# File 'app/models/user.rb', line 1769

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.filter_map { |email| email.email if email.confirmed? })
  all_emails.uniq
end

#all_expanded_groupsObject

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



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

def all_expanded_groups
  return groups if groups.empty?

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

#all_ssh_keysObject



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

def all_ssh_keys
  keys.map(&:publishable_key)
end

#allow_password_authentication?Boolean

Returns:

  • (Boolean)


1484
1485
1486
# File 'app/models/user.rb', line 1484

def allow_password_authentication?
  allow_password_authentication_for_web? || allow_password_authentication_for_git?
end

#allow_password_authentication_for_git?Boolean

Returns:

  • (Boolean)


1495
1496
1497
1498
1499
1500
# File 'app/models/user.rb', line 1495

def allow_password_authentication_for_git?
  return false if password_based_omniauth_user?
  return false if disable_password_authentication_for_sso_users?

  Gitlab::CurrentSettings.password_authentication_enabled_for_git?
end

#allow_password_authentication_for_web?Boolean

Returns:

  • (Boolean)


1488
1489
1490
1491
1492
1493
# File 'app/models/user.rb', line 1488

def allow_password_authentication_for_web?
  return false if ldap_user?
  return false if disable_password_authentication_for_sso_users?

  Gitlab::CurrentSettings.password_authentication_enabled_for_web?
end

#allow_user_to_create_group_and_project?Boolean

Returns:

  • (Boolean)


1510
1511
1512
1513
1514
1515
# File 'app/models/user.rb', line 1510

def allow_user_to_create_group_and_project?
  return true if Gitlab::CurrentSettings.allow_project_creation_for_guest_and_below
  return true if can_admin_all_resources?

  highest_role > Gitlab::Access::GUEST
end

#already_forked?(project) ⇒ Boolean

Returns:

  • (Boolean)


1580
1581
1582
# File 'app/models/user.rb', line 1580

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

#any_email?(check_email) ⇒ Boolean

Returns:

  • (Boolean)


1797
1798
1799
1800
1801
1802
1803
1804
1805
# File 'app/models/user.rb', line 1797

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

#assign_personal_namespace(organization) ⇒ Object



1834
1835
1836
1837
1838
1839
1840
1841
1842
1843
1844
1845
1846
# File 'app/models/user.rb', line 1834

def assign_personal_namespace(organization)
  return namespace if namespace

  namespace_attributes = { path: username, name: name }

  # Do not explicitly assign if organization is `nil`
  namespace_attributes[:organization] = organization if organization

  build_namespace(namespace_attributes)
  namespace.build_namespace_settings

  namespace
end

#assigned_open_issues_count(force: false) ⇒ Object



2139
2140
2141
2142
2143
# File 'app/models/user.rb', line 2139

def assigned_open_issues_count(force: false)
  Rails.cache.fetch(['users', id, 'assigned_open_issues_count'], force: force, expires_in: COUNT_CACHE_VALIDITY_PERIOD) 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



2120
2121
2122
2123
2124
2125
2126
2127
# File 'app/models/user.rb', line 2120

def assigned_open_merge_requests_count(force: false)
  Rails.cache.fetch(['users', id, 'assigned_open_merge_requests_count', merge_request_dashboard_enabled?], force: force, expires_in: COUNT_CACHE_VALIDITY_PERIOD) do
    params = { assignee_id: id, state: 'opened', non_archived: true }
    params[:reviewer_id] = 'none' if merge_request_dashboard_enabled?

    MergeRequestsFinder.new(self, params).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)`



1419
1420
1421
1422
1423
1424
1425
1426
1427
# File 'app/models/user.rb', line 1419

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 direct or inherited membership or a project authorization



1324
1325
1326
1327
1328
1329
1330
1331
1332
1333
1334
1335
1336
1337
1338
1339
1340
1341
1342
# File 'app/models/user.rb', line 1324

def authorized_groups
  Group.unscoped do
    direct_groups_cte = Gitlab::SQL::CTE.new(:direct_groups, groups)
    direct_groups_cte_alias = direct_groups_cte.table.alias(Group.table_name)

    groups_from_authorized_projects = Group.id_in(authorized_projects.select(:namespace_id)).self_and_ancestors
    groups_from_shares = Group.joins(:shared_with_group_links)
                           .where(group_group_links: { shared_with_group_id: Group.from(direct_groups_cte_alias) })
                           .self_and_descendants

    Group
      .with(direct_groups_cte.to_arel)
      .from_union([
        Group.from(direct_groups_cte_alias).self_and_descendants,
        groups_from_authorized_projects,
        groups_from_shares
      ])
  end
end

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

Returns:

  • (Boolean)


1408
1409
1410
# File 'app/models/user.rb', line 1408

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

#authorized_project_mirrors(level) ⇒ Object



2032
2033
2034
2035
2036
2037
2038
# File 'app/models/user.rb', line 2032

def authorized_project_mirrors(level)
  projects = Ci::ProjectMirror.by_project_id(ci_project_ids_for_project_members(level))

  namespace_projects = Ci::ProjectMirror.by_namespace_id(ci_namespace_mirrors_for_group_members(level).select(:namespace_id))

  Ci::ProjectMirror.from_union([projects, namespace_projects])
end

#authorized_projects(min_access_level = nil) ⇒ Object

rubocop: enable CodeReuse/ServiceClass



1395
1396
1397
1398
1399
1400
1401
1402
1403
1404
1405
1406
# File 'app/models/user.rb', line 1395

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



1748
1749
1750
# File 'app/models/user.rb', line 1748

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

#build_default_user_detailObject



1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
1112
1113
1114
# File 'app/models/user.rb', line 1100

def build_default_user_detail
  # We will need to ensure we keep checking to see if it exists logic since this runs from
  # an after_initialize.
  # In cases where user_detail params are added during a `User.new` or create call with user_detail
  # attributes set through delegation of setters, we will already have some user_detail
  # attributes created from a built user_detail that will then be removed by an
  # initialization of a new user_detail.
  # We can see one case of that in the Users::BuildService where it assigns user attributes that can
  # have delegated user_detail attributes added by classes that inherit this class and add
  # to the user attributes hash.
  # Therefore we need to check for presence of an existing built user_detail here.
  # TODO: Add lazy loading explicitly here when we remove the user_detail
  # override in https://gitlab.com/gitlab-org/gitlab/-/issues/462919
  user_detail
end

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

Returns:

  • (Boolean)


1525
1526
1527
# File 'app/models/user.rb', line 1525

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

#can_admin_all_resources?Boolean

Returns:

  • (Boolean)


2241
2242
2243
# File 'app/models/user.rb', line 2241

def can_admin_all_resources?
  can?(:admin_all_resources)
end

#can_admin_organization?(organization) ⇒ Boolean

Returns:

  • (Boolean)


2255
2256
2257
# File 'app/models/user.rb', line 2255

def can_admin_organization?(organization)
  can?(:admin_organization, organization)
end

#can_be_deactivated?Boolean

Returns:

  • (Boolean)


2406
2407
2408
# File 'app/models/user.rb', line 2406

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

#can_be_removed?Boolean

Returns true if the user can be removed, false otherwise. A user can be removed if they do not own any groups or organizations where they are the sole owner Method ‘none?` is used to ensure faster retrieval, See gitlab.com/gitlab-org/gitlab/-/issues/417105

Returns:

  • (Boolean)


2022
2023
2024
2025
2026
# File 'app/models/user.rb', line 2022

def can_be_removed?
  return solo_owned_groups.none? && solo_owned_organizations.none? if Feature.enabled?(:ui_for_organizations)

  solo_owned_groups.none?
end

#can_change_username?Boolean

Returns:

  • (Boolean)


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

def can_change_username?
  gitlab_config.username_changing_enabled
end

#can_create_group?Boolean

Returns:

  • (Boolean)


1517
1518
1519
# File 'app/models/user.rb', line 1517

def can_create_group?
  can?(:create_group)
end

#can_create_project?Boolean

Returns:

  • (Boolean)


1506
1507
1508
# File 'app/models/user.rb', line 1506

def can_create_project?
  projects_limit_left > 0 && allow_user_to_create_group_and_project?
end

#can_leave_project?(project) ⇒ Boolean

Returns:

  • (Boolean)


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

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

#can_log_in_with_non_expired_password?Boolean

Returns:

  • (Boolean)


2402
2403
2404
# File 'app/models/user.rb', line 2402

def 
  can?(:log_in) && !password_expired_if_applicable?
end

#can_read_all_resources?Boolean

Returns:

  • (Boolean)


2237
2238
2239
# File 'app/models/user.rb', line 2237

def can_read_all_resources?
  can?(:read_all_resources)
end

#can_remove_self?Boolean

Returns:

  • (Boolean)


2028
2029
2030
# File 'app/models/user.rb', line 2028

def can_remove_self?
  true
end

#can_select_namespace?Boolean

Returns:

  • (Boolean)


1521
1522
1523
# File 'app/models/user.rb', line 1521

def can_select_namespace?
  several_namespaces? || admin
end

#can_trigger_notifications?Boolean

Returns:

  • (Boolean)


2475
2476
2477
# File 'app/models/user.rb', line 2475

def can_trigger_notifications?
  confirmed? && !blocked? && !ghost?
end

#check_for_verified_emailObject

see if the new email is already a verified secondary email



1315
1316
1317
# File 'app/models/user.rb', line 1315

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

#ci_job_token_scopeObject

This attribute hosts a Ci::JobToken::Scope object which is set when the user is authenticated successfully via CI_JOB_TOKEN.



2481
2482
2483
# File 'app/models/user.rb', line 2481

def ci_job_token_scope
  Gitlab::SafeRequestStore[ci_job_token_scope_cache_key]
end

#ci_owned_runnersObject



2040
2041
2042
2043
2044
2045
# File 'app/models/user.rb', line 2040

def ci_owned_runners
  @ci_owned_runners ||= Ci::Runner
      .from_union([ci_owned_project_runners_from_project_members,
        ci_owned_project_runners_from_group_members,
        ci_owned_group_runners])
end

#closest_non_global_group_notification_setting(group) ⇒ Object

Returns the notification_setting of the lowest group in hierarchy with non global level



2098
2099
2100
2101
2102
2103
2104
2105
2106
2107
2108
2109
2110
2111
2112
2113
2114
# File 'app/models/user.rb', line 2098

def closest_non_global_group_notification_setting(group)
  return unless group

  notification_level = NotificationSetting.levels[:global]

  if notification_settings.loaded?
    group.self_and_ancestors_asc.find do |group|
      notification_setting = notification_setting_find_by_source(group)

      next unless notification_setting
      next if NotificationSetting.levels[notification_setting&.level] == notification_level
      break notification_setting if notification_setting.present?
    end
  else
    group.notification_settings(hierarchy_order: :asc).where(user: self).where.not(level: notification_level).first
  end
end

#color_mode_idObject



1545
1546
1547
1548
1549
# File 'app/models/user.rb', line 1545

def color_mode_id
  return Gitlab::ColorModes::APPLICATION_DARK if theme_id == 11

  read_attribute(:color_mode_id)
end

#commit_email_or_defaultObject



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

def commit_email_or_default
  if self.commit_email == Gitlab::PrivateCommitEmail::TOKEN
    return private_commit_email
  end

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

#confirm_deletion_with_password?Boolean

Returns:

  • (Boolean)


1529
1530
1531
# File 'app/models/user.rb', line 1529

def confirm_deletion_with_password?
  !password_automatically_set? && allow_password_authentication?
end

#confirmation_required_on_sign_in?Boolean

Returns:

  • (Boolean)


2441
2442
2443
2444
2445
2446
2447
2448
2449
2450
2451
# File 'app/models/user.rb', line 2441

def confirmation_required_on_sign_in?
  return false if confirmed?

  if ::Gitlab::CurrentSettings.email_confirmation_setting_off?
    false
  elsif ::Gitlab::CurrentSettings.email_confirmation_setting_soft?
    !in_confirmation_period?
  elsif ::Gitlab::CurrentSettings.email_confirmation_setting_hard?
    true
  end
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.



2008
2009
2010
2011
2012
2013
2014
2015
2016
# File 'app/models/user.rb', line 2008

def contributed_projects
  events = Event.select(:project_id)
    .contributions.where(author_id: self)
    .created_after(Time.current - 1.year)
    .distinct
    .reorder(nil)

  Project.where(id: events).not_aimed_for_deletion
end

#created_recently?Boolean

Returns:

  • (Boolean)


2457
2458
2459
# File 'app/models/user.rb', line 2457

def created_recently?
  created_at > Devise.confirm_within.ago
end

#credit_card_validated_atObject



1624
1625
1626
# File 'app/models/user.rb', line 1624

def credit_card_validated_at
  credit_card_validation&.credit_card_validated_at
end

#crowd_user?Boolean

Returns:

  • (Boolean)


1592
1593
1594
1595
1596
1597
1598
# File 'app/models/user.rb', line 1592

def crowd_user?
  if identities.loaded?
    identities.find { |identity| identity.provider == 'crowd' && identity.extern_uid.present? }
  else
    identities.with_any_extern_uid('crowd').exists?
  end
end

#current_highest_access_levelObject

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



2437
2438
2439
# File 'app/models/user.rb', line 2437

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

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



1883
1884
1885
1886
1887
1888
1889
1890
1891
1892
1893
1894
1895
1896
1897
1898
# File 'app/models/user.rb', line 1883

def delete_async(deleted_by:, params: {})
  if should_delay_delete?(deleted_by)
    new_note = format(_("User deleted own account on %{timestamp}"), timestamp: Time.zone.now)
    self.note = "#{new_note}\n#{note}".strip
    UserCustomAttribute.(self)

    block_or_ban
    DeleteUserWorker.perform_in(DELETION_DELAY_IN_DAYS, deleted_by.id, id, params.to_h)

    return
  end

  block if params[:hard_delete]

  DeleteUserWorker.perform_async(deleted_by.id, id, params.to_h)
end

#deleted_own_account?Boolean

Returns:

  • (Boolean)


2537
2538
2539
# File 'app/models/user.rb', line 2537

def deleted_own_account?
  custom_attributes.by_key(UserCustomAttribute::DELETED_OWN_ACCOUNT_AT).exists?
end

#direct_groups_with_routeObject



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

def direct_groups_with_route
  groups.with_route.order_id_asc
end

#disable_two_factor!Object



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

def disable_two_factor!
  transaction do
    self.disable_webauthn!
    self.disable_two_factor_otp!
    self.reset_backup_codes!
  end
end

#disable_two_factor_otp!Object



1213
1214
1215
1216
1217
1218
1219
1220
1221
1222
# File 'app/models/user.rb', line 1213

def disable_two_factor_otp!
  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_secret_expires_at: nil
  )
end

#disable_webauthn!Object



1224
1225
1226
# File 'app/models/user.rb', line 1224

def disable_webauthn!
  self.webauthn_registrations.destroy_all # rubocop:disable Cop/DestroyAll
end

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

Returns:

  • (Boolean)


2417
2418
2419
2420
2421
# File 'app/models/user.rb', line 2417

def dismissed_callout?(feature_name:, ignore_dismissal_earlier_than: nil)
  callout = callouts_by_feature_name[feature_name]

  callout_dismissed?(callout, ignore_dismissal_earlier_than)
end

#dismissed_callout_for_group?(feature_name:, group:, ignore_dismissal_earlier_than: nil) ⇒ Boolean

Returns:

  • (Boolean)


2423
2424
2425
2426
2427
2428
# File 'app/models/user.rb', line 2423

def dismissed_callout_for_group?(feature_name:, group:, ignore_dismissal_earlier_than: nil)
  source_feature_name = "#{feature_name}_#{group.id}"
  callout = group_callouts_by_feature_name[source_feature_name]

  callout_dismissed?(callout, ignore_dismissal_earlier_than)
end

#dismissed_callout_for_project?(feature_name:, project:, ignore_dismissal_earlier_than: nil) ⇒ Boolean

Returns:

  • (Boolean)


2430
2431
2432
2433
2434
# File 'app/models/user.rb', line 2430

def dismissed_callout_for_project?(feature_name:, project:, ignore_dismissal_earlier_than: nil)
  callout = project_callouts.find_by(feature_name: feature_name, project: project)

  callout_dismissed?(callout, ignore_dismissal_earlier_than)
end

#enabled_incoming_email_tokenObject



2286
2287
2288
# File 'app/models/user.rb', line 2286

def enabled_incoming_email_token
  incoming_email_token if Gitlab::Email::IncomingEmail.supports_issue_creation?
end

#enabled_static_object_tokenObject



2282
2283
2284
# File 'app/models/user.rb', line 2282

def enabled_static_object_token
  static_object_token if Gitlab::CurrentSettings.static_objects_external_storage_enabled?
end

#ensure_namespace_correctObject



1827
1828
1829
1830
1831
1832
# File 'app/models/user.rb', line 1827

def ensure_namespace_correct
  if namespace
    namespace.path = username if username_changed?
    namespace.name = name if name_changed?
  end
end

#expanded_groups_requiring_two_factor_authenticationObject



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

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 a ‘feed_token`. we do this on read since migrating all existing users is not a feasible solution.



2271
2272
2273
# File 'app/models/user.rb', line 2271

def feed_token
  ensure_feed_token! unless Gitlab::CurrentSettings.disable_feed_token
end

#find_or_initialize_callout(feature_name) ⇒ Object



2461
2462
2463
# File 'app/models/user.rb', line 2461

def find_or_initialize_callout(feature_name)
  callouts.find_or_initialize_by(feature_name: ::Users::Callout.feature_names[feature_name])
end

#find_or_initialize_group_callout(feature_name, group_id) ⇒ Object



2465
2466
2467
2468
# File 'app/models/user.rb', line 2465

def find_or_initialize_group_callout(feature_name, group_id)
  group_callouts
    .find_or_initialize_by(feature_name: ::Users::GroupCallout.feature_names[feature_name], group_id: group_id)
end

#find_or_initialize_project_callout(feature_name, project_id) ⇒ Object



2470
2471
2472
2473
# File 'app/models/user.rb', line 2470

def find_or_initialize_project_callout(feature_name, project_id)
  project_callouts
    .find_or_initialize_by(feature_name: ::Users::ProjectCallout.feature_names[feature_name], project_id: project_id)
end

#first_group_pathsObject



1381
1382
1383
1384
1385
1386
1387
# File 'app/models/user.rb', line 1381

def first_group_paths
  first_groups = direct_groups_with_route.take(FIRST_GROUP_PATHS_LIMIT + 1)

  return if first_groups.count > FIRST_GROUP_PATHS_LIMIT

  first_groups.map(&:full_path).sort!
end

#first_nameObject



1533
1534
1535
1536
1537
# File 'app/models/user.rb', line 1533

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

#follow(user) ⇒ Object



1941
1942
1943
1944
1945
1946
1947
1948
1949
1950
1951
# File 'app/models/user.rb', line 1941

def follow(user)
  return false unless following_users_allowed?(user)

  begin
    followee = Users::UserFollowUser.create(follower_id: self.id, followee_id: user.id)
    self.followees.reset if followee.persisted?
    followee
  rescue ActiveRecord::RecordNotUnique
    nil
  end
end

#followed_by?(user) ⇒ Boolean

Returns:

  • (Boolean)


1937
1938
1939
# File 'app/models/user.rb', line 1937

def followed_by?(user)
  self.followers.include?(user)
end

#following?(user) ⇒ Boolean

Returns:

  • (Boolean)


1933
1934
1935
# File 'app/models/user.rb', line 1933

def following?(user)
  self.followees.exists?(user.id)
end

#following_users_allowed?(user) ⇒ Boolean

Returns:

  • (Boolean)


1953
1954
1955
1956
1957
# File 'app/models/user.rb', line 1953

def following_users_allowed?(user)
  return false if self.id == user.id

  enabled_following && user.enabled_following
end

#forget_me!Object



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

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

#fork_of(project) ⇒ Object



1584
1585
1586
# File 'app/models/user.rb', line 1584

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

#forkable_namespacesObject



1959
1960
1961
1962
1963
1964
1965
1966
1967
1968
1969
1970
# File 'app/models/user.rb', line 1959

def forkable_namespaces
  strong_memoize(:forkable_namespaces) do
    personal_namespace = Namespace.where(id: namespace_id)
    groups_allowing_project_creation = Groups::AcceptingProjectCreationsFinder.new(self).execute

    Namespace.from_union(
      [
        groups_allowing_project_creation,
        personal_namespace
      ])
  end
end

#from_ci_job_token?Boolean

Returns:

  • (Boolean)


2489
2490
2491
# File 'app/models/user.rb', line 2489

def from_ci_job_token?
  ci_job_token_scope.present?
end

#full_pathObject

Instance methods



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

def full_path
  username
end

#full_website_urlObject



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

def full_website_url
  return "http://#{website_url}" unless %r{\Ahttps?://}.match?(website_url)

  website_url
end

#generate_otp_backup_codes!Object



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

def generate_otp_backup_codes!
  if Gitlab::FIPS.enabled?
    generate_otp_backup_codes_pbkdf2!
  else
    super
  end
end

#generate_reset_tokenObject



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

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



2079
2080
2081
2082
2083
2084
2085
2086
2087
2088
2089
2090
2091
2092
2093
2094
2095
# File 'app/models/user.rb', line 2079

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

  # lookup in preloaded notification settings first, before making another query
  if notification_settings.loaded?
    @global_notification_setting = notification_settings.find do |notification|
      notification.source_id.nil? && notification.source_type.nil?
    end

    return @global_notification_setting if @global_notification_setting.present?
  end

  @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

#has_composite_identity?Boolean

Returns:

  • (Boolean)


2545
2546
2547
# File 'app/models/user.rb', line 2545

def has_composite_identity?
  false
end

#highest_roleObject



1620
1621
1622
# File 'app/models/user.rb', line 1620

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

#hook_attrsObject



1817
1818
1819
1820
1821
1822
1823
1824
1825
# File 'app/models/user.rb', line 1817

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

#impersonated?Boolean

Returns:

  • (Boolean)


2453
2454
2455
# File 'app/models/user.rb', line 2453

def impersonated?
  impersonator.present?
end

#inactive_messageObject

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



717
718
719
720
721
722
723
724
725
726
727
# File 'app/models/user.rb', line 717

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



2209
2210
2211
2212
2213
2214
2215
2216
2217
2218
2219
# File 'app/models/user.rb', line 2209

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_authored_todo_user_pending_todo_cache_countsObject



2194
2195
2196
2197
2198
2199
2200
# File 'app/models/user.rb', line 2194

def invalidate_authored_todo_user_pending_todo_cache_counts
  # Invalidate the todo cache counts for other users with pending todos authored by the user
  cache_keys = authored_todos.pending.distinct.pluck(:user_id).map { |id| ['users', id, 'todos_pending_count'] }
  Gitlab::Instrumentation::RedisClusterValidator.allow_cross_slot_commands do
    Rails.cache.delete_multi(cache_keys)
  end
end

#invalidate_cache_countsObject



2168
2169
2170
2171
2172
2173
# File 'app/models/user.rb', line 2168

def invalidate_cache_counts
  invalidate_issue_cache_counts
  invalidate_merge_request_cache_counts
  invalidate_todos_cache_counts
  invalidate_personal_projects_count
end

#invalidate_issue_cache_countsObject



2175
2176
2177
2178
# File 'app/models/user.rb', line 2175

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

#invalidate_merge_request_cache_countsObject



2180
2181
2182
2183
# File 'app/models/user.rb', line 2180

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

#invalidate_otp_backup_code!(code) ⇒ Object



1160
1161
1162
1163
1164
1165
1166
# File 'app/models/user.rb', line 1160

def invalidate_otp_backup_code!(code)
  if Gitlab::FIPS.enabled? && pbkdf2?
    invalidate_otp_backup_code_pdkdf2!(code)
  else
    super(code)
  end
end

#invalidate_personal_projects_countObject



2190
2191
2192
# File 'app/models/user.rb', line 2190

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

#invalidate_todos_cache_countsObject



2185
2186
2187
2188
# File 'app/models/user.rb', line 2185

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

#last_active_atObject



2410
2411
2412
2413
2414
2415
# File 'app/models/user.rb', line 2410

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

  [last_activity, ].compact.max
end

#last_nameObject



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

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

#ldap_identityObject



1608
1609
1610
# File 'app/models/user.rb', line 1608

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

#ldap_sync_timeObject



1680
1681
1682
1683
# File 'app/models/user.rb', line 1680

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)


1600
1601
1602
1603
1604
1605
1606
# File 'app/models/user.rb', line 1600

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!(opts = {}) ⇒ Object

override, from Devise



2307
2308
2309
2310
2311
# File 'app/models/user.rb', line 2307

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

#log_info(message) ⇒ Object

rubocop: enable CodeReuse/ServiceClass



1906
1907
1908
# File 'app/models/user.rb', line 1906

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

#manageable_groups(include_groups_with_developer_maintainer_access: false) ⇒ Object



1972
1973
1974
1975
1976
1977
1978
1979
1980
1981
1982
1983
1984
# File 'app/models/user.rb', line 1972

def manageable_groups(include_groups_with_developer_maintainer_access: false)
  owned_and_maintainer_group_hierarchy = owned_or_maintainers_groups.self_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

#matches_identity?(provider, extern_uid) ⇒ Boolean

Returns:

  • (Boolean)


1612
1613
1614
# File 'app/models/user.rb', line 1612

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

#max_member_access_for_group(group_id) ⇒ Object



2352
2353
2354
# File 'app/models/user.rb', line 2352

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.



2342
2343
2344
2345
2346
2347
2348
2349
2350
# File 'app/models/user.rb', line 2342

def max_member_access_for_group_ids(group_ids)
  Gitlab::SafeRequestLoader.execute(
    resource_key: max_member_access_for_resource_key(Group),
    resource_ids: group_ids,
    default_value: Gitlab::Access::NO_ACCESS
  ) 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



2335
2336
2337
# File 'app/models/user.rb', line 2335

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.



2323
2324
2325
2326
2327
2328
2329
2330
2331
2332
2333
# File 'app/models/user.rb', line 2323

def max_member_access_for_project_ids(project_ids)
  Gitlab::SafeRequestLoader.execute(
    resource_key: max_member_access_for_resource_key(Project),
    resource_ids: project_ids,
    default_value: Gitlab::Access::NO_ACCESS
  ) 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



1355
1356
1357
# File 'app/models/user.rb', line 1355

def membership_groups
  groups.self_and_descendants
end

#merge_request_dashboard_enabled?Boolean

Returns:

  • (Boolean)


2116
2117
2118
# File 'app/models/user.rb', line 2116

def merge_request_dashboard_enabled?
  Feature.enabled?(:merge_request_dashboard, self, type: :wip)
end

#namespace_commit_email_for_namespace(namespace) ⇒ Object



2531
2532
2533
2534
2535
# File 'app/models/user.rb', line 2531

def namespace_commit_email_for_namespace(namespace)
  return if namespace.nil?

  namespace_commit_emails.find_by(namespace: namespace)
end

#namespace_commit_email_for_project(project) ⇒ Object



2513
2514
2515
2516
2517
2518
# File 'app/models/user.rb', line 2513

def namespace_commit_email_for_project(project)
  return if project.nil?

  namespace_commit_emails.find_by(namespace: project.project_namespace) ||
    namespace_commit_emails.find_by(namespace: project.root_namespace)
end

#namespace_idObject



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

def namespace_id
  namespace.try :id
end

#namespace_move_dir_allowedObject



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

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

#namespaces(owned_only: false) ⇒ Object



1986
1987
1988
1989
1990
1991
# File 'app/models/user.rb', line 1986

def namespaces(owned_only: false)
  user_groups = owned_only ? owned_groups : groups
  personal_namespace = Namespace.where(id: namespace.id)

  Namespace.from_union([user_groups, personal_namespace])
end

#needs_new_otp_secret?Boolean

Returns:

  • (Boolean)


1247
1248
1249
# File 'app/models/user.rb', line 1247

def needs_new_otp_secret?
  !two_factor_otp_enabled? && otp_secret_expired?
end

#notification_email_for(notification_group) ⇒ Object



2053
2054
2055
2056
2057
2058
2059
2060
2061
2062
# File 'app/models/user.rb', line 2053

def notification_email_for(notification_group)
  # Return group-specific email address if present, otherwise return global notification email address
  group_email = if notification_settings.loaded?
                  closest_notification_email_in_group_hierarchy(notification_group)
                elsif notification_group && notification_group.respond_to?(:notification_email_for)
                  notification_group.notification_email_for(self)
                end

  group_email || notification_email_or_default
end

#notification_email_or_defaultObject



1305
1306
1307
1308
# File 'app/models/user.rb', line 1305

def notification_email_or_default
  # The notification email is the same as the primary email if undefined
  self.notification_email.presence || self.email
end

#notification_serviceObject

rubocop: disable CodeReuse/ServiceClass



1901
1902
1903
# File 'app/models/user.rb', line 1901

def notification_service
  NotificationService.new
end

#notification_settings_for(source, inherit: false) ⇒ Object



2064
2065
2066
2067
2068
2069
2070
# File 'app/models/user.rb', line 2064

def notification_settings_for(source, inherit: false)
  if notification_settings.loaded?
    notification_setting_find_by_source(source)
  else
    notification_setting_find_or_initialize_by_source(source, inherit)
  end
end

#notification_settings_for_groups(groups) ⇒ Object



2072
2073
2074
2075
# File 'app/models/user.rb', line 2072

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



1993
1994
1995
# File 'app/models/user.rb', line 1993

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

#otp_secret_expired?Boolean

Returns:

  • (Boolean)


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

def otp_secret_expired?
  return true unless otp_secret_expires_at

  otp_secret_expires_at.past?
end

#owned_projectsObject



1439
1440
1441
1442
1443
1444
1445
1446
1447
1448
1449
# File 'app/models/user.rb', line 1439

def owned_projects
  @owned_projects ||= Project.from_union(
    [
      Project.where(namespace: namespace),
      Project.joins(:project_authorizations)
        .where.not('projects.namespace_id' => namespace.id)
        .where(project_authorizations: { user_id: id, access_level: Gitlab::Access::OWNER })
    ],
    remove_duplicates: false
  )
end

#owns_organization?(organization) ⇒ Boolean

Returns:

  • (Boolean)


2245
2246
2247
2248
2249
2250
2251
2252
2253
# File 'app/models/user.rb', line 2245

def owns_organization?(organization)
  strong_memoize_with(:owns_organization, organization) do
    break false unless organization

    organization_id = organization.is_a?(Integer) ? organization : organization.id

    organization_users.where(organization_id: organization_id).owner.exists?
  end
end

#owns_runner?(runner) ⇒ Boolean

Returns:

  • (Boolean)


2047
2048
2049
2050
2051
# File 'app/models/user.rb', line 2047

def owns_runner?(runner)
  runner = runner.__getobj__ if runner.is_a?(Ci::RunnerPresenter)

  ci_owned_runners.include?(runner)
end

#password_allowed?(password) ⇒ Boolean

Returns:

  • (Boolean)


1171
1172
1173
1174
1175
1176
1177
1178
1179
# File 'app/models/user.rb', line 1171

def password_allowed?(password)
  password_allowed = true

  DISALLOWED_PASSWORDS.each do |disallowed_password|
    password_allowed = false if Devise.secure_compare(password, disallowed_password)
  end

  password_allowed
end

#password_based_omniauth_user?Boolean

Returns:

  • (Boolean)


1588
1589
1590
# File 'app/models/user.rb', line 1588

def password_based_omniauth_user?
  ldap_user? || crowd_user?
end

#password_expired?Boolean

Returns:

  • (Boolean)


2389
2390
2391
# File 'app/models/user.rb', line 2389

def password_expired?
  !!(password_expires_at && password_expires_at.past?)
end

#password_expired_if_applicable?Boolean

Returns:

  • (Boolean)


2393
2394
2395
2396
2397
2398
2399
2400
# File 'app/models/user.rb', line 2393

def password_expired_if_applicable?
  return false if bot?
  return false unless password_expired?
  return false if password_automatically_set?
  return false unless allow_password_authentication?

  true
end

#pending_invitationsObject



1765
1766
1767
# File 'app/models/user.rb', line 1765

def pending_invitations
  Members::PendingInvitationsFinder.new(verified_emails).execute
end

#pending_todo_for(target) ⇒ Object



2385
2386
2387
# File 'app/models/user.rb', line 2385

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

#personal_projects_count(force: false) ⇒ Object



2157
2158
2159
2160
2161
# File 'app/models/user.rb', line 2157

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



1869
1870
1871
1872
1873
# File 'app/models/user.rb', line 1869

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

  system_hook_service.execute_hooks_for(self, :destroy)
end

#preferred_languageObject



704
705
706
# File 'app/models/user.rb', line 704

def preferred_language
  read_attribute('preferred_language').presence || Gitlab::CurrentSettings.default_preferred_language
end

#primary_email_verified?Boolean

rubocop: enable CodeReuse/ServiceClass

Returns:

  • (Boolean)


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

def primary_email_verified?
  return false unless confirmed? && !temp_oauth_email?

  !email_changed? || new_record?
end

#private_commit_emailObject



1310
1311
1312
# File 'app/models/user.rb', line 1310

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

#project_deploy_keysObject



1616
1617
1618
# File 'app/models/user.rb', line 1616

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

#projects_limit_leftObject



1551
1552
1553
# File 'app/models/user.rb', line 1551

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.



1454
1455
1456
# File 'app/models/user.rb', line 1454

def projects_where_can_admin_issues
  authorized_projects(Gitlab::Access::PLANNER).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.



1435
1436
1437
# File 'app/models/user.rb', line 1435

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

#public_verified_emailsObject



1789
1790
1791
1792
1793
1794
1795
# File 'app/models/user.rb', line 1789

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

#read_only_attribute?(attribute) ⇒ Boolean

Returns:

  • (Boolean)


2302
2303
2304
# File 'app/models/user.rb', line 2302

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

#recent_push(project = nil) ⇒ Object

rubocop: disable CodeReuse/ServiceClass



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

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)


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

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

#refresh_authorized_projects(source: nil) ⇒ Object

rubocop: disable CodeReuse/ServiceClass



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

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

#remember_me!Object

Override Devise Rememberable#remember_me!

In Devise this method sets ‘remember_created_at` and writes the session token to the session cookie. When remember me is disabled this method ensures these values aren’t set.



1186
1187
1188
# File 'app/models/user.rb', line 1186

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

#remember_me?(token, generated_at) ⇒ Boolean

Override Devise Rememberable#remember_me?

In Devise this method compares the remember me token received from the user session and compares to the stored value. When remember me is disabled this method ensures the upstream comparison does not happen.

Returns:

  • (Boolean)


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

def remember_me?(token, generated_at)
  return false unless ::Gitlab::CurrentSettings.remember_me_enabled?

  super
end

#remove_key_cacheObject

rubocop: disable CodeReuse/ServiceClass



1876
1877
1878
# File 'app/models/user.rb', line 1876

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

#require_extra_setup_for_git_auth?Boolean

Returns:

  • (Boolean)


1480
1481
1482
# File 'app/models/user.rb', line 1480

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)


1470
1471
1472
# File 'app/models/user.rb', line 1470

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)


1466
1467
1468
# File 'app/models/user.rb', line 1466

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)


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

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

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

#require_ssh_key?Boolean

rubocop: disable CodeReuse/ServiceClass

Returns:

  • (Boolean)


1459
1460
1461
1462
1463
# File 'app/models/user.rb', line 1459

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

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

#required_terms_not_accepted?Boolean

Returns:

  • (Boolean)


2366
2367
2368
2369
# File 'app/models/user.rb', line 2366

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

#requires_ldap_check?Boolean

Returns:

  • (Boolean)


1670
1671
1672
1673
1674
1675
1676
1677
1678
# File 'app/models/user.rb', line 1670

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

#requires_usage_stats_consent?Boolean

Returns:

  • (Boolean)


2371
2372
2373
# File 'app/models/user.rb', line 2371

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

#reset_backup_codes!Object



1228
1229
1230
# File 'app/models/user.rb', line 1228

def reset_backup_codes!
  update(otp_backup_codes: nil)
end

#review_requested_open_merge_requests_count(force: false) ⇒ Object



2129
2130
2131
2132
2133
2134
2135
2136
2137
# File 'app/models/user.rb', line 2129

def review_requested_open_merge_requests_count(force: false)
  Rails.cache.fetch(['users', id, 'review_requested_open_merge_requests_count', merge_request_dashboard_enabled?], force: force, expires_in: COUNT_CACHE_VALIDITY_PERIOD) do
    if merge_request_dashboard_enabled?
      MergeRequestsFinder.new(self, assigned_user_id: id, reviewer_review_states: %w[unreviewed unapproved review_started], assigned_review_states: %w[requested_changes reviewed], state: 'opened', non_archived: true).execute.count
    else
      MergeRequestsFinder.new(self, reviewer_id: id, state: 'opened', non_archived: true).execute.count
    end
  end
end

#sanitize_attrsObject



1636
1637
1638
# File 'app/models/user.rb', line 1636

def sanitize_attrs
  sanitize_name
end

#sanitize_nameObject



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

def sanitize_name
  return unless self.name

  self.name = self.name.gsub(%r{(?:</?[^>]*>|<|>)}, '-')
end

#search_on_authorized_groups(query, use_minimum_char_limit: true) ⇒ Object

Used to search on the user’s authorized_groups effeciently by using a CTE



1345
1346
1347
1348
1349
1350
1351
1352
# File 'app/models/user.rb', line 1345

def search_on_authorized_groups(query, use_minimum_char_limit: true)
  authorized_groups_cte = Gitlab::SQL::CTE.new(:authorized_groups, authorized_groups)
  authorized_groups_cte_alias = authorized_groups_cte.table.alias(Group.table_name)
  Group
    .with(authorized_groups_cte.to_arel)
    .from(authorized_groups_cte_alias)
    .search(query, use_minimum_char_limit: use_minimum_char_limit)
end

#set_ci_job_token_scope!(job) ⇒ Object



2485
2486
2487
# File 'app/models/user.rb', line 2485

def set_ci_job_token_scope!(job)
  Gitlab::SafeRequestStore[ci_job_token_scope_cache_key] = Ci::JobToken::Scope.new(job.project)
end

#set_projects_limitObject



1661
1662
1663
1664
1665
1666
1667
1668
# File 'app/models/user.rb', line 1661

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_username_errorsObject



1848
1849
1850
1851
1852
1853
1854
1855
1856
1857
1858
1859
1860
1861
1862
1863
# File 'app/models/user.rb', line 1848

def set_username_errors
  namespace_path_errors = self.errors.delete(:"namespace.path")

  return unless namespace_path_errors&.any?

  if namespace_path_errors.include?('has already been taken') && !User.exists?(username: username)
    self.errors.add(:base, :username_exists_as_a_different_namespace)
  else
    namespace_path_errors.each do |msg|
      # Already handled by username validation.
      next if msg.ends_with?('is a reserved name')

      self.errors.add(:username, msg)
    end
  end
end

#several_namespaces?Boolean

rubocop: enable CodeReuse/ServiceClass

Returns:

  • (Boolean)


1567
1568
1569
1570
1571
1572
1573
1574
# File 'app/models/user.rb', line 1567

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



1735
1736
1737
# File 'app/models/user.rb', line 1735

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

#skip_confirmation=(bool) ⇒ Object



1120
1121
1122
# File 'app/models/user.rb', line 1120

def skip_confirmation=(bool)
  skip_confirmation! if bool
end

#skip_reconfirmation=(bool) ⇒ Object



1124
1125
1126
# File 'app/models/user.rb', line 1124

def skip_reconfirmation=(bool)
  skip_reconfirmation! if bool
end

#solo_owned_groupsObject



1692
1693
1694
1695
1696
1697
1698
1699
1700
1701
1702
1703
# File 'app/models/user.rb', line 1692

def solo_owned_groups
  # For each owned group, count the owners found in self and ancestors.
  counts = GroupMember
    .from('unnest(namespaces.traversal_ids) AS ancestors(ancestor_id), members')
    .where('members.source_id = ancestors.ancestor_id')
    .all_by_access_level(GroupMember::OWNER)
    .having('count(members.user_id) = 1')

  Group
    .from(owned_groups, :namespaces)
    .where_exists(counts)
end

#solo_owned_organizationsObject

All organizations that are owned by this user, and only this user.



1706
1707
1708
1709
1710
1711
1712
1713
1714
1715
1716
1717
1718
1719
1720
1721
1722
# File 'app/models/user.rb', line 1706

def solo_owned_organizations
  ownerships_cte = Gitlab::SQL::CTE.new(:ownerships, organization_users.owners, materialized: false)

  owned_orgs_from_cte = Organizations::Organization
    .joins('INNER JOIN ownerships ON ownerships.organization_id = organizations.id')

  counts = Organizations::OrganizationUser
    .owners
    .where('organization_users.organization_id = organizations.id')
    .group(:organization_id)
    .having('count(organization_users.user_id) = 1')

  Organizations::Organization
    .with(ownerships_cte.to_arel)
    .from(owned_orgs_from_cte, :organizations)
    .where_exists(counts)
end

#source_groups_of_two_factor_authentication_requirementObject



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

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)


1916
1917
1918
# File 'app/models/user.rb', line 1916

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.



2278
2279
2280
# File 'app/models/user.rb', line 2278

def static_object_token
  ensure_static_object_token!
end

#supports_saved_replies?Boolean

Returns:

  • (Boolean)


2541
2542
2543
# File 'app/models/user.rb', line 2541

def supports_saved_replies?
  true
end

#sync_attribute?(attribute) ⇒ Boolean

Returns:

  • (Boolean)


2290
2291
2292
2293
2294
2295
2296
2297
2298
2299
2300
# File 'app/models/user.rb', line 2290

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



1911
1912
1913
# File 'app/models/user.rb', line 1911

def system_hook_service
  SystemHooksService.new
end

#temp_oauth_email?Boolean

Returns:

  • (Boolean)


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

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

#terms_accepted?Boolean

Returns:

  • (Boolean)


2356
2357
2358
2359
2360
2361
2362
2363
2364
# File 'app/models/user.rb', line 2356

def terms_accepted?
  return true if project_bot? || service_account? || security_policy_bot? || import_user?

  if Feature.enabled?(:enforce_acceptance_of_changed_terms)
    !!ApplicationSetting::Term.latest&.accepted_by_user?(self)
  else
    accepted_term_id.present?
  end
end

#to_paramObject



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

def to_param
  username
end

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



1116
1117
1118
# File 'app/models/user.rb', line 1116

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

#todos_done_count(force: false) ⇒ Object



2145
2146
2147
2148
2149
# File 'app/models/user.rb', line 2145

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

#todos_pending_count(force: false) ⇒ Object



2151
2152
2153
2154
2155
# File 'app/models/user.rb', line 2151

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

#toggle_star(project) ⇒ Object



1920
1921
1922
1923
1924
1925
1926
1927
1928
1929
1930
1931
# File 'app/models/user.rb', line 1920

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

#trusted?Boolean

Returns:

  • (Boolean)


2527
2528
2529
# File 'app/models/user.rb', line 2527

def trusted?
  trusted_with_spam_attribute.present?
end

#try_obtain_ldap_leaseObject



1685
1686
1687
1688
1689
1690
# File 'app/models/user.rb', line 1685

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)


1232
1233
1234
# File 'app/models/user.rb', line 1232

def two_factor_enabled?
  two_factor_otp_enabled? || two_factor_webauthn_enabled?
end

#two_factor_otp_enabled?Boolean

Returns:

  • (Boolean)


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

def two_factor_otp_enabled?
  otp_required_for_login? ||
    forti_authenticator_enabled?(self) ||
    forti_token_cloud_enabled?(self) ||
    duo_auth_enabled?(self)
end

#two_factor_webauthn_enabled?Boolean

Returns:

  • (Boolean)


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

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

#unique_emailObject



1275
1276
1277
1278
1279
1280
1281
1282
1283
1284
1285
1286
1287
1288
1289
1290
1291
1292
1293
1294
# File 'app/models/user.rb', line 1275

def unique_email
  email_taken = errors.added?(:email, _('has already been taken'))

  if !email_taken && Email.where.not(user: self).where(email: email).exists?
    errors.add(:email, _('has already been taken'))
    email_taken = true
  end

  if email_taken &&
      ::Feature.enabled?(:delay_delete_own_user) &&
      User.find_by_any_email(email)&.deleted_own_account?

    help_page_url = Rails.application.routes.url_helpers.help_page_url(
      'user/profile/account/delete_account.md',
      anchor: 'delete-your-own-account'
    )

    errors.add(:email, _('is linked to an account pending deletion.'), help_page_url: help_page_url)
  end
end

#unlock_access!(unlocked_by: self) ⇒ Object

override, from Devise



2314
2315
2316
2317
2318
# File 'app/models/user.rb', line 2314

def unlock_access!(unlocked_by: self)
  audit_unlock_access(author: unlocked_by)

  super()
end

#unset_secondary_emails_matching_deleted_email!(deleted_email) ⇒ Object



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

def unset_secondary_emails_matching_deleted_email!(deleted_email)
  secondary_email_attribute_changed = false
  SECONDARY_EMAIL_ATTRIBUTES.each do |attribute|
    if read_attribute(attribute) == deleted_email
      self.write_attribute(attribute, nil)
      secondary_email_attribute_changed = true
    end
  end
  save if secondary_email_attribute_changed
end

#update_invalid_gpg_signaturesObject



1319
1320
1321
# File 'app/models/user.rb', line 1319

def update_invalid_gpg_signatures
  gpg_keys.each(&:update_invalid_gpg_signatures)
end

#update_otp_secret!Object



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

def update_otp_secret!
  self.otp_secret = User.generate_otp_secret(OTP_SECRET_LENGTH)
  self.otp_secret_expires_at = Time.current + OTP_SECRET_TTL
end

#update_todos_count_cacheObject



2163
2164
2165
2166
# File 'app/models/user.rb', line 2163

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



133
134
135
136
137
138
139
140
141
142
143
# File 'app/models/user.rb', line 133

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

  update_tracked_fields(request)

  Gitlab::ExclusiveLease.throttle(id) do
    ::Ability.forgetting(/admin/) do
      Users::UpdateService.new(self, user: self).execute(validate: false)
    end
  end
end

#update_two_factor_requirementObject



2259
2260
2261
2262
2263
2264
2265
2266
# File 'app/models/user.rb', line 2259

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

#uploads_sharding_keyObject



2549
2550
2551
# File 'app/models/user.rb', line 2549

def uploads_sharding_key
  {}
end

#user_detailObject



2380
2381
2382
2383
# File 'app/models/user.rb', line 2380

def user_detail
  # TODO: remove override in https://gitlab.com/gitlab-org/gitlab/-/issues/462919
  super.presence || build_user_detail
end

#user_preferenceObject

Avoid migrations only building user preference object when needed.



2376
2377
2378
# File 'app/models/user.rb', line 2376

def user_preference
  super.presence || build_user_preference
end

#user_projectObject



2493
2494
2495
2496
2497
# File 'app/models/user.rb', line 2493

def user_project
  strong_memoize(:user_project) do
    personal_projects.find_by(path: username, visibility_level: Gitlab::VisibilityLevel::PUBLIC)
  end
end

#user_readmeObject



2499
2500
2501
2502
2503
# File 'app/models/user.rb', line 2499

def user_readme
  strong_memoize(:user_readme) do
    user_project&.repository&.readme
  end
end

#username_changed_hookObject



1865
1866
1867
# File 'app/models/user.rb', line 1865

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

#valid_password?(password) ⇒ Boolean

Overwrites valid_password? from Devise::Models::DatabaseAuthenticatable In constant-time, check both that the password isn’t on a denylist AND that the password is the user’s password

Returns:

  • (Boolean)


1144
1145
1146
1147
1148
1149
1150
# File 'app/models/user.rb', line 1144

def valid_password?(password)
  return false unless password_allowed?(password)
  return false if password_automatically_set?
  return false unless allow_password_authentication_for_web?

  super
end

#verified_detumbled_emailsObject



1785
1786
1787
# File 'app/models/user.rb', line 1785

def verified_detumbled_emails
  emails.distinct.confirmed.pluck(:detumbled_email).compact
end

#verified_email?(check_email) ⇒ Boolean

Returns:

  • (Boolean)


1807
1808
1809
1810
1811
1812
1813
1814
1815
# File 'app/models/user.rb', line 1807

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



1777
1778
1779
1780
1781
1782
1783
# File 'app/models/user.rb', line 1777

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.uniq
end

#webhook_emailObject



2509
2510
2511
# File 'app/models/user.rb', line 2509

def webhook_email
  public_email.presence || _('[REDACTED]')
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)


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

def will_save_change_to_login?
  will_save_change_to_username? || will_save_change_to_email?
end