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_NAME_LENGTH =
127
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
SERVICE_ACCOUNT_PREFIX =
'service_account'
NOREPLY_EMAIL_DOMAIN =
"noreply.#{Gitlab.config.gitlab.host}".freeze
CI_PROJECT_RUNNERS_BATCH_SIZE =
15_000
CI_RUNNERS_PROJECT_COUNT_LIMIT =
10_000
RESERVED_AI_USERNAME_PREFIXES =
%w[duo- duo_ ai- ai_].freeze
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_SALT_LENGTH, EncryptedUserPassword::PBKDF2_SHA512_PREFIX, EncryptedUserPassword::PBKDF2_SHA512_STRATEGY

Constants included from Cells::Claimable

Cells::Claimable::CLAIMS_BUCKET_TYPE, Cells::Claimable::CLAIMS_SOURCE_TYPE, Cells::Claimable::CLAIMS_SUBJECT_TYPE, Cells::Claimable::MissingPrimaryKeyError

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 BatchDeleteDependentAssociations

BatchDeleteDependentAssociations::DEPENDENT_ASSOCIATIONS_BATCH_SIZE

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, #migrated_password?, #password=

Methods included from Cells::Claimable

#handle_grpc_error

Methods included from Users::EmailOtpEnrollment

#can_modify_email_otp_enrollment?, #email_otp_enrollment_restriction, #email_otp_required_as_boolean, #email_otp_required_as_boolean=, #must_require_email_otp?, #set_email_otp_required_after_based_on_restrictions

Methods included from Ci::PipelineScheduleOwnershipValidator

#notify_and_disable_all_pipeline_schedules_for_user

Methods included from Gitlab::InternalEventsTracking

#track_internal_event

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 BatchDeleteDependentAssociations

#delete_dependent_associations_in_batches, #dependent_associations_to_delete

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, current_transaction, declarative_enum, default_select_columns, delete_all_returning, #deleted_from_database?, id_in, id_not_in, iid_in, nullable_column?, primary_key_in, 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 Organizations::Sharding

#sharding_organization

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



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

def force_random_password
  @force_random_password
end

#impersonatorObject

Virtual attribute for impersonator



158
159
160
# File 'app/models/user.rb', line 158

def impersonator
  @impersonator
end

#loginObject

Virtual attribute for authenticating by either username or email



155
156
157
# File 'app/models/user.rb', line 155

def 
  @login
end

#skip_ai_prefix_validationObject

rubocop: enable CodeReuse/ServiceClass



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

def skip_ai_prefix_validation
  @skip_ai_prefix_validation
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



986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
1001
1002
1003
1004
1005
1006
# File 'app/models/user.rb', line 986

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

.confirm_by_token(confirmation_token) ⇒ Object

override from Devise github.com/heartcombo/devise/blob/e9c534d363cc9d552662049b38582eead87bedd6/lib/devise/models/confirmable.rb#L329C1-L354C12 modified to use Current.organization in primary query rubocop:disable Gitlab/AvoidCurrentOrganization – this method is only called by Devise::ConfirmationsController



936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
# File 'app/models/user.rb', line 936

def confirm_by_token(confirmation_token)
  if confirmation_token.blank?
    confirmable = new
    confirmable.errors.add(:confirmation_token, :blank)
    return confirmable
  end

  confirmable = find_first_by_auth_conditions(
    organization_id: Current.organization.id,
    confirmation_token: confirmation_token
  )

  unless confirmable
    confirmation_digest = Devise.token_generator.digest(self, :confirmation_token, confirmation_token)
    confirmable = find_or_initialize_with_errors(
      [:organization_id, :confirmation_token],
      { organization_id: Current.organization.id, confirmation_token: confirmation_digest }
    )
  end

  confirmable.confirm if confirmable.persisted?
  confirmable
end

.ends_with_reserved_file_extension?(username) ⇒ Boolean

Returns:

  • (Boolean)


1261
1262
1263
# File 'app/models/user.rb', line 1261

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

.filter_by_state(filter_name) ⇒ Object



1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
# File 'app/models/user.rb', line 1040

def filter_by_state(filter_name)
  case filter_name
  when 'blocked'
    blocked
  when 'blocked_pending_approval'
    blocked_pending_approval
  when 'banned'
    banned
  when 'deactivated'
    deactivated
  when 'active'
    active_without_ghosts
  end
end

.filter_items(filter_name) ⇒ Object



1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
# File 'app/models/user.rb', line 1008

def filter_items(filter_name)
  case filter_name
  when 'blocked', 'blocked_pending_approval', 'banned',
       'deactivated', 'active'
    filter_by_state(filter_name)
  when 'admins'
    admins
  when 'two_factor_disabled'
    without_two_factor
  when 'two_factor_enabled'
    with_two_factor
  when 'wop'
    without_projects
  when 'external'
    external
  when 'trusted'
    trusted
  when 'placeholder'
    placeholder
  when 'without_placeholders'
    without_placeholders
  when 'ldap_sync'
    ldap
  when "without_bots"
    without_bots
  when "bots"
    bots
  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



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

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



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

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



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

def ()
  ().take
end

.find_by_ssh_key_id(key_id) ⇒ Object

Returns a user for the given SSH key. Deploy keys are excluded.



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

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



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

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

.find_by_username!(username) ⇒ Object



1203
1204
1205
# File 'app/models/user.rb', line 1203

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



923
924
925
926
927
928
929
930
# File 'app/models/user.rb', line 923

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



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

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

.get_ids_by_ids_or_usernames(ids, usernames) ⇒ Object



1241
1242
1243
# File 'app/models/user.rb', line 1241

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

.gfm_autocomplete_search(query) ⇒ Object



1122
1123
1124
1125
1126
1127
1128
1129
1130
1131
1132
1133
1134
1135
1136
# File 'app/models/user.rb', line 1122

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

.id_exists?(id) ⇒ Boolean

Returns:

  • (Boolean)


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

