user.rb 38.9 KB
Newer Older
1 2
require 'carrierwave/orm/activerecord'

gitlabhq's avatar
gitlabhq committed
3
class User < ActiveRecord::Base
4
  extend Gitlab::ConfigHelper
5
  extend Gitlab::CurrentSettings
6 7

  include Gitlab::ConfigHelper
8
  include Gitlab::CurrentSettings
9
  include Gitlab::SQL::Pattern
10
  include Avatarable
11 12
  include Referable
  include Sortable
13
  include CaseSensitivity
14
  include TokenAuthenticatable
15
  include IgnorableColumn
16
  include FeatureGate
17
  include CreatedAtFilterable
18
  include IgnorableColumn
19

20 21
  DEFAULT_NOTIFICATION_LEVEL = :participating

22 23
  ignore_column :external_email
  ignore_column :email_provider
24
  ignore_column :authentication_token
25

26
  add_authentication_token_field :incoming_email_token
27
  add_authentication_token_field :rss_token
28

29
  default_value_for :admin, false
30
  default_value_for(:external) { current_application_settings.user_default_external }
31
  default_value_for :can_create_group, gitlab_config.default_can_create_group
32 33
  default_value_for :can_create_team, false
  default_value_for :hide_no_ssh_key, false
34
  default_value_for :hide_no_password, false
35
  default_value_for :project_view, :files
36
  default_value_for :notified_of_own_activity, false
37
  default_value_for :preferred_language, I18n.default_locale
38
  default_value_for :theme_id, gitlab_config.default_theme
39

40
  attr_encrypted :otp_secret,
41
    key:       Gitlab::Application.secrets.otp_key_base,
42
    mode:      :per_attribute_iv_and_salt,
43
    insecure_mode: true,
44 45
    algorithm: 'aes-256-cbc'

46
  devise :two_factor_authenticatable,
47
         otp_secret_encryption_key: Gitlab::Application.secrets.otp_key_base
48

49
  devise :two_factor_backupable, otp_number_of_backup_codes: 10
50
  serialize :otp_backup_codes, JSON # rubocop:disable Cop/ActiveRecordSerialize
51

52
  devise :lockable, :recoverable, :rememberable, :trackable,
53
    :validatable, :omniauthable, :confirmable, :registerable
gitlabhq's avatar
gitlabhq committed
54

55 56
  # Override Devise::Models::Trackable#update_tracked_fields!
  # to limit database writes to at most once every hour
57
  def update_tracked_fields!(request)
58 59
    update_tracked_fields(request)

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

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

66
  attr_accessor :force_random_password
gitlabhq's avatar
gitlabhq committed
67

68 69 70
  # Virtual attribute for authenticating by either username or email
  attr_accessor :login

71 72 73 74
  #
  # Relations
  #

75
  # Namespace for personal projects
76
  has_one :namespace, -> { where(type: nil) }, dependent: :destroy, foreign_key: :owner_id, autosave: true # rubocop:disable Cop/ActiveRecordDependent
77 78

  # Profile
79 80 81
  has_many :keys, -> do
    type = Key.arel_table[:type]
    where(type.not_eq('DeployKey').or(type.eq(nil)))
82 83
  end, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
  has_many :deploy_keys, -> { where(type: 'DeployKey') }, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
84
  has_many :gpg_keys
85

86 87 88 89 90
  has_many :emails, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
  has_many :personal_access_tokens, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
  has_many :identities, dependent: :destroy, autosave: true # rubocop:disable Cop/ActiveRecordDependent
  has_many :u2f_registrations, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
  has_many :chat_names, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
91
  has_one :user_synced_attributes_metadata, autosave: true
92 93

  # Groups
94 95
  has_many :members, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
  has_many :group_members, -> { where(requested_at: nil) }, dependent: :destroy, source: 'GroupMember' # rubocop:disable Cop/ActiveRecordDependent
96
  has_many :groups, through: :group_members
97 98
  has_many :owned_groups, -> { where members: { access_level: Gitlab::Access::OWNER } }, through: :group_members, source: :group
  has_many :masters_groups, -> { where members: { access_level: Gitlab::Access::MASTER } }, through: :group_members, source: :group
99

100
  # Projects
101 102
  has_many :groups_projects,          through: :groups, source: :projects
  has_many :personal_projects,        through: :namespace, source: :projects
103
  has_many :project_members, -> { where(requested_at: nil) }, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
104
  has_many :projects,                 through: :project_members
105
  has_many :created_projects,         foreign_key: :creator_id, class_name: 'Project'
106
  has_many :users_star_projects, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
Ciro Santilli's avatar
Ciro Santilli committed
107
  has_many :starred_projects, through: :users_star_projects, source: :project
108
  has_many :project_authorizations
109
  has_many :authorized_projects, through: :project_authorizations, source: :project
110

111 112 113 114 115 116
  has_many :snippets,                 dependent: :destroy, foreign_key: :author_id # rubocop:disable Cop/ActiveRecordDependent
  has_many :notes,                    dependent: :destroy, foreign_key: :author_id # rubocop:disable Cop/ActiveRecordDependent
  has_many :issues,                   dependent: :destroy, foreign_key: :author_id # rubocop:disable Cop/ActiveRecordDependent
  has_many :merge_requests,           dependent: :destroy, foreign_key: :author_id # rubocop:disable Cop/ActiveRecordDependent
  has_many :events,                   dependent: :destroy, foreign_key: :author_id # rubocop:disable Cop/ActiveRecordDependent
  has_many :subscriptions,            dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
