diff options
Diffstat (limited to 'app/models/user.rb')
-rw-r--r-- | app/models/user.rb | 118 |
1 files changed, 107 insertions, 11 deletions
diff --git a/app/models/user.rb b/app/models/user.rb index 40096dfa411..12f434db631 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -9,6 +9,7 @@ class User < ApplicationRecord include Gitlab::SQL::Pattern include AfterCommitQueue include Avatarable + include Awareness include Referable include Sortable include CaseSensitivity @@ -80,7 +81,7 @@ class User < ApplicationRecord serialize :otp_backup_codes, JSON # rubocop:disable Cop/ActiveRecordSerialize devise :lockable, :recoverable, :rememberable, :trackable, - :validatable, :omniauthable, :confirmable, :registerable + :validatable, :omniauthable, :confirmable, :registerable, :pbkdf2_encryptable include AdminChangedPasswordNotifier @@ -88,6 +89,7 @@ class User < ApplicationRecord # and should be added after Devise modules are initialized. include AsyncDeviseEmail include ForcedEmailConfirmation + include RequireEmailVerification MINIMUM_INACTIVE_DAYS = 90 MINIMUM_DAYS_CREATED = 7 @@ -220,6 +222,7 @@ class User < ApplicationRecord has_many :custom_attributes, class_name: 'UserCustomAttribute' has_many :callouts, class_name: 'Users::Callout' has_many :group_callouts, class_name: 'Users::GroupCallout' + has_many :namespace_callouts, class_name: 'Users::NamespaceCallout' has_many :term_agreements belongs_to :accepted_term, class_name: 'ApplicationSetting::Term' @@ -476,8 +479,8 @@ class User < ApplicationRecord end scope :order_recent_sign_in, -> { reorder(arel_table[:current_sign_in_at].desc.nulls_last) } scope :order_oldest_sign_in, -> { reorder(arel_table[:current_sign_in_at].asc.nulls_last) } - scope :order_recent_last_activity, -> { reorder(arel_table[:last_activity_on].desc.nulls_last) } - scope :order_oldest_last_activity, -> { reorder(arel_table[:last_activity_on].asc.nulls_first) } + scope :order_recent_last_activity, -> { reorder(arel_table[:last_activity_on].desc.nulls_last, arel_table[:id].asc) } + scope :order_oldest_last_activity, -> { reorder(arel_table[:last_activity_on].asc.nulls_first, arel_table[:id].desc) } scope :by_id_and_login, ->(id, login) { where(id: id).where('username = LOWER(:login) OR email = LOWER(:login)', login: login) } scope :dormant, -> { with_state(:active).human_or_service_user.where('last_activity_on <= ?', MINIMUM_INACTIVE_DAYS.day.ago.to_date) } scope :with_no_activity, -> { with_state(:active).human_or_service_user.where(last_activity_on: nil).where('created_at <= ?', MINIMUM_DAYS_CREATED.day.ago.to_date) } @@ -687,7 +690,33 @@ class User < ApplicationRecord scope = options[:with_private_emails] ? with_primary_or_secondary_email(query) : with_public_email(query) scope = scope.or(search_by_name_or_username(query, use_minimum_char_limit: options[:use_minimum_char_limit])) - scope.reorder(sanitized_order_sql, :name) + if Feature.enabled?(:use_keyset_aware_user_search_query) + 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, + distinct: false + ), + Gitlab::Pagination::Keyset::ColumnOrderDefinition.new( + attribute_name: 'users_name', + order_expression: arel_table[:name].asc, + add_to_projections: true, + nullable: :not_nullable, + distinct: false + ), + Gitlab::Pagination::Keyset::ColumnOrderDefinition.new( + attribute_name: 'users_id', + order_expression: arel_table[:id].asc, + add_to_projections: true, + nullable: :not_nullable, + distinct: true + ) + ]) + scope.reorder(order) + else + scope.reorder(sanitized_order_sql, :name) + end end # Limits the result set to users _not_ in the given query/list of IDs. @@ -894,21 +923,59 @@ class User < ApplicationRecord reset_password_sent_at.present? && reset_password_sent_at >= 1.minute.ago end - # See https://gitlab.com/gitlab-org/security/gitlab/-/issues/638 - DISALLOWED_PASSWORDS = %w[123qweQWE!@#000000000].freeze + def authenticatable_salt + return encrypted_password[0, 29] unless Feature.enabled?(:pbkdf2_password_encryption) + return super if password_strategy == :pbkdf2_sha512 + + encrypted_password[0, 29] + end # 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 def valid_password?(password) + return false unless password_allowed?(password) + return super if Feature.enabled?(:pbkdf2_password_encryption) + + Devise::Encryptor.compare(self.class, encrypted_password, password) + rescue Devise::Pbkdf2Encryptable::Encryptors::InvalidHash + validate_and_migrate_bcrypt_password(password) + rescue ::BCrypt::Errors::InvalidHash + false + end + + # This method should be removed once the :pbkdf2_password_encryption feature flag is removed. + def password=(new_password) + if Feature.enabled?(:pbkdf2_password_encryption) && Feature.enabled?(:pbkdf2_password_encryption_write, self) + super + else + # Copied from Devise DatabaseAuthenticatable. + @password = new_password + self.encrypted_password = Devise::Encryptor.digest(self.class, new_password) if new_password.present? + end + end + + def password_strategy + super + rescue Devise::Pbkdf2Encryptable::Encryptors::InvalidHash + begin + return :bcrypt if BCrypt::Password.new(encrypted_password) + rescue BCrypt::Errors::InvalidHash + :unknown + end + end + + # See https://gitlab.com/gitlab-org/security/gitlab/-/issues/638 + DISALLOWED_PASSWORDS = %w[123qweQWE!@#000000000].freeze + + def password_allowed?(password) password_allowed = true + DISALLOWED_PASSWORDS.each do |disallowed_password| password_allowed = false if Devise.secure_compare(password, disallowed_password) end - original_result = super - - password_allowed && original_result + password_allowed end def remember_me! @@ -1570,6 +1637,10 @@ class User < ApplicationRecord self.followees.exists?(user.id) end + def followed_by?(user) + self.followers.include?(user) + end + def follow(user) return false if self.id == user.id @@ -1625,7 +1696,7 @@ class User < ApplicationRecord end def oauth_authorized_tokens - Doorkeeper::AccessToken.where(resource_owner_id: id, revoked_at: nil) + OauthAccessToken.where(resource_owner_id: id, revoked_at: nil) end # Returns the projects a user contributed to in the last year. @@ -1899,7 +1970,7 @@ class User < ApplicationRecord end # override, from Devise - def lock_access! + def lock_access!(opts = {}) Gitlab::AppLogger.info("Account Locked: username=#{username}") super end @@ -2015,6 +2086,13 @@ class User < ApplicationRecord callout_dismissed?(callout, ignore_dismissal_earlier_than) end + def dismissed_callout_for_namespace?(feature_name:, namespace:, ignore_dismissal_earlier_than: nil) + source_feature_name = "#{feature_name}_#{namespace.id}" + callout = namespace_callouts_by_feature_name[source_feature_name] + + callout_dismissed?(callout, ignore_dismissal_earlier_than) + end + # Load the current highest access by looking directly at the user's memberships def current_highest_access_level members.non_request.maximum(:access_level) @@ -2041,6 +2119,11 @@ class User < ApplicationRecord .find_or_initialize_by(feature_name: ::Users::GroupCallout.feature_names[feature_name], group_id: group_id) end + def find_or_initialize_namespace_callout(feature_name, namespace_id) + namespace_callouts + .find_or_initialize_by(feature_name: ::Users::NamespaceCallout.feature_names[feature_name], namespace_id: namespace_id) + end + def can_trigger_notifications? confirmed? && !blocked? && !ghost? end @@ -2158,6 +2241,10 @@ class User < ApplicationRecord @group_callouts_by_feature_name ||= group_callouts.index_by(&:source_feature_name) end + def namespace_callouts_by_feature_name + @namespace_callouts_by_feature_name ||= namespace_callouts.index_by(&:source_feature_name) + end + def authorized_groups_without_shared_membership Group.from_union([ groups.select(*Namespace.cached_column_list), @@ -2318,6 +2405,15 @@ class User < ApplicationRecord Ci::NamespaceMirror.contains_traversal_ids(traversal_ids) end + + def validate_and_migrate_bcrypt_password(password) + return false unless Devise::Encryptor.compare(self.class, encrypted_password, password) + return true unless Feature.enabled?(:pbkdf2_password_encryption_write, self) + + update_attribute(:password, password) + rescue ::BCrypt::Errors::InvalidHash + false + end end User.prepend_mod_with('User') |