def id_exists?(id)
  exists?(id: 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.



836
837
838
839
840
841
842
# File 'app/models/user.rb', line 836

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



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

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

.prefix_for_feed_tokenObject



3180
3181
3182
# File 'app/models/user.rb', line 3180

def self.prefix_for_feed_token
  ::Authn::TokenField::PrefixHelper.prepend_instance_prefix(FEED_TOKEN_PREFIX)
end

.prefix_for_incoming_mail_tokenObject



1249
1250
1251
# File 'app/models/user.rb', line 1249

def prefix_for_incoming_mail_token
  ::Authn::TokenField::PrefixHelper.prepend_instance_prefix(INCOMING_MAIL_TOKEN_PREFIX)
end

.random_passwordObject

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



878
879
880
# File 'app/models/user.rb', line 878

def random_password
  Devise.friendly_token(password_length.max)
end

.reference_patternObject

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



1222
1223
1224
1225
1226
1227
1228
1229
# File 'app/models/user.rb', line 1222

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

.reference_prefixObject



1217
1218
1219
# File 'app/models/user.rb', line 1217

def reference_prefix
  '@'
end

.reorder_by_nameObject



1146
1147
1148
# File 'app/models/user.rb', line 1146

def reorder_by_name
  reorder(:name)
end

.reset_password_by_token(attributes = {}) ⇒ Object

extracted and modified from original Devise method in github.com/heartcombo/devise/blob/731074bf09c2a0cd498c1b8a2a01434e722f94d5/lib/devise/models/recoverable.rb#L134C1-L150C12 rubocop:disable Gitlab/AvoidCurrentOrganization – this is only called by Devise from PasswordsController



885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
# File 'app/models/user.rb', line 885

def reset_password_by_token(attributes = {})
  original_token       = attributes[:reset_password_token]
  reset_password_token = Devise.token_generator.digest(self, :reset_password_token, original_token)

  recoverable = find_or_initialize_with_errors(
    [:organization_id, :reset_password_token],
    { organization_id: ::Current.organization.id, reset_password_token: reset_password_token }
  )

  if recoverable.persisted?
    if recoverable.reset_password_period_valid?
      recoverable.reset_password(attributes[:password], attributes[:password_confirmation])
    else
      recoverable.errors.add(:reset_password_token, :expired)
    end
  end

  recoverable.reset_password_token = original_token if recoverable.reset_password_token.present?
  recoverable
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.



1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
1112
1113
1114
1115
1116
1117
# File 'app/models/user.rb', line 1064

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.



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

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



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

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)


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

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

.sort_by_attribute(method) ⇒ Object

rubocop:enable Gitlab/AvoidCurrentOrganization



961
962
963
964
965
966
967
968
969
970
971
972
# File 'app/models/user.rb', line 961

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



781
782
783
784
785
786
787
788
789
# File 'app/models/user.rb', line 781

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.



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

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

.unlock_access_by_token(unlock_token) ⇒ Object



908
909
910
911
912
913
914
915
916
917
918
919
# File 'app/models/user.rb', line 908

def unlock_access_by_token(unlock_token)
  original_token = unlock_token
  unlock_token   = Devise.token_generator.digest(self, :unlock_token, unlock_token)

  lockable = find_or_initialize_with_errors(
    [:organization_id, :unlock_token],
    { organization_id: ::Current.organization.id, unlock_token: unlock_token }
  )
  lockable.unlock_access! if lockable.persisted?
  lockable.unlock_token = original_token
  lockable
end

.user_search_minimum_char_limitObject

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



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

def user_search_minimum_char_limit
  true
end

.username_exists?(username) ⇒ Boolean

Returns:

  • (Boolean)


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

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.


1142
1143
1144
# File 'app/models/user.rb', line 1142

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



1166
1167
1168
1169
1170
1171
1172
1173
1174
1175
1176
1177
1178
1179
1180
1181
1182
1183
1184
1185
1186
1187
# File 'app/models/user.rb', line 1166

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



1162
1163
1164
# File 'app/models/user.rb', line 1162

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

.with_two_factorObject



857
858
859
860
# File 'app/models/user.rb', line 857

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



818
819
820
821
822
823
824
825
826
# File 'app/models/user.rb', line 818

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



862
863
864
865
866
# File 'app/models/user.rb', line 862

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

Instance Method Details

#abuse_metadataObject



2824
2825
2826
2827
2828
2829
# File 'app/models/user.rb', line 2824

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

#accept_pending_invitations!Object



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

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

#access_levelObject

rubocop: enable CodeReuse/ServiceClass



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

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

#access_level=(new_level) ⇒ Object



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

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



1849
1850
1851
1852
1853
1854
1855
# File 'app/models/user.rb', line 1849

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



2809
2810
2811
# File 'app/models/user.rb', line 2809

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

#active_for_authentication?Boolean

Returns:

  • (Boolean)


797
798
799
800
801
802
803
# File 'app/models/user.rb', line 797

def active_for_authentication?
  return false unless super

  check_ldap_if_ldap_blocked!

  can?(:log_in)
end

#add_admin_note(new_note) ⇒ Object



2853
2854
2855
# File 'app/models/user.rb', line 2853

def add_admin_note(new_note)
  self.note = "#{new_note}\n#{self.note}"
end

#admin_unsubscribe!Object



1878
1879
1880
# File 'app/models/user.rb', line 1878

def admin_unsubscribe!
  update_column :admin_email_unsubscribed_at, Time.current
end

#all_assigned_merge_requests_count(force: false, cached_only: false) ⇒ Object



2434
2435
2436
2437
2438
2439
2440
2441
# File 'app/models/user.rb', line 2434

def all_assigned_merge_requests_count(force: false, cached_only: false)
  assigned_count = assigned_open_merge_requests_count(force: force, cached_only: cached_only)
  returned_to_you_count = returned_to_you_merge_requests_count(force: force, cached_only: cached_only)

  return if assigned_count.nil? && returned_to_you_count.nil?

  assigned_count.to_i + returned_to_you_count.to_i
end

#all_emails(include_private_email: true) ⇒ Object



1997
1998
1999
2000
2001
2002
2003
# File 'app/models/user.rb', line 1997

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).



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

def all_expanded_groups
  return groups if groups.empty?

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

#all_ssh_keysObject



1967
1968
1969
# File 'app/models/user.rb', line 1967

def all_ssh_keys
  keys.map(&:publishable_key)
end