117
  has_many :recent_events, -> { order "id DESC" }, foreign_key: :author_id,   class_name: "Event"
118 119 120 121 122 123 124 125 126 127
  has_many :oauth_applications, class_name: 'Doorkeeper::Application', as: :owner, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
  has_one  :abuse_report,             dependent: :destroy, foreign_key: :user_id # rubocop:disable Cop/ActiveRecordDependent
  has_many :reported_abuse_reports,   dependent: :destroy, foreign_key: :reporter_id, class_name: "AbuseReport" # rubocop:disable Cop/ActiveRecordDependent
  has_many :spam_logs,                dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
  has_many :builds,                   dependent: :nullify, class_name: 'Ci::Build' # rubocop:disable Cop/ActiveRecordDependent
  has_many :pipelines,                dependent: :nullify, class_name: 'Ci::Pipeline' # rubocop:disable Cop/ActiveRecordDependent
  has_many :todos,                    dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
  has_many :notification_settings,    dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
  has_many :award_emoji,              dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
  has_many :triggers,                 dependent: :destroy, class_name: 'Ci::Trigger', foreign_key: :owner_id # rubocop:disable Cop/ActiveRecordDependent
128

129 130
  has_many :issue_assignees
  has_many :assigned_issues, class_name: "Issue", through: :issue_assignees, source: :issue
131
  has_many :assigned_merge_requests,  dependent: :nullify, foreign_key: :assignee_id, class_name: "MergeRequest" # rubocop:disable Cop/ActiveRecordDependent
132

133 134
  has_many :custom_attributes, class_name: 'UserCustomAttribute'

135 136 137
  #
  # Validations
  #
138
  # Note: devise :validatable above adds validations for :email and :password
Cyril's avatar
Cyril committed
139
  validates :name, presence: true
Douwe Maan's avatar
Douwe Maan committed
140
  validates :email, confirmation: true
141 142
  validates :notification_email, presence: true
  validates :notification_email, email: true, if: ->(user) { user.notification_email != user.email }
143
  validates :public_email, presence: true, uniqueness: true, email: true, allow_blank: true
144
  validates :bio, length: { maximum: 255 }, allow_blank: true
145 146 147
  validates :projects_limit,
    presence: true,
    numericality: { greater_than_or_equal_to: 0, less_than_or_equal_to: Gitlab::Database::MAX_INT_VALUE }
148
  validates :username,
149
    dynamic_path: true,
150
    presence: true,
151
    uniqueness: { case_sensitive: false }
152

153
  validate :namespace_uniq, if: :username_changed?
154 155
  validate :namespace_move_dir_allowed, if: :username_changed?

156
  validate :avatar_type, if: ->(user) { user.avatar.present? && user.avatar_changed? }
157 158 159
  validate :unique_email, if: :email_changed?
  validate :owns_notification_email, if: :notification_email_changed?
  validate :owns_public_email, if: :public_email_changed?
160
  validate :signup_domain_valid?, on: :create, if: ->(user) { !user.created_by_id }
161
  validates :avatar, file_size: { maximum: 200.kilobytes.to_i }
162

163
  before_validation :sanitize_attrs
164 165
  before_validation :set_notification_email, if: :email_changed?
  before_validation :set_public_email, if: :public_email_changed?
Douwe Maan's avatar
Douwe Maan committed
166
  before_save :ensure_incoming_email_token
167
  before_save :ensure_user_rights_and_limits, if: ->(user) { user.new_record? || user.external_changed? }
168
  before_save :skip_reconfirmation!, if: ->(user) { user.email_changed? && user.read_only_attribute?(:email) }
169
  before_save :check_for_verified_email, if: ->(user) { user.email_changed? && !user.new_record? }
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
170
  after_save :ensure_namespace_correct
171
  after_update :username_changed_hook, if: :username_changed?
172 173
  after_destroy :post_destroy_hook
  after_commit :update_emails_with_primary_email, on: :update, if: -> { previous_changes.key?('email') }
174
  after_commit :update_invalid_gpg_signatures, on: :update, if: -> { previous_changes.key?('email') }
175

176
  after_initialize :set_projects_limit
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
177

178
  # User's Layout preference
179
  enum layout: [:fixed, :fluid]
180

181 182
  # User's Dashboard preference
  # Note: When adding an option, it MUST go on the end of the array.
183
  enum dashboard: [:projects, :stars, :project_activity, :starred_project_activity, :groups, :todos]
184

185
  # User's Project preference
186 187 188
  # Note: When adding an option, it MUST go on the end of the array.
  enum project_view: [:readme, :activity, :files]

189
  delegate :path, to: :namespace, allow_nil: true, prefix: true
190

191 192 193
  state_machine :state, initial: :active do
    event :block do
      transition active: :blocked
194
      transition ldap_blocked: :blocked
195 196
    end

197 198 199 200
    event :ldap_block do
      transition active: :ldap_blocked
    end

201 202
    event :activate do
      transition blocked: :active
203
      transition ldap_blocked: :active
204
    end
205 206 207 208 209

    state :blocked, :ldap_blocked do
      def blocked?
        true
      end
210 211 212 213 214 215 216 217 218

      def active_for_authentication?
        false
      end

      def inactive_message
        "Your account has been blocked. Please contact your GitLab " \
          "administrator if you think this is an error."
      end
219
    end