#allow_password_authentication?Boolean

Returns:

  • (Boolean)


1710
1711
1712
# File 'app/models/user.rb', line 1710

def allow_password_authentication?
  allow_password_authentication_for_web? || allow_password_authentication_for_git?
end

#allow_password_authentication_for_git?Boolean

Returns:

  • (Boolean)


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

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)


1714
1715
1716
1717
1718
1719
# File 'app/models/user.rb', line 1714

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)


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

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)


1801
1802
1803
# File 'app/models/user.rb', line 1801

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

#any_email?(check_email) ⇒ Boolean

Returns:

  • (Boolean)


2027
2028
2029
2030
2031
2032
2033
2034
2035
# File 'app/models/user.rb', line 2027

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



2064
2065
2066
2067
2068
2069
2070
2071
2072
2073
2074
2075
2076
2077
2078
# File 'app/models/user.rb', line 2064

def assign_personal_namespace(organization)
  return namespace if namespace

  namespace_attributes = { path: username, name: name }

  if organization
    namespace_attributes[:organization] = organization
    namespace_attributes[:visibility_level] = organization.visibility_level
  end

  build_namespace(namespace_attributes)
  namespace.build_namespace_settings

  namespace
end

#assigned_open_issues_count(force: false) ⇒ Object



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

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, cached_only: false) ⇒ Object



2398
2399
2400
2401
2402
2403
2404
2405
2406
2407
2408
2409
2410
2411
2412
2413
2414
2415
2416
2417
2418
2419
2420
2421
2422
2423
2424
2425
2426
2427
2428
2429
2430
2431
2432
# File 'app/models/user.rb', line 2398