220 221
  end

222
  mount_uploader :avatar, AvatarUploader
223
  has_many :uploads, as: :model, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
224

Andrey Kumanyaev's avatar
Andrey Kumanyaev committed
225
  # Scopes
226
  scope :admins, -> { where(admin: true) }
227
  scope :blocked, -> { with_states(:blocked, :ldap_blocked) }
228
  scope :external, -> { where(external: true) }
James Lopez's avatar
James Lopez committed
229
  scope :active, -> { with_state(:active).non_internal }
230
  scope :without_projects, -> { where('id NOT IN (SELECT DISTINCT(user_id) FROM members WHERE user_id IS NOT NULL AND requested_at IS NULL)') }
231
  scope :todo_authors, ->(user_id, state) { where(id: Todo.where(user_id: user_id, state: state).select(:author_id)) }
232 233
  scope :order_recent_sign_in, -> { reorder(Gitlab::Database.nulls_last_order('last_sign_in_at', 'DESC')) }
  scope :order_oldest_sign_in, -> { reorder(Gitlab::Database.nulls_last_order('last_sign_in_at', 'ASC')) }
234 235

  def self.with_two_factor
236 237
    joins("LEFT OUTER JOIN u2f_registrations AS u2f ON u2f.user_id = users.id")
      .where("u2f.id IS NOT NULL OR otp_required_for_login = ?", true).distinct(arel_table[:id])
238 239 240
  end

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

245 246 247
  #
  # Class methods
  #
Andrey Kumanyaev's avatar
Andrey Kumanyaev committed
248
  class << self
249
    # Devise method overridden to allow sign in with email or username
250 251 252
    def find_for_database_authentication(warden_conditions)
      conditions = warden_conditions.dup
      if login = conditions.delete(:login)
Gabriel Mazetto's avatar
Gabriel Mazetto committed
253
        where(conditions).find_by("lower(username) = :value OR lower(email) = :value", value: login.downcase)
254
      else
Gabriel Mazetto's avatar
Gabriel Mazetto committed
255
        find_by(conditions)
256 257
      end
    end
258

Valery Sizov's avatar
Valery Sizov committed
259
    def sort(method)
260 261 262
      order_method = method || 'id_desc'

      case order_method.to_s
263 264
      when 'recent_sign_in' then order_recent_sign_in
      when 'oldest_sign_in' then order_oldest_sign_in
265
      else
266
        order_by(order_method)
Valery Sizov's avatar
Valery Sizov committed
267 268 269
      end
    end

270 271
    # Find a User by their primary email or any associated secondary email
    def find_by_any_email(email)
272 273 274 275 276 277 278
      sql = 'SELECT *
      FROM users
      WHERE id IN (
        SELECT id FROM users WHERE email = :email
        UNION
        SELECT emails.user_id FROM emails WHERE email = :email
      )
279 280 281
      LIMIT 1;'

      User.find_by_sql([sql, { email: email }]).first
282
    end
283

284
    def filter(filter_name)
Andrey Kumanyaev's avatar
Andrey Kumanyaev committed
285
      case filter_name
286
      when 'admins'
287
        admins
288
      when 'blocked'
289
        blocked
290
      when 'two_factor_disabled'
291
        without_two_factor
292
      when 'two_factor_enabled'
293
        with_two_factor
294
      when 'wop'
295
        without_projects
296
      when 'external'
297
        external
Andrey Kumanyaev's avatar
Andrey Kumanyaev committed
298
      else
299
        active
Andrey Kumanyaev's avatar
Andrey Kumanyaev committed
300
      end
301 302
    end

303 304 305 306 307 308 309
    # Searches users matching the given query.
    #
    # This method uses ILIKE on PostgreSQL and LIKE on MySQL.
    #
    # query - The search query as a String
    #
    # Returns an ActiveRecord::Relation.
310
    def search(query)
311
      table   = arel_table
312
      pattern = User.to_pattern(query)
313

314 315 316 317 318 319 320 321 322
      order = <<~SQL
        CASE
          WHEN users.name = %{query} THEN 0
          WHEN users.username = %{query} THEN 1
          WHEN users.email = %{query} THEN 2
          ELSE 3
        END
      SQL

323
      where(
324 325 326
        table[:name].matches(pattern)
          .or(table[:email].matches(pattern))
          .or(table[:username].matches(pattern))
327
      ).reorder(order % { query: ActiveRecord::Base.connection.quote(query) }, :name)
Andrey Kumanyaev's avatar
Andrey Kumanyaev committed
328
    end
329

330 331 332 333 334 335 336 337 338 339 340
    # searches user by given pattern
    # it compares name, email, username fields and user's secondary emails with given pattern
    # This method uses ILIKE on PostgreSQL and LIKE on MySQL.

    def search_with_secondary_emails(query)
      table = arel_table
      email_table = Email.arel_table
      pattern = "%#{query}%"
      matched_by_emails_user_ids = email_table.project(email_table[:user_id]).where(email_table[:email].matches(pattern))

      where(
341 342 343 344
        table[:name].matches(pattern)
          .or(table[:email].matches(pattern))
          .or(table[:username].matches(pattern))
          .or(table[:id].in(matched_by_emails_user_ids))
345 346 347
      )
    end

348
    def by_login(login)
349 350 351 352 353 354 355
      return nil unless login

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

358 359 360 361
    def find_by_username(username)
      iwhere(username: username).take
    end

362
    def find_by_username!(username)
363
      iwhere(username: username).take!
364 365
    end

366
    def find_by_personal_access_token(token_string)
367 368
      return unless token_string

369
      PersonalAccessTokensFinder.new(state: 'active').find_by(token: token_string)&.user
370 371
    end

372 373
    # Returns a user for the given SSH key.
    def find_by_ssh_key_id(key_id)
374
      Key.find_by(id: key_id)&.user
375 376
    end

377
    def find_by_full_path(path, follow_redirects: false)
378 379
      namespace = Namespace.for_user.find_by_full_path(path, follow_redirects: follow_redirects)
      namespace&.owner
380 381
    end

382 383 384
    def reference_prefix
      '@'
    end
385 386 387 388

    # Pattern used to extract `@user` user references from text
    def reference_pattern
      %r{
389
        (?<!\w)
390
        #{Regexp.escape(reference_prefix)}
391
        (?<user>#{Gitlab::PathRegex::FULL_NAMESPACE_FORMAT_REGEX})
392 393
      }x
    end
394 395 396 397

    # Return (create if necessary) the ghost user. The ghost user
    # owns records previously belonging to deleted users.
    def ghost
398 399
      email = 'ghost%s@example.com'
      unique_internal(where(ghost: true), 'ghost', email) do |u|
400 401
        u.bio = 'This is a "Ghost User", created to hold all issues authored by users that have since been deleted. This user cannot be removed.'
        u.name = 'Ghost User'
402
        u.notification_email = email
403
      end
404
    end
vsizov's avatar
vsizov committed
405
  end
randx's avatar
randx committed
406

Michael Kozono's avatar
Michael Kozono committed
407 408 409 410
  def full_path
    username
  end

411 412 413 414
  def self.internal_attributes
    [:ghost]
  end

415
  def internal?
416 417 418 419 420 421 422 423
    self.class.internal_attributes.any? { |a| self[a] }
  end

  def self.internal
    where(Hash[internal_attributes.zip([true] * internal_attributes.size)])
  end

  def self.non_internal
424
    where(Hash[internal_attributes.zip([[false, nil]] * internal_attributes.size)])
425 426
  end

427 428 429
  #
  # Instance methods
  #
430 431 432 433 434

  def to_param
    username
  end

435
  def to_reference(_from_project = nil, target_project: nil, full: nil)
436 437 438
    "#{self.class.reference_prefix}#{username}"
  end

439 440
  def skip_confirmation=(bool)
    skip_confirmation! if bool
randx's avatar
randx committed
441
  end
442

443
  def generate_reset_token
444
    @reset_token, enc = Devise.token_generator.generate(self.class, :reset_password_token)
445 446 447 448

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

449
    @reset_token
450 451
  end

452 453 454 455
  def recently_sent_password_reset?
    reset_password_sent_at.present? && reset_password_sent_at >= 1.minute.ago
  end

456 457 458 459 460 461 462 463
  def remember_me!
    super if ::Gitlab::Database.read_write?
  end

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

464
  def disable_two_factor!
465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482
    transaction do
      update_attributes(
        otp_required_for_login:      false,
        encrypted_otp_secret:        nil,
        encrypted_otp_secret_iv:     nil,
        encrypted_otp_secret_salt:   nil,
        otp_grace_period_started_at: nil,
        otp_backup_codes:            nil
      )
      self.u2f_registrations.destroy_all
    end
  end

  def two_factor_enabled?
    two_factor_otp_enabled? || two_factor_u2f_enabled?
  end

  def two_factor_otp_enabled?
483
    otp_required_for_login?
484 485 486
  end

  def two_factor_u2f_enabled?
487
    u2f_registrations.exists?
488 489
  end

490
  def namespace_uniq
491
    # Return early if username already failed the first uniqueness validation
492
    return if errors.key?(:username) &&
493
        errors[:username].include?('has already been taken')
494

495 496 497
    existing_namespace = Namespace.by_path(username)
    if existing_namespace && existing_namespace != namespace
      errors.add(:username, 'has already been taken')
498 499
    end
  end
500

501 502 503 504 505 506
  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

507
  def avatar_type
508 509
    unless avatar.image?
      errors.add :avatar, "only images allowed"
510 511 512
    end
  end

513
  def unique_email
514 515
    if !emails.exists?(email: email) && Email.exists?(email: email)
      errors.add(:email, 'has already been taken')
516
    end
517 518
  end

519
  def owns_notification_email
520
    return if temp_oauth_email?
521

522
    errors.add(:notification_email, "is not an email you own") unless all_emails.include?(notification_email)
523 524
  end

525
  def owns_public_email
526
    return if public_email.blank?
527

528
    errors.add(:public_email, "is not an email you own") unless all_emails.include?(public_email)
529 530
  end

531 532 533 534 535
  # see if the new email is already a verified secondary email
  def check_for_verified_email
    skip_reconfirmation! if emails.confirmed.where(email: self.email).any?
  end

536
  # Note: the use of the Emails services will cause `saves` on the user object, running
537
  # through the callbacks again and can have side effects, such as the `previous_changes`
538 539 540
  # hash and `_was` variables getting munged.
  # By using an `after_commit` instead of `after_update`, we avoid the recursive callback
  # scenario, though it then requires us to use the `previous_changes` hash
541
  def update_emails_with_primary_email
542
    previous_email = previous_changes[:email][0]  # grab this before the DestroyService is called
543
    primary_email_record = emails.find_by(email: email)
544
    Emails::DestroyService.new(self, user: self).execute(primary_email_record) if primary_email_record
545

546 547
    # the original primary email was confirmed, and we want that to carry over.  We don't
    # have access to the original confirmation values at this point, so just set confirmed_at
548
    Emails::CreateService.new(self, user: self, email: previous_email).execute(confirmed_at: confirmed_at)
549 550
  end

551
  def update_invalid_gpg_signatures
552
    gpg_keys.each(&:update_invalid_gpg_signatures)
553 554
  end

555 556
  # Returns the groups a user has access to
  def authorized_groups
557 558
    union = Gitlab::SQL::Union
      .new([groups.select(:id), authorized_projects.select(:namespace_id)])
559

560
    Group.where("namespaces.id IN (#{union.to_sql})") # rubocop:disable GitlabSecurity/SqlInjection
561 562
  end

563 564
  # Returns a relation of groups the user has access to, including their parent
  # and child groups (recursively).
565
  def all_expanded_groups
566
    Gitlab::GroupHierarchy.new(groups).all_groups
567 568 569 570 571 572
  end

  def expanded_groups_requiring_two_factor_authentication
    all_expanded_groups.where(require_two_factor_authentication: true)
  end

573
  def refresh_authorized_projects
574 575 576 577
    Users::RefreshAuthorizedProjectsService.new(self).execute
  end

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

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

    if min_access_level
587 588
      projects = projects
        .where('project_authorizations.access_level >= ?', min_access_level)
589
    end
590 591 592 593 594 595

    projects
  end

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

598 599 600 601 602 603 604 605 606 607
  # Returns the projects this user has reporter (or greater) access to, limited
  # to at most the given projects.
  #
  # This method is useful when you have a list of projects and want to
  # efficiently check to which of these projects the user has at least reporter
  # access.
  def projects_with_reporter_access_limited_to(projects)
    authorized_projects(Gitlab::Access::REPORTER).where(id: projects)
  end

608
  def owned_projects
609
    @owned_projects ||=
610 611
      Project.where('namespace_id IN (?) OR namespace_id = ?',
                    owned_groups.select(:id), namespace.id).joins(:namespace)
612 613
  end

614 615 616 617
  # Returns projects which user can admin issues on (for example to move an issue to that project).
  #
  # This logic is duplicated from `Ability#project_abilities` into a SQL form.
  def projects_where_can_admin_issues
618
    authorized_projects(Gitlab::Access::REPORTER).non_archived.with_issues_enabled
619 620
  end

Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
621
  def require_ssh_key?
622
    keys.count == 0 && Gitlab::ProtocolAccess.allowed?('ssh')
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
623 624
  end

625 626
  def require_password_creation?
    password_automatically_set? && allow_password_authentication?
627 628
  end

629
  def require_personal_access_token_creation_for_git_auth?
630
    return false if current_application_settings.password_authentication_enabled? || ldap_user?
631 632

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

635 636 637 638
  def allow_password_authentication?
    !ldap_user? && current_application_settings.password_authentication_enabled?
  end

639
  def can_change_username?
640
    gitlab_config.username_changing_enabled
641 642
  end

Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
643
  def can_create_project?
644
    projects_limit_left > 0
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
645 646 647
  end

  def can_create_group?
648
    can?(:create_group)
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
649 650
  end

651 652 653 654
  def can_select_namespace?
    several_namespaces? || admin
  end

655
  def can?(action, subject = :global)
656
    Ability.allowed?(self, action, subject)
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
657 658
  end

659 660 661 662
  def confirm_deletion_with_password?
    !password_automatically_set? && allow_password_authentication?
  end

Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
663 664 665 666
  def first_name
    name.split.first unless name.blank?
  end

667
  def projects_limit_left
668 669 670 671 672
    projects_limit - personal_projects_count
  end

  def personal_projects_count
    @personal_projects_count ||= personal_projects.count
673 674
  end

675 676
  def recent_push(project = nil)
    service = Users::LastPushEventService.new(self)
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
677

678 679 680 681
    if project
      service.last_event_for_project(project)
    else
      service.last_event_for_user
682
    end
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
683 684 685
  end

  def several_namespaces?
686
    owned_groups.any? || masters_groups.any?
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
687 688 689 690 691
  end

  def namespace_id
    namespace.try :id
  end
692

693 694 695
  def name_with_username
    "#{name} (#{username})"
  end
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
696

697
  def already_forked?(project)
698 699 700
    !!fork_of(project)
  end

701
  def fork_of(project)
702
    namespace.find_fork_of(project)
703
  end
704 705

  def ldap_user?
706 707 708 709 710
    if identities.loaded?
      identities.find { |identity| identity.provider.start_with?('ldap') && !identity.extern_uid.nil? }
    else
      identities.exists?(["provider LIKE ? AND extern_uid IS NOT NULL", "ldap%"])
    end
711 712 713 714
  end

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

717
  def project_deploy_keys
718
    DeployKey.unscoped.in_projects(authorized_projects.pluck(:id)).distinct(:id)
719 720
  end

721
  def accessible_deploy_keys
722 723 724 725 726
    @accessible_deploy_keys ||= begin
      key_ids = project_deploy_keys.pluck(:id)
      key_ids.push(*DeployKey.are_public.pluck(:id))
      DeployKey.where(id: key_ids)
    end
727
  end
728 729

  def created_by
skv's avatar
skv committed
730
    User.find_by(id: created_by_id) if created_by_id
731
  end
732 733

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

740
  def set_notification_email
741 742
    if notification_email.blank? || !all_emails.include?(notification_email)
      self.notification_email = email
743 744 745
    end
  end

746
  def set_public_email
747
    if public_email.blank? || !all_emails.include?(public_email)
748
      self.public_email = ''
749 750 751
    end
  end

752
  def update_secondary_emails!
753 754 755
    set_notification_email
    set_public_email
    save if notification_email_changed? || public_email_changed?
756 757
  end

758
  def set_projects_limit
759 760 761
    # `User.select(:id)` raises
    # `ActiveModel::MissingAttributeError: missing attribute: projects_limit`
    # without this safeguard!
762
    return unless has_attribute?(:projects_limit)
763

764
    connection_default_value_defined = new_record? && !projects_limit_changed?
765
    return unless projects_limit.nil? || connection_default_value_defined
766 767 768 769

    self.projects_limit = current_application_settings.default_projects_limit
  end

770
  def requires_ldap_check?
771 772 773
    if !Gitlab.config.ldap.enabled
      false
    elsif ldap_user?
774 775 776 777 778 779
      !last_credential_check_at || (last_credential_check_at + 1.hour) < Time.now
    else
      false
    end
  end

Jacob Vosmaer's avatar
Jacob Vosmaer committed
780 781 782 783 784 785 786
  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

787 788 789 790 791
  def solo_owned_groups
    @solo_owned_groups ||= owned_groups.select do |group|
      group.owners == [self]
    end
  end
792 793

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

    self
799
  end
800

801 802 803 804
  def can_leave_project?(project)
    project.namespace != namespace &&
      project.project_member(self)
  end
805

Jerome Dalbert's avatar
Jerome Dalbert committed
806
  def full_website_url
807
    return "http://#{website_url}" if website_url !~ /\Ahttps?:\/\//
Jerome Dalbert's avatar
Jerome Dalbert committed
808 809 810 811 812

    website_url
  end

  def short_website_url
813
    website_url.sub(/\Ahttps?:\/\//, '')
Jerome Dalbert's avatar
Jerome Dalbert committed
814
  end
GitLab's avatar
GitLab committed
815

816
  def all_ssh_keys
817
    keys.map(&:publishable_key)
818
  end
819 820

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

824 825 826
  def avatar_url(size: nil, scale: 2, **args)
    # We use avatar_path instead of overriding avatar_url because of carrierwave.
    # See https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/11001/diffs#note_28659864
827
    avatar_path(args) || GravatarService.new.execute(email, size, scale, username: username)
828
  end
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
829

830 831 832 833
  def primary_email_verified?
    confirmed? && !temp_oauth_email?
  end

834
  def all_emails
835
    all_emails = []
836 837
    all_emails << email unless temp_oauth_email?
    all_emails.concat(emails.map(&:email))
838
    all_emails
839 840
  end

841
  def verified_emails
842
    verified_emails = []
843
    verified_emails << email if primary_email_verified?
844
    verified_emails.concat(emails.confirmed.pluck(:email))
845 846 847
    verified_emails
  end

848
  def verified_email?(check_email)
849
    downcased = check_email.downcase
850
    email == downcased ? primary_email_verified? : emails.confirmed.where(email: downcased).exists?
851 852
  end

Kirill Zaitsev's avatar
Kirill Zaitsev committed
853 854 855 856
  def hook_attrs
    {
      name: name,
      username: username,
857
      avatar_url: avatar_url(only_path: false)
Kirill Zaitsev's avatar
Kirill Zaitsev committed
858 859 860
    }
  end

Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
861 862
  def ensure_namespace_correct
    # Ensure user has namespace
863
    create_namespace!(path: username, name: username) unless namespace
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
864

865
    if username_changed?
866 867 868 869 870 871
      unless namespace.update_attributes(path: username, name: username)
        namespace.errors.each do |attribute, message|
          self.errors.add(:"namespace_#{attribute}", message)
        end
        raise ActiveRecord::RecordInvalid.new(namespace)
      end
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
872 873 874
    end
  end

875 876 877 878
  def username_changed_hook
    system_hook_service.execute_hooks_for(self, :rename)
  end

Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
879
  def post_destroy_hook
880
    log_info("User \"#{name}\" (#{email})  was removed")
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
881 882 883
    system_hook_service.execute_hooks_for(self, :destroy)
  end

884 885 886 887 888
  def delete_async(deleted_by:, params: {})
    block if params[:hard_delete]
    DeleteUserWorker.perform_async(deleted_by.id, id, params)
  end

Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
889
  def notification_service
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
890 891 892
    NotificationService.new
  end

893
  def log_info(message)
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
894 895 896 897 898 899
    Gitlab::AppLogger.info message
  end

  def system_hook_service
    SystemHooksService.new
  end
Ciro Santilli's avatar
Ciro Santilli committed
900 901

  def starred?(project)
902
    starred_projects.exists?(project.id)
Ciro Santilli's avatar
Ciro Santilli committed
903 904 905
  end

  def toggle_star(project)
906
    UsersStarProject.transaction do
907 908
      user_star_project = users_star_projects
          .where(project: project, user: self).lock(true).first
909 910 911 912 913 914

      if user_star_project
        user_star_project.destroy
      else
        UsersStarProject.create!(project: project, user: self)
      end
Ciro Santilli's avatar
Ciro Santilli committed
915 916
    end
  end
917 918

  def manageable_namespaces
919
    @manageable_namespaces ||= [namespace] + owned_groups + masters_groups
920
  end
921

922 923 924 925 926 927
  def namespaces
    namespace_ids = groups.pluck(:id)
    namespace_ids.push(namespace.id)
    Namespace.where(id: namespace_ids)
  end

928
  def oauth_authorized_tokens
929
    Doorkeeper::AccessToken.where(resource_owner_id: id, revoked_at: nil)
930
  end
931

932 933 934 935 936 937 938 939 940
  # 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
941
  # ms on a database with a similar size to GitLab.com's database. On the other
942 943
  # hand, using a subquery means we can get the exact same data in about 40 ms.
  def contributed_projects
944 945 946 947 948
    events = Event.select(:project_id)
      .contributions.where(author_id: self)
      .where("created_at > ?", Time.now - 1.year)
      .uniq
      .reorder(nil)
949 950

    Project.where(id: events)
951
  end
952

953 954 955
  def can_be_removed?
    !solo_owned_groups.present?
  end
956 957

  def ci_authorized_runners
958
    @ci_authorized_runners ||= begin
959
      runner_ids = Ci::RunnerProject
960
        .where("ci_runner_projects.project_id IN (#{ci_projects_union.to_sql})") # rubocop:disable GitlabSecurity/SqlInjection
961
        .select(:runner_id)
962 963
      Ci::Runner.specific.where(id: runner_ids)
    end
964
  end
965

966 967 968 969
  def notification_settings_for(source)
    notification_settings.find_or_initialize_by(source: source)
  end

970 971 972
  # Lazy load global notification setting
  # Initializes User setting with Participating level if setting not persisted
  def global_notification_setting
973 974 975 976 977 978
    return @global_notification_setting if defined?(@global_notification_setting)

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

    @global_notification_setting
979 980
  end

981
  def assigned_open_merge_requests_count(force: false)
982
    Rails.cache.fetch(['users', id, 'assigned_open_merge_requests_count'], force: force, expires_in: 20.minutes) do
983
      MergeRequestsFinder.new(self, assignee_id: self.id, state: 'opened').execute.count
984 985 986
    end
  end

987
  def assigned_open_issues_count(force: false)
988
    Rails.cache.fetch(['users', id, 'assigned_open_issues_count'], force: force, expires_in: 20.minutes) do
989
      IssuesFinder.new(self, assignee_id: self.id, state: 'opened').execute.count
990
    end
991 992
  end

993
  def update_cache_counts
994
    assigned_open_merge_requests_count(force: true)
995 996 997
    assigned_open_issues_count(force: true)
  end

998
  def invalidate_cache_counts
999 1000 1001 1002 1003
    invalidate_issue_cache_counts
    invalidate_merge_request_cache_counts
  end

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

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

1011 1012
  def todos_done_count(force: false)
    Rails.cache.fetch(['users', id, 'todos_done_count'], force: force) do
1013
      TodosFinder.new(self, state: :done).execute.count
1014 1015 1016 1017 1018
    end
  end

  def todos_pending_count(force: false)
    Rails.cache.fetch(['users', id, 'todos_pending_count'], force: force) do
1019
      TodosFinder.new(self, state: :pending).execute.count
1020 1021 1022 1023 1024 1025 1026 1027
    end
  end

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

1028 1029 1030 1031 1032 1033 1034 1035 1036 1037 1038 1039
  # 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.0.0/lib/devise/models/lockable.rb#L92>
  #
  def increment_failed_attempts!
    self.failed_attempts ||= 0
    self.failed_attempts += 1
    if attempts_exceeded?
      lock_access! unless access_locked?
    else
1040
      Users::UpdateService.new(self, user: self).execute(validate: false)
1041 1042 1043
    end
  end

1044 1045 1046 1047 1048 1049 1050 1051 1052
  def access_level
    if admin?
      :admin
    else
      :regular
    end
  end

  def access_level=(new_level)
Douwe Maan's avatar
Douwe Maan committed
1053 1054
    new_level = new_level.to_s
    return unless %w(admin regular).include?(new_level)
1055

Douwe Maan's avatar
Douwe Maan committed
1056 1057
    self.admin = (new_level == 'admin')
  end
1058

1059 1060 1061 1062 1063 1064
  # Does the user have access to all private groups & projects?
  # Overridden in EE to also check auditor?
  def full_private_access?
    admin?
  end

1065
  def update_two_factor_requirement
1066
    periods = expanded_groups_requiring_two_factor_authentication.pluck(:two_factor_grace_period)
1067

1068
    self.require_two_factor_authentication_from_group = periods.any?
1069 1070 1071 1072 1073
    self.two_factor_grace_period = periods.min || User.column_defaults['two_factor_grace_period']

    save
  end

Alexis Reigel's avatar
Alexis Reigel committed
1074 1075 1076 1077 1078 1079 1080
  # each existing user needs to have an `rss_token`.
  # we do this on read since migrating all existing users is not a feasible
  # solution.
  def rss_token
    ensure_rss_token!
  end

1081 1082 1083 1084 1085 1086 1087 1088 1089 1090 1091 1092 1093 1094 1095 1096
  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

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

Brian Neel's avatar
Brian Neel committed
1097 1098
  # override, from Devise
  def lock_access!
1099
    Gitlab::AppLogger.info("Account Locked: username=#{username}")
Brian Neel's avatar
Brian Neel committed
1100 1101 1102
    super
  end

1103 1104 1105 1106 1107 1108 1109 1110
  protected

  # override, from Devise::Validatable
  def password_required?
    return false if internal?
    super
  end

1111 1112
  private

1113 1114 1115 1116 1117 1118 1119 1120
  def ci_projects_union
    scope  = { access_level: [Gitlab::Access::MASTER, Gitlab::Access::OWNER] }
    groups = groups_projects.where(members: scope)
    other  = projects.where(members: scope)

    Gitlab::SQL::Union.new([personal_projects.select(:id), groups.select(:id),
                            other.select(:id)])
  end
1121 1122 1123

  # Added according to https://github.com/plataformatec/devise/blob/7df57d5081f9884849ca15e4fde179ef164a575f/README.md#activejob-integration
  def send_devise_notification(notification, *args)
1124
    return true unless can?(:receive_notifications)
1125
    devise_mailer.__send__(notification, self, *args).deliver_later # rubocop:disable GitlabSecurity/PublicSend
1126
  end
Zeger-Jan van de Weg's avatar
Zeger-Jan van de Weg committed
1127

1128 1129 1130 1131 1132 1133 1134 1135 1136
  # This works around a bug in Devise 4.2.0 that erroneously causes a user to
  # be considered active in MySQL specs due to a sub-second comparison
  # issue. For more details, see: https://gitlab.com/gitlab-org/gitlab-ee/issues/2362#note_29004709
  def confirmation_period_valid?
    return false if self.class.allow_unconfirmed_access_for == 0.days

    super
  end

1137 1138 1139 1140 1141
  def ensure_user_rights_and_limits
    if external?
      self.can_create_group = false
      self.projects_limit   = 0
    else
1142 1143 1144
      # Only revert these back to the default if they weren't specifically changed in this update.
      self.can_create_group = gitlab_config.default_can_create_group unless can_create_group_changed?
      self.projects_limit = current_application_settings.default_projects_limit unless projects_limit_changed?
1145
    end
Zeger-Jan van de Weg's avatar
Zeger-Jan van de Weg committed
1146
  end
1147

1148 1149 1150 1151 1152 1153
  def signup_domain_valid?
    valid = true
    error = nil

    if current_application_settings.domain_blacklist_enabled?
      blocked_domains = current_application_settings.domain_blacklist
1154
      if domain_matches?(blocked_domains, email)
1155 1156 1157 1158 1159
        error = 'is not from an allowed domain.'
        valid = false
      end
    end

1160
    allowed_domains = current_application_settings.domain_whitelist
1161
    unless allowed_domains.blank?
1162
      if domain_matches?(allowed_domains, email)
1163 1164
        valid = true
      else
1165
        error = "domain is not authorized for sign-up"
1166 1167 1168 1169
        valid = false
      end
    end

1170
    errors.add(:email, error) unless valid
1171 1172 1173

    valid
  end
1174

1175
  def domain_matches?(email_domains, email)
1176 1177 1178 1179 1180 1181 1182
    signup_domain = Mail::Address.new(email).domain
    email_domains.any? do |domain|
      escaped = Regexp.escape(domain).gsub('\*', '.*?')
      regexp = Regexp.new "^#{escaped}$", Regexp::IGNORECASE
      signup_domain =~ regexp
    end
  end
1183 1184 1185 1186

  def generate_token(token_field)
    if token_field == :incoming_email_token
      # Needs to be all lowercase and alphanumeric because it's gonna be used in an email address.
1187
      SecureRandom.hex.to_i(16).to_s(36)
1188 1189 1190 1191
    else
      super
    end
  end
1192

1193 1194 1195 1196 1197 1198
  def self.unique_internal(scope, username, email_pattern, &b)
    scope.first || create_unique_internal(scope, username, email_pattern, &b)
  end

  def self.create_unique_internal(scope, username, email_pattern, &creation_block)
    # Since we only want a single one of these in an instance, we use an
1199
    # exclusive lease to ensure than this block is never run concurrently.
1200
    lease_key = "user:unique_internal:#{username}"
1201 1202 1203 1204 1205 1206 1207 1208
    lease = Gitlab::ExclusiveLease.new(lease_key, timeout: 1.minute.to_i)

    until uuid = lease.try_obtain
      # Keep trying until we obtain the lease. To prevent hammering Redis too
      # much we'll wait for a bit between retries.
      sleep(1)
    end

1209
    # Recheck if the user is already present. One might have been
1210 1211
    # added between the time we last checked (first line of this method)
    # and the time we acquired the lock.
1212 1213
    existing_user = uncached { scope.first }
    return existing_user if existing_user.present?
1214 1215 1216

    uniquify = Uniquify.new

1217
    username = uniquify.string(username) { |s| User.find_by_username(s) }
1218

1219
    email = uniquify.string(-> (n) { Kernel.sprintf(email_pattern, n) }) do |s|
1220 1221 1222
      User.find_by_email(s)
    end

1223
    user = scope.build(
1224 1225 1226
      username: username,
      email: email,
      &creation_block
1227
    )
James Lopez's avatar
James Lopez committed
1228

1229
    Users::UpdateService.new(user, user: user).execute(validate: false)
1230
    user
1231 1232 1233
  ensure
    Gitlab::ExclusiveLease.cancel(lease_key, uuid)
  end
gitlabhq's avatar
gitlabhq committed
1234
end