def assigned_open_merge_requests_count(force: false, cached_only: false)
  Rails.cache.fetch(['users', id, 'assigned_open_merge_requests_count', user_preference.role_based?, merge_request_dashboard_show_drafts?], force: force, expires_in: COUNT_CACHE_VALIDITY_PERIOD, skip_nil: true) do
    return if cached_only # rubocop:disable Cop/AvoidReturnFromBlocks -- return from method to prevent caching nil when only reading cache

    params = {
      state: 'opened',
      non_archived: true,
      include_assigned: true,
      author_id: id
    }

    unless user_preference.role_based?
      params[:or] = { reviewer_wildcard: 'none', review_states: %w[reviewed requested_changes], only_reviewer_username: 'GitLabDuo' }
    end

    unless merge_request_dashboard_show_drafts?
      params[:draft] = false
      params[:or] = { reviewer_wildcard: 'NONE', only_reviewer_username: ::Users::Internal.duo_code_review_bot.username }
    end

    begin
      MergeRequestsFinder.new(self, params).execute.count
    # rubocop:disable Database/RescueStatementTimeout, Database/RescueQueryCanceled -- Expensive query can throw 500 error, temporary while the query gets improved
    rescue ActiveRecord::StatementTimeout, ActiveRecord::QueryCanceled => e
      # rubocop:enable Database/RescueStatementTimeout, Database/RescueQueryCanceled
      Gitlab::AppLogger.error(
        message: 'Timeout counting assigned merge requests',
        user_id: id,
        error: e.message
      )

      nil
    end
  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)`



1655
1656
1657
1658
1659
1660
1661
1662
1663
# File 'app/models/user.rb', line 1655

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_groups(with_minimal_access: true, include_project_authorizations: true) ⇒ Object

Returns the groups a user has access to, either through direct or inherited membership or a project authorization with_minimal_access is used in EE only



1551
1552
1553
1554
1555
1556
1557
1558
1559
1560
1561
1562
1563
1564
1565
1566
1567
1568
1569
1570
1571
1572
1573
1574
# File 'app/models/user.rb', line 1551

def authorized_groups(with_minimal_access: true, include_project_authorizations: true)
  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_shares = Group.joins(:shared_with_group_links)
                           .where(group_group_links: { shared_with_group_id: Group.from(direct_groups_cte_alias) })
                           .self_and_descendants

    union_relations = [
      Group.from(direct_groups_cte_alias).self_and_descendants,
      groups_from_shares
    ]

    if include_project_authorizations
      groups_from_authorized_projects = Group.id_in(authorized_projects.select(:namespace_id)).self_and_ancestors
      union_relations << groups_from_authorized_projects
    end

    Group
      .with(direct_groups_cte.to_arel)
      .from_union(union_relations)
  end
end

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

Returns:

  • (Boolean)


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

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

#authorized_project_mirrors(level) ⇒ Object



2267
2268
2269
2270
2271
2272
2273
# File 'app/models/user.rb', line 2267

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



1631
1632
1633
1634
1635
1636
1637
1638
1639
1640
1641
1642
# File 'app/models/user.rb', line 1631

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

#authorized_root_ancestor_idsObject



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

def authorized_root_ancestor_ids
  authorized_groups&.top_level&.pluck(:id)
end

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

rubocop: disable CodeReuse/ServiceClass



1976
1977
1978
# File 'app/models/user.rb', line 1976

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

#build_default_user_detailObject



1282
1283
1284
1285
1286
1287
1288
1289
1290
1291
1292
1293
1294
# File 'app/models/user.rb', line 1282

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.
  user_detail || build_user_detail
end

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

Returns:

  • (Boolean)


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

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

#can_access_admin_area?Boolean

Returns:

  • (Boolean)


2873
2874
2875
# File 'app/models/user.rb', line 2873

def can_access_admin_area?
  admin?
end

#can_admin_all_resources?Boolean

Returns:

  • (Boolean)


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

def can_admin_all_resources?
  can?(:admin_all_resources)
end

#can_admin_organization?(organization) ⇒ Boolean

Returns:

  • (Boolean)


2564
2565
2566
# File 'app/models/user.rb', line 2564

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

#can_be_deactivated?Boolean

Returns:

  • (Boolean)


2710
2711
2712
# File 'app/models/user.rb', line 2710

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)


2257
2258
2259
2260
2261
# File 'app/models/user.rb', line 2257

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)


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

def can_change_username?
  gitlab_config.username_changing_enabled
end

#can_create_group?Boolean

Returns:

  • (Boolean)


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

def can_create_group?
  can?(:create_group)
end

#can_create_project?Boolean

Returns:

  • (Boolean)


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

def can_create_project?
  projects_limit_left > 0 && allow_user_to_create_group_and_project?
end

#can_leave_group?(group) ⇒ Boolean

Returns:

  • (Boolean)


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

def can_leave_group?(group)
  can?(:destroy_group_member, group.member(self))
end

#can_leave_project?(member_or_project) ⇒ Boolean

Returns:

  • (Boolean)


1949
1950
1951
1952
1953
1954
1955
# File 'app/models/user.rb', line 1949

def can_leave_project?(member_or_project)
  return can?(:destroy_project_member, member_or_project) if member_or_project.is_a?(ProjectMember)

  return can?(:destroy_project_member, member_or_project.member(self)) if member_or_project.is_a?(Project)

  false
end

#can_log_in_with_non_expired_password?Boolean

Returns:

  • (Boolean)


2706
2707
2708
# File 'app/models/user.rb', line 2706

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

#can_read_all_resources?Boolean

Returns:

  • (Boolean)


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

def can_read_all_resources?
  can?(:read_all_resources)
end

#can_remove_self?Boolean

Returns:

  • (Boolean)


2263
2264
2265
# File 'app/models/user.rb', line 2263

def can_remove_self?
  true
end

#can_select_namespace?Boolean

Returns:

  • (Boolean)


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

def can_select_namespace?
  has_groups_allowing_project_creation? || admin
end

#can_trigger_notifications?Boolean

Returns:

  • (Boolean)


2779
2780
2781
# File 'app/models/user.rb', line 2779

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

#can_use_existing_webauthn_authenticator_for_2fa?Boolean

This predicate allows either passkeys or second_factor_webauthn_registrations to be used as a 2FA method without breaking existing 2FA implementations.

Once passkeys are fully rolled out with adequate recovery options, #two_factor_webauthn_enabled? will become second_factor_webauthn_registrations.any? || passkeys_enabled?

Returns:

  • (Boolean)


1464
1465
1466
# File 'app/models/user.rb', line 1464

def can_use_existing_webauthn_authenticator_for_2fa?
  two_factor_webauthn_enabled? || passkey_via_2fa_enabled?
end

#check_for_verified_emailObject

see if the new email is already a verified secondary email



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

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

#ci_available_project_runnersObject



2283
2284
2285
2286
2287
2288
2289
2290
2291
2292
2293
2294
2295
2296
2297
2298
# File 'app/models/user.rb', line 2283

def ci_available_project_runners
  project_ids = project_authorizations.where(access_level: Gitlab::Access::MAINTAINER..).pluck(:project_id)

  # track the size of project_ids to optimise this query further in future
  track_ci_available_project_runners_query(project_ids.size)

  return Ci::Runner.belonging_to_project(project_ids) if project_ids.size <= CI_RUNNERS_PROJECT_COUNT_LIMIT

  projects_with_runners = Set.new

  project_ids.each_slice(CI_PROJECT_RUNNERS_BATCH_SIZE) do |ids|
    projects_with_runners.merge(Ci::RunnerProject.existing_project_ids(ids))
  end

  Ci::Runner.belonging_to_project(projects_with_runners)
end

#ci_available_runnersObject

Lists runners that are available to the user (group runners assigned to groups where the user has owner access to and project runners assigned to projects the user has maintainer access to)



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

def ci_available_runners
  Ci::Runner.from_union([ci_available_project_runners, ci_available_group_runners])
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.



2785
2786
2787
# File 'app/models/user.rb', line 2785

def ci_job_token_scope
  Gitlab::SafeRequestStore[ci_job_token_scope_cache_key]
end

#closest_non_global_group_notification_setting(group) ⇒ Object

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



2345
2346
2347
2348
2349
2350
2351
2352
2353
2354
2355
2356
2357
2358
2359
2360
2361
# File 'app/models/user.rb', line 2345

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



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

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

  read_attribute(:color_mode_id)
end

#commit_email_or_defaultObject



1522
1523
1524
1525
1526
1527
1528
1529
# File 'app/models/user.rb', line 1522

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

#composite_identity_enforced!Object



2887
2888
2889
# File 'app/models/user.rb', line 2887

def composite_identity_enforced!
  @composite_identity_enforced_override = true
end

#composite_identity_enforced?Boolean

Returns:

  • (Boolean)


2881
2882
2883
2884
2885
# File 'app/models/user.rb', line 2881

def composite_identity_enforced?
  return !!@composite_identity_enforced_override if defined?(@composite_identity_enforced_override)

  !!self[:composite_identity_enforced]
end

#confirm_deletion_with_password?Boolean

Returns:

  • (Boolean)


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

def confirm_deletion_with_password?
  !password_automatically_set? && allow_password_authentication?
end

#confirmation_required_on_sign_in?Boolean

Returns:

  • (Boolean)


2745
2746
2747
2748
2749
2750
2751
2752
2753
2754
2755
# File 'app/models/user.rb', line 2745

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.



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

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)


2761
2762
2763
# File 'app/models/user.rb', line 2761

def created_recently?
  created_at > Devise.confirm_within.ago
end

#credit_card_validated_atObject



1845
1846
1847
# File 'app/models/user.rb', line 1845

def credit_card_validated_at
  credit_card_validation&.credit_card_validated_at
end

#crowd_user?Boolean

Returns:

  • (Boolean)


1813
1814
1815
1816
1817
1818
1819
# File 'app/models/user.rb', line 1813

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



2741
2742
2743
# File 'app/models/user.rb', line 2741

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

#dashboard=(value) ⇒ Object

Override enum setter for dashboard to support flipped mapping for rollout



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

def dashboard=(value)
  if should_use_flipped_dashboard_mapping_for_rollout?
    numeric_value = dashboard_enum_mapping[value.to_s]
    super(numeric_value)
  else
    super(value)
  end
end

#dashboard_enum_mappingObject



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

def dashboard_enum_mapping
  return self.class.dashboards unless should_use_flipped_dashboard_mapping_for_rollout?

  self.class.dashboards.dup.merge(
    projects: self.class.dashboards[:homepage],
    homepage: self.class.dashboards[:projects]
  ).with_indifferent_access
end

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



2115
2116
2117
2118
2119
2120
2121
2122
2123
2124
2125
2126
2127
2128
2129
2130
# File 'app/models/user.rb', line 2115

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)


2841
2842
2843
# File 'app/models/user.rb', line 2841

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

#destroy_second_factor_webauthn_device(device_id) ⇒ Object



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

def destroy_second_factor_webauthn_device(device_id)
  self.second_factor_webauthn_registrations.find(device_id).destroy
end

#direct_groups_with_routeObject



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

def direct_groups_with_route
  groups.with_route.order_id_asc
end

#disable_second_factor_webauthn!Object



1423
1424
1425
# File 'app/models/user.rb', line 1423

def disable_second_factor_webauthn!
  self.second_factor_webauthn_registrations.destroy_all # rubocop:disable Cop/DestroyAll
end

#disable_two_factor!Object



1403
1404
1405
1406
1407
1408
1409
1410
# File 'app/models/user.rb', line 1403

def disable_two_factor!
  transaction do
    self.disable_second_factor_webauthn!
    self.disable_two_factor_otp!
    self.reset_backup_codes!
    self.set_email_otp_required_after_based_on_restrictions(save: true)
  end
end

#disable_two_factor_otp!Object



1412
1413
1414
1415
1416
1417
1418
1419
1420
1421
# File 'app/models/user.rb', line 1412

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

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

Returns:

  • (Boolean)


2721
2722
2723
2724
2725
# File 'app/models/user.rb', line 2721

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)


2727
2728
2729
2730
2731
2732
# File 'app/models/user.rb', line 2727

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)


2734
2735
2736
2737
2738
# File 'app/models/user.rb', line 2734

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

#effective_dashboard_for_routingObject

Returns the effective dashboard value for routing purposes For GitLab team members with feature flag enabled, flips homepage/projects values



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

def effective_dashboard_for_routing
  return dashboard unless should_use_flipped_dashboard_mapping_for_rollout?

  case dashboard
  when 'projects'
    'homepage'
  when 'homepage'
    'projects'
  else
    dashboard
  end
end

#email_based_otp_required?Boolean

Returns:

  • (Boolean)


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

def email_based_otp_required?
  Feature.enabled?(:email_based_mfa, self) &&
    !!email_otp_required_after&.past?
end

#enabled_incoming_email_tokenObject



2595
2596
2597
# File 'app/models/user.rb', line 2595

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

#enabled_static_object_tokenObject



2591
2592
2593
# File 'app/models/user.rb', line 2591

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

#ensure_namespace_correctObject



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

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



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

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.



2580
2581
2582
# File 'app/models/user.rb', line 2580

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

#find_or_initialize_callout(feature_name) ⇒ Object



2765
2766
2767
# File 'app/models/user.rb', line 2765

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



2769
2770
2771
2772
# File 'app/models/user.rb', line 2769

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



2774
2775
2776
2777
# File 'app/models/user.rb', line 2774

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



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

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



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

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

#follow(user) ⇒ Object



2173
2174
2175
2176
2177
2178
2179
2180
2181
2182
2183
# File 'app/models/user.rb', line 2173

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)


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

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

#following?(user) ⇒ Boolean

Returns:

  • (Boolean)


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

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

#following_users_allowed?(user) ⇒ Boolean

Returns:

  • (Boolean)


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

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

  enabled_following && user.enabled_following
end

#forget_me!Object



1375
1376
1377
# File 'app/models/user.rb', line 1375

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

#fork_of(project) ⇒ Object



1805
1806
1807
# File 'app/models/user.rb', line 1805

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

#forkable_namespacesObject



2195
2196
2197
2198
2199
2200
2201
2202
2203
2204
2205
# File 'app/models/user.rb', line 2195

def forkable_namespaces
  strong_memoize(:forkable_namespaces) do
    personal_namespace = Namespace.where(id: namespace_id)

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

#free_or_trial_owned_group_idsObject



2877
2878
2879
# File 'app/models/user.rb', line 2877

def free_or_trial_owned_group_ids
  @free_or_trial_owned_group_ids ||= owned_groups.free_or_trial.ids
end

#from_ci_job_token?Boolean

Returns:

  • (Boolean)


2793
2794
2795
# File 'app/models/user.rb', line 2793

def from_ci_job_token?
  ci_job_token_scope.present?
end

#full_pathObject



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

def full_path
  username
end

#full_website_urlObject



1957
1958
1959
1960
1961
# File 'app/models/user.rb', line 1957

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

  website_url
end

#generate_otp_backup_codes!Object



1337
1338
1339
1340
1341
1342
1343
# File 'app/models/user.rb', line 1337

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

#generate_reset_tokenObject



1312
1313
1314
1315
1316
1317
1318
1319
# File 'app/models/user.rb', line 1312

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

#get_all_webauthn_credential_idsObject

Instance methods



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

def get_all_webauthn_credential_ids
  webauthn_registrations.pluck(:credential_xid)
end

#global_notification_settingObject

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



2326
2327
2328
2329
2330
2331
2332
2333
2334
2335
2336
2337
2338
2339
2340
2341
2342
# File 'app/models/user.rb', line 2326

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_groups_allowing_project_creation?Boolean

Returns:

  • (Boolean)


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

def has_groups_allowing_project_creation?
  groups_allowing_project_creation.exists?
end

#has_multiple_organizations?Boolean

Returns:

  • (Boolean)


1945
1946
1947
# File 'app/models/user.rb', line 1945

def has_multiple_organizations?
  organization_users.many?
end

#highest_roleObject



1841
1842
1843
# File 'app/models/user.rb', line 1841

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

#hook_attrsObject



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

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

#impersonated?Boolean

Returns:

  • (Boolean)


2757
2758
2759
# File 'app/models/user.rb', line 2757

def impersonated?
  impersonator.present?
end

#inactive_messageObject

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



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

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



2516
2517
2518
2519
2520
2521
2522
2523
2524
2525
2526
# File 'app/models/user.rb', line 2516

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_all_remember_tokens!Object

This is a copy of #forget_me! without the check for expire_all_remember_me_on_sign_out github.com/heartcombo/devise/blob/v4.9.4/lib/devise/models/rememberable.rb#L58-L63

We need a separate method because we disabled that setting but we also need to be able to manually expire these tokens when a session is manually destroyed



1384
1385
1386
1387
1388
1389
1390
# File 'app/models/user.rb', line 1384

def invalidate_all_remember_tokens!
  return unless persisted?

  self.remember_token = nil if respond_to?(:remember_token)
  self.remember_created_at = nil
  save(validate: false)
end

#invalidate_authored_todo_user_pending_todo_cache_countsObject



2501
2502
2503
2504
2505
2506
2507
# File 'app/models/user.rb', line 2501

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



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

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



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

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



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

def invalidate_merge_request_cache_counts
  Rails.cache.delete(['users', id, 'assigned_open_merge_requests_count', user_preference.role_based?, merge_request_dashboard_show_drafts?])
  Rails.cache.delete(['users', id, 'review_requested_open_merge_requests_count'])
  Rails.cache.delete(['users', id, 'returned_to_you_merge_requests_count'])
end

#invalidate_otp_backup_code!(code) ⇒ Object



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

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



2497
2498
2499
# File 'app/models/user.rb', line 2497

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

#invalidate_todos_cache_countsObject



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

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

#last_active_atObject



2714
2715
2716
2717
2718
2719
# File 'app/models/user.rb', line 2714

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

  [last_activity, ].compact.max
end

#last_nameObject



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

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

#ldap_identityObject



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

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

#ldap_sync_timeObject



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

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)


1821
1822
1823
1824
1825
1826
1827
# File 'app/models/user.rb', line 1821

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



2616
2617
2618
2619
2620
# File 'app/models/user.rb', line 2616

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



2138
2139
2140
# File 'app/models/user.rb', line 2138

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

#manageable_groups(include_groups_with_developer_access: false) ⇒ Object



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

def manageable_groups(include_groups_with_developer_access: false)
  owned_and_maintainer_group_hierarchy = owned_or_maintainers_groups.self_and_descendants

  if include_groups_with_developer_access
    union_sql = ::Gitlab::SQL::Union.new(
      [owned_and_maintainer_group_hierarchy,
        groups_with_developer_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)


1833
1834
1835
# File 'app/models/user.rb', line 1833

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

#max_member_access_for_group(group_id) ⇒ Object



2661
2662
2663
# File 'app/models/user.rb', line 2661

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.



2651
2652
2653
2654
2655
2656
2657
2658
2659
# File 'app/models/user.rb', line 2651

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



2644
2645
2646
# File 'app/models/user.rb', line 2644

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.



2632
2633
2634
2635
2636
2637
2638
2639
2640
2641
2642
# File 'app/models/user.rb', line 2632

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

#member_of_organization?(organization) ⇒ Boolean

Returns:

  • (Boolean)


2558
2559
2560
2561
2562
# File 'app/models/user.rb', line 2558

def member_of_organization?(organization)
  strong_memoize_with(:in_organization, organization) do
    organization_membership_exists?(organization)
  end
end

#membership_groupsObject

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



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

def membership_groups
  groups.self_and_descendants
end

#merge_request_dashboard_show_drafts?Boolean

Returns:

  • (Boolean)


2363
2364
2365
# File 'app/models/user.rb', line 2363

def merge_request_dashboard_show_drafts?
  merge_request_dashboard_show_drafts
end

#namespace_commit_email_for_namespace(namespace) ⇒ Object



2835
2836
2837
2838
2839
# File 'app/models/user.rb', line 2835

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



2817
2818
2819
2820
2821
2822
# File 'app/models/user.rb', line 2817

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

rubocop: enable CodeReuse/ServiceClass



1797
1798
1799
# File 'app/models/user.rb', line 1797

def namespace_id
  namespace.try :id
end

#namespace_move_dir_allowedObject



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

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



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

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)


1468
1469
1470
# File 'app/models/user.rb', line 1468

def needs_new_otp_secret?
  !two_factor_otp_enabled? && otp_secret_expired?
end

#notification_email_for(notification_group) ⇒ Object



2300
2301
2302
2303
2304
2305
2306
2307
2308
2309
# File 'app/models/user.rb', line 2300

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



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

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



2133
2134
2135
# File 'app/models/user.rb', line 2133

def notification_service
  NotificationService.new
end

#notification_settings_for(source, inherit: false) ⇒ Object



2311
2312
2313
2314
2315
2316
2317
# File 'app/models/user.rb', line 2311

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



2319
2320
2321
2322
# File 'app/models/user.rb', line 2319

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



2228
2229
2230
# File 'app/models/user.rb', line 2228

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

#otp_secret_expired?Boolean

Returns:

  • (Boolean)


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

def otp_secret_expired?
  return true unless otp_secret_expires_at

  otp_secret_expires_at.past?
end

#owned_projectsObject



1665
1666
1667
1668
1669
1670
1671
1672
1673
1674
1675
# File 'app/models/user.rb', line 1665

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)


2552
2553
2554
2555
2556
# File 'app/models/user.rb', line 2552

def owns_organization?(organization)
  strong_memoize_with(:owns_organization, organization) do
    organization_membership_exists?(organization, :owner)
  end
end

#passkey_via_2fa_enabled?Boolean

Returns:

  • (Boolean)


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

def passkey_via_2fa_enabled?
  Feature.enabled?(:passkeys, self) && self.two_factor_enabled? && self.passkeys_enabled?
end

#passkeys_enabled?Boolean

Returns:

  • (Boolean)


1450
1451
1452
# File 'app/models/user.rb', line 1450

def passkeys_enabled?
  passkeys.any?
end

#password_allowed?(password) ⇒ Boolean

Returns:

  • (Boolean)


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

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)


1809
1810
1811
# File 'app/models/user.rb', line 1809

def password_based_omniauth_user?
  ldap_user? || crowd_user?
end

#password_expired?Boolean

Returns:

  • (Boolean)


2693
2694
2695
# File 'app/models/user.rb', line 2693

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

#password_expired_if_applicable?Boolean

Returns:

  • (Boolean)


2697
2698
2699
2700
2701
2702
2703
2704
# File 'app/models/user.rb', line 2697

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



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

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

#pending_todo_for(target) ⇒ Object



2689
2690
2691
# File 'app/models/user.rb', line 2689

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

#personal_projects_count(force: false) ⇒ Object



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

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



2101
2102
2103
2104
2105
# File 'app/models/user.rb', line 2101

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

  system_hook_service.execute_hooks_for(self, :destroy)
end

#preferred_languageObject



793
794
795
# File 'app/models/user.rb', line 793

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

#primary_email_verified?Boolean

rubocop: enable CodeReuse/ServiceClass

Returns:

  • (Boolean)


1981
1982
1983
1984
1985
# File 'app/models/user.rb', line 1981

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

  !email_changed? || new_record?
end

#private_commit_emailObject



1536
1537
1538
# File 'app/models/user.rb', line 1536

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

#project_deploy_keysObject



1837
1838
1839
# File 'app/models/user.rb', line 1837

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

#projects_limit_leftObject



1781
1782
1783
# File 'app/models/user.rb', line 1781

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.



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

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

#public_verified_emailsObject



2019
2020
2021
2022
2023
2024
2025
# File 'app/models/user.rb', line 2019

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)


2611
2612
2613
# File 'app/models/user.rb', line 2611

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

#readable_by?(user) ⇒ Boolean

Returns:

  • (Boolean)


1300
1301
1302
# File 'app/models/user.rb', line 1300

def readable_by?(user)
  id == user.id
end

#recent_push(project = nil) ⇒ Object

rubocop: disable CodeReuse/ServiceClass



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

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)


1321
1322
1323
# File 'app/models/user.rb', line 1321

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



1626
1627
1628
# File 'app/models/user.rb', line 1626

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.



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

def remember_me!
  super if ::Gitlab::Database.read_write? && ::Gitlab::CurrentSettings.allow_user_remember_me?
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)


1397
1398
1399
1400
1401
# File 'app/models/user.rb', line 1397

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

  super
end

#remove_key_cacheObject

rubocop: disable CodeReuse/ServiceClass



2108
2109
2110
# File 'app/models/user.rb', line 2108

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

#require_extra_setup_for_git_auth?Boolean

Returns:

  • (Boolean)


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

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)


1696
1697
1698
# File 'app/models/user.rb', line 1696

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)


1692
1693
1694
# File 'app/models/user.rb', line 1692

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)


1700
1701
1702
1703
1704
# File 'app/models/user.rb', line 1700

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)


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

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

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

#required_terms_not_accepted?Boolean

Returns:

  • (Boolean)


2675
2676
2677
2678
# File 'app/models/user.rb', line 2675

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

#requires_ldap_check?Boolean

Returns:

  • (Boolean)


1891
1892
1893
1894
1895
1896
1897
1898
1899
# File 'app/models/user.rb', line 1891

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)


2680
2681
2682
# File 'app/models/user.rb', line 2680

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



1431
1432
1433
# File 'app/models/user.rb', line 1431

def reset_backup_codes!
  update(otp_backup_codes: nil)
end

#returned_to_you_merge_requests_count(force: false, cached_only: false) ⇒ Object



2367
2368
2369
2370
2371
2372
2373
2374
2375
2376
2377
2378
2379
2380
2381
2382
2383
2384
2385
2386
2387
2388
2389
2390
2391
2392
2393
2394
2395
2396
# File 'app/models/user.rb', line 2367

def returned_to_you_merge_requests_count(force: false, cached_only: false)
  return if merge_request_dashboard_show_drafts? || user_preference.role_based?

  Rails.cache.fetch(['users', id, 'returned_to_you_merge_requests_count'], force: force, expires_in: COUNT_CACHE_VALIDITY_PERIOD, skip_nil: true) do
    return if cached_only # rubocop:disable Cop/AvoidReturnFromBlocks -- return from method to prevent caching nil when only reading cache

    params = {
      state: 'opened',
      non_archived: true,
      include_assigned: true,
      author_id: id,
      review_states: %w[reviewed requested_changes],
      ignored_reviewer_username: ::Users::Internal.duo_code_review_bot.username
    }

    begin
      MergeRequestsFinder.new(self, params).execute.count
    # rubocop:disable Database/RescueStatementTimeout, Database/RescueQueryCanceled -- Expensive query can throw 500 error, temporary while the query gets improved
    rescue ActiveRecord::StatementTimeout, ActiveRecord::QueryCanceled => e
      # rubocop:enable Database/RescueStatementTimeout, Database/RescueQueryCanceled
      Gitlab::AppLogger.error(
        message: 'Timeout counting assigned merge requests',
        user_id: id,
        error: e.message
      )

      nil
    end
  end
end

#review_requested_open_merge_requests_count(force: false, cached_only: false) ⇒ Object



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

def review_requested_open_merge_requests_count(force: false, cached_only: false)
  Rails.cache.fetch(['users', id, 'review_requested_open_merge_requests_count'], force: force, expires_in: COUNT_CACHE_VALIDITY_PERIOD) do
    return if cached_only # rubocop:disable Cop/AvoidReturnFromBlocks -- return from method to prevent caching nil when only reading cache

    params = { reviewer_id: id, state: 'opened', non_archived: true, review_states: %w[unapproved unreviewed review_started] }

    MergeRequestsFinder.new(self, params).execute.count
  end
end

#sanitize_attrsObject



1857
1858
1859
# File 'app/models/user.rb', line 1857

def sanitize_attrs
  sanitize_name
end

#sanitize_nameObject



1861
1862
1863
1864
1865
# File 'app/models/user.rb', line 1861

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



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

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



2789
2790
2791
# File 'app/models/user.rb', line 2789

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



1882
1883
1884
1885
1886
1887
1888
1889
# File 'app/models/user.rb', line 1882

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



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

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

#short_website_urlObject



1963
1964
1965
# File 'app/models/user.rb', line 1963

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

#should_use_flipped_dashboard_mapping_for_rollout?Boolean

Determines if this user should use flipped dashboard enum mapping

Returns:

  • (Boolean)


475
476
477
478
479
480
481
482
# File 'app/models/user.rb', line 475

def should_use_flipped_dashboard_mapping_for_rollout?
  return false unless Feature.enabled?(:personal_homepage, self)

  # Don't flip for SM admins who have no authorized projects as they go through different onboarding flow.
  return false if self_managed_admin? && !authorized_projects.exists?

  true
end

#skip_confirmation=(bool) ⇒ Object



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

def skip_confirmation=(bool)
  skip_confirmation! if bool
end

#skip_reconfirmation=(bool) ⇒ Object



1308
1309
1310
# File 'app/models/user.rb', line 1308

def skip_reconfirmation=(bool)
  skip_reconfirmation! if bool
end

#solo_owned_groupsObject



1913
1914
1915
1916
1917
1918
1919
1920
1921
1922
1923
1924
# File 'app/models/user.rb', line 1913

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.



1927
1928
1929
1930
1931
1932
1933
1934
1935
1936
1937
1938
1939
1940
1941
1942
1943
# File 'app/models/user.rb', line 1927

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



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

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)


2148
2149
2150
# File 'app/models/user.rb', line 2148

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.



2587
2588
2589
# File 'app/models/user.rb', line 2587

def static_object_token
  ensure_static_object_token!
end

#support_pinObject

rubocop: enable CodeReuse/ServiceClass



2865
2866
2867
# File 'app/models/user.rb', line 2865

def support_pin
  support_pin_data&.fetch(:pin, nil)
end

#support_pin_dataObject

rubocop: disable CodeReuse/ServiceClass



2858
2859
2860
2861
2862
# File 'app/models/user.rb', line 2858

def support_pin_data
  strong_memoize(:support_pin_data) do
    Users::SupportPin::RetrieveService.new(self).execute
  end
end

#support_pin_expires_atObject



2869
2870
2871
# File 'app/models/user.rb', line 2869

def support_pin_expires_at
  support_pin_data&.fetch(:expires_at, nil)
end

#supports_saved_replies?Boolean

Returns:

  • (Boolean)


2845
2846
2847
# File 'app/models/user.rb', line 2845

def supports_saved_replies?
  true
end

#sync_attribute?(attribute) ⇒ Boolean

Returns:

  • (Boolean)


2599
2600
2601
2602
2603
2604
2605
2606
2607
2608
2609
# File 'app/models/user.rb', line 2599

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



2143
2144
2145
# File 'app/models/user.rb', line 2143

def system_hook_service
  SystemHooksService.new
end

#temp_oauth_email?Boolean

Returns:

  • (Boolean)


1971
1972
1973
# File 'app/models/user.rb', line 1971

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

#terms_accepted?Boolean

Returns:

  • (Boolean)


2665
2666
2667
2668
2669
2670
2671
2672
2673
# File 'app/models/user.rb', line 2665

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



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

def to_param
  username
end

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



1296
1297
1298
# File 'app/models/user.rb', line 1296

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

#todos_pending_count(force: false) ⇒ Object



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

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(users: self, state: :pending).execute.count
  end
end

#toggle_star(project) ⇒ Object



2152
2153
2154
2155
2156
2157
2158
2159
2160
2161
2162
2163
# File 'app/models/user.rb', line 2152

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)


2831
2832
2833
# File 'app/models/user.rb', line 2831

def trusted?
  trusted_with_spam_attribute.present?
end

#try_obtain_ldap_leaseObject



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

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)


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

def two_factor_enabled?
  two_factor_otp_enabled? || two_factor_webauthn_enabled?
end

#two_factor_otp_enabled?Boolean

Returns:

  • (Boolean)


1439
1440
1441
1442
1443
1444
# File 'app/models/user.rb', line 1439

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)


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

def two_factor_webauthn_enabled?
  second_factor_webauthn_registrations.any?
end

#unique_emailObject



1501
1502
1503
1504
1505
1506
1507
1508
1509
1510
1511
1512
1513
1514
1515
1516
1517
1518
1519
1520
# File 'app/models/user.rb', line 1501

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 &&
      ::Gitlab::CurrentSettings. &&
      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



2623
2624
2625
2626
2627
# File 'app/models/user.rb', line 2623

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

  super()
end

#unset_secondary_emails_matching_deleted_email!(deleted_email) ⇒ Object



1867
1868
1869
1870
1871
1872
1873
1874
1875
1876
# File 'app/models/user.rb', line 1867

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



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

def update_invalid_gpg_signatures
  gpg_keys.each(&:update_invalid_gpg_signatures)
end

#update_otp_secret!Object



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

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



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

def update_todos_count_cache
  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



139
140
141
142
143
144
145
146
147
148
149
# File 'app/models/user.rb', line 139

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



2568
2569
2570
2571
2572
2573
2574
2575
# File 'app/models/user.rb', line 2568

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



2849
2850
2851
# File 'app/models/user.rb', line 2849

def uploads_sharding_key
  { organization_id: organization_id }
end

#user_preferenceObject

Avoid migrations only building user preference object when needed.



2685
2686
2687
# File 'app/models/user.rb', line 2685

def user_preference
  super.presence || build_user_preference
end

#user_projectObject



2797
2798
2799
2800
2801
# File 'app/models/user.rb', line 2797

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

#user_readmeObject



2803
2804
2805
2806
2807
# File 'app/models/user.rb', line 2803

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

#username_changed_hookObject



2097
2098
2099
# File 'app/models/user.rb', line 2097

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)


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

def valid_password?(password)
  return false if password.blank?
  return false unless password_allowed?(password)
  return false if password_automatically_set?
  return false unless allow_password_authentication?

  super
end

#verified_detumbled_emailsObject



2015
2016
2017
# File 'app/models/user.rb', line 2015

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

#verified_email?(check_email) ⇒ Boolean

Returns:

  • (Boolean)


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

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



2005
2006
2007
2008
2009
2010
2011
2012
2013
# File 'app/models/user.rb', line 2005

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.loaded? ? emails.select(&:confirmed?).pluck(:email) : emails.confirmed.pluck(:email)
  )
  verified_emails.uniq
end

#webhook_emailObject



2813
2814
2815
# File 'app/models/user.rb', line 2813

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)


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

def will_save_change_to_login?
  will_save_change_to_username? || will_save_change_to_email?
end