user.rb 37.1 KB
Newer Older
S
Steven Thonus 已提交
1 2
require 'carrierwave/orm/activerecord'

G
gitlabhq 已提交
3
class User < ActiveRecord::Base
4
  extend Gitlab::ConfigHelper
5
  extend Gitlab::CurrentSettings
6 7

  include Gitlab::ConfigHelper
8
  include Gitlab::CurrentSettings
H
Hiroyuki Sato 已提交
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

25
  add_authentication_token_field :authentication_token
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
R
Robert Speicher 已提交
48

49
  devise :two_factor_backupable, otp_number_of_backup_codes: 10
50
  serialize :otp_backup_codes, JSON # rubocop:disable Cop/ActiveRecordSerialize
R
Robert Speicher 已提交
51

52
  devise :lockable, :recoverable, :rememberable, :trackable,
53
    :validatable, :omniauthable, :confirmable, :registerable
G
gitlabhq 已提交
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

J
James Lopez 已提交
63
    Users::UpdateService.new(self, user: self).execute(validate: false)
64 65
  end

66
  attr_accessor :force_random_password
G
gitlabhq 已提交
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
C
Ciro Santilli 已提交
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 135
  #
  # Validations
  #
136
  # Note: devise :validatable above adds validations for :email and :password
C
Cyril 已提交
137
  validates :name, presence: true
D
Douwe Maan 已提交
138
  validates :email, confirmation: true
139 140
  validates :notification_email, presence: true
  validates :notification_email, email: true, if: ->(user) { user.notification_email != user.email }
141
  validates :public_email, presence: true, uniqueness: true, email: true, allow_blank: true
142
  validates :bio, length: { maximum: 255 }, allow_blank: true
143 144 145
  validates :projects_limit,
    presence: true,
    numericality: { greater_than_or_equal_to: 0, less_than_or_equal_to: Gitlab::Database::MAX_INT_VALUE }
146
  validates :username,
147
    dynamic_path: true,
148
    presence: true,
R
Robert Speicher 已提交
149
    uniqueness: { case_sensitive: false }
150

T
Tiago Botelho 已提交
151
  validate :namespace_uniq, if: :username_changed?
152 153
  validate :namespace_move_dir_allowed, if: :username_changed?

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

161
  before_validation :sanitize_attrs
T
Tiago Botelho 已提交
162 163
  before_validation :set_notification_email, if: :email_changed?
  before_validation :set_public_email, if: :public_email_changed?
164

T
Tiago Botelho 已提交
165
  after_update :update_emails_with_primary_email, if: :email_changed?
A
Alexis Reigel 已提交
166
  before_save :ensure_authentication_token, :ensure_incoming_email_token
T
Tiago Botelho 已提交
167
  before_save :ensure_user_rights_and_limits, if: :external_changed?
168
  before_save :skip_reconfirmation!, if: ->(user) { user.email_changed? && user.read_only_attribute?(:email) }
D
Dmitriy Zaporozhets 已提交
169
  after_save :ensure_namespace_correct
170
  after_commit :update_invalid_gpg_signatures, on: :update, if: -> { previous_changes.key?('email') }
171
  after_initialize :set_projects_limit
D
Dmitriy Zaporozhets 已提交
172 173
  after_destroy :post_destroy_hook

174
  # User's Layout preference
175
  enum layout: [:fixed, :fluid]
176

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

181
  # User's Project preference
D
Douwe Maan 已提交
182 183 184 185 186 187 188
  #
  # Note: When adding an option, it MUST go on the end of the hash with a
  # number higher than the current max. We cannot move options and/or change
  # their numbers.
  #
  # We skip 0 because this was used by an option that has since been removed.
  enum project_view: { activity: 1, files: 2 }
189

N
Nihad Abbasov 已提交
190
  alias_attribute :private_token, :authentication_token
191

192
  delegate :path, to: :namespace, allow_nil: true, prefix: true
193

194 195 196
  state_machine :state, initial: :active do
    event :block do
      transition active: :blocked
197
      transition ldap_blocked: :blocked
198 199
    end

200 201 202 203
    event :ldap_block do
      transition active: :ldap_blocked
    end

204 205
    event :activate do
      transition blocked: :active
206
      transition ldap_blocked: :active
207
    end
208 209 210 211 212

    state :blocked, :ldap_blocked do
      def blocked?
        true
      end
213 214 215 216 217 218 219 220 221

      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
222
    end
223 224
  end

225
  mount_uploader :avatar, AvatarUploader
226
  has_many :uploads, as: :model, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
S
Steven Thonus 已提交
227

A
Andrey Kumanyaev 已提交
228
  # Scopes
229
  scope :admins, -> { where(admin: true) }
230
  scope :blocked, -> { with_states(:blocked, :ldap_blocked) }
231
  scope :external, -> { where(external: true) }
J
James Lopez 已提交
232
  scope :active, -> { with_state(:active).non_internal }
B
Ben Bodenmiller 已提交
233
  scope :without_projects, -> { where('id NOT IN (SELECT DISTINCT(user_id) FROM members WHERE user_id IS NOT NULL AND requested_at IS NULL)') }
V
Valery Sizov 已提交
234
  scope :todo_authors, ->(user_id, state) { where(id: Todo.where(user_id: user_id, state: state).select(:author_id)) }
235 236
  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')) }
237 238

  def self.with_two_factor
239 240
    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])
241 242 243
  end

  def self.without_two_factor
244 245
    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)
246
  end
A
Andrey Kumanyaev 已提交
247

248 249 250
  #
  # Class methods
  #
A
Andrey Kumanyaev 已提交
251
  class << self
252
    # Devise method overridden to allow sign in with email or username
253 254 255
    def find_for_database_authentication(warden_conditions)
      conditions = warden_conditions.dup
      if login = conditions.delete(:login)
G
Gabriel Mazetto 已提交
256
        where(conditions).find_by("lower(username) = :value OR lower(email) = :value", value: login.downcase)
257
      else
G
Gabriel Mazetto 已提交
258
        find_by(conditions)
259 260
      end
    end
261

V
Valery Sizov 已提交
262
    def sort(method)
263 264 265
      order_method = method || 'id_desc'

      case order_method.to_s
266 267
      when 'recent_sign_in' then order_recent_sign_in
      when 'oldest_sign_in' then order_oldest_sign_in
268
      else
269
        order_by(order_method)
V
Valery Sizov 已提交
270 271 272
      end
    end

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

      User.find_by_sql([sql, { email: email }]).first
285
    end
286

287
    def filter(filter_name)
A
Andrey Kumanyaev 已提交
288
      case filter_name
289
      when 'admins'
290
        admins
291
      when 'blocked'
292
        blocked
293
      when 'two_factor_disabled'
294
        without_two_factor
295
      when 'two_factor_enabled'
296
        with_two_factor
297
      when 'wop'
298
        without_projects
299
      when 'external'
300
        external
A
Andrey Kumanyaev 已提交
301
      else
302
        active
A
Andrey Kumanyaev 已提交
303
      end
304 305
    end

306 307 308 309 310 311 312
    # 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.
313
    def search(query)
314
      table   = arel_table
H
Hiroyuki Sato 已提交
315
      pattern = User.to_pattern(query)
316

317 318 319 320 321 322 323 324 325
      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

326
      where(
327 328 329
        table[:name].matches(pattern)
          .or(table[:email].matches(pattern))
          .or(table[:username].matches(pattern))
330
      ).reorder(order % { query: ActiveRecord::Base.connection.quote(query) }, :name)
A
Andrey Kumanyaev 已提交
331
    end
332

333 334 335 336 337 338 339 340 341 342 343
    # 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(
344 345 346 347
        table[:name].matches(pattern)
          .or(table[:email].matches(pattern))
          .or(table[:username].matches(pattern))
          .or(table[:id].in(matched_by_emails_user_ids))
348 349 350
      )
    end

351
    def by_login(login)
352 353 354 355 356 357 358
      return nil unless login

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

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

R
Robert Speicher 已提交
365
    def find_by_username!(username)
366
      iwhere(username: username).take!
R
Robert Speicher 已提交
367 368
    end

T
Timothy Andrew 已提交
369
    def find_by_personal_access_token(token_string)
370 371
      return unless token_string

372
      PersonalAccessTokensFinder.new(state: 'active').find_by(token: token_string)&.user
T
Timothy Andrew 已提交
373 374
    end

Y
Yorick Peterse 已提交
375 376
    # Returns a user for the given SSH key.
    def find_by_ssh_key_id(key_id)
377
      Key.find_by(id: key_id)&.user
Y
Yorick Peterse 已提交
378 379
    end

380
    def find_by_full_path(path, follow_redirects: false)
381 382
      namespace = Namespace.for_user.find_by_full_path(path, follow_redirects: follow_redirects)
      namespace&.owner
383 384
    end

385 386 387
    def reference_prefix
      '@'
    end
388 389 390 391

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

    # Return (create if necessary) the ghost user. The ghost user
    # owns records previously belonging to deleted users.
    def ghost
401 402
      email = 'ghost%s@example.com'
      unique_internal(where(ghost: true), 'ghost', email) do |u|
403 404
        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'
405
        u.notification_email = email
406
      end
407
    end
V
vsizov 已提交
408
  end
R
randx 已提交
409

M
Michael Kozono 已提交
410 411 412 413
  def full_path
    username
  end

414 415 416 417
  def self.internal_attributes
    [:ghost]
  end

418
  def internal?
419 420 421 422 423 424 425 426
    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
427
    where(Hash[internal_attributes.zip([[false, nil]] * internal_attributes.size)])
428 429
  end

430 431 432
  #
  # Instance methods
  #
433 434 435 436 437

  def to_param
    username
  end

438
  def to_reference(_from_project = nil, target_project: nil, full: nil)
439 440 441
    "#{self.class.reference_prefix}#{username}"
  end

442 443
  def skip_confirmation=(bool)
    skip_confirmation! if bool
R
randx 已提交
444
  end
445

446
  def generate_reset_token
M
Marin Jankovski 已提交
447
    @reset_token, enc = Devise.token_generator.generate(self.class, :reset_password_token)
448 449 450 451

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

M
Marin Jankovski 已提交
452
    @reset_token
453 454
  end

455 456 457 458
  def recently_sent_password_reset?
    reset_password_sent_at.present? && reset_password_sent_at >= 1.minute.ago
  end

R
Robert Speicher 已提交
459
  def disable_two_factor!
460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477
    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?
478
    otp_required_for_login?
479 480 481
  end

  def two_factor_u2f_enabled?
482
    u2f_registrations.exists?
R
Robert Speicher 已提交
483 484
  end

485
  def namespace_uniq
486
    # Return early if username already failed the first uniqueness validation
487
    return if errors.key?(:username) &&
488
        errors[:username].include?('has already been taken')
489

490 491 492
    existing_namespace = Namespace.by_path(username)
    if existing_namespace && existing_namespace != namespace
      errors.add(:username, 'has already been taken')
493 494
    end
  end
495

496 497 498 499 500 501
  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

502
  def avatar_type
503 504
    unless avatar.image?
      errors.add :avatar, "only images allowed"
505 506 507
    end
  end

508
  def unique_email
509 510
    if !emails.exists?(email: email) && Email.exists?(email: email)
      errors.add(:email, 'has already been taken')
511
    end
512 513
  end

514
  def owns_notification_email
515
    return if temp_oauth_email?
516

517
    errors.add(:notification_email, "is not an email you own") unless all_emails.include?(notification_email)
518 519
  end

520
  def owns_public_email
521
    return if public_email.blank?
522

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

  def update_emails_with_primary_email
527
    primary_email_record = emails.find_by(email: email)
528
    if primary_email_record
J
James Lopez 已提交
529 530
      Emails::DestroyService.new(self, user: self, email: email).execute
      Emails::CreateService.new(self, user: self, email: email_was).execute
531 532 533
    end
  end

534
  def update_invalid_gpg_signatures
535
    gpg_keys.each(&:update_invalid_gpg_signatures)
536 537
  end

538 539
  # Returns the groups a user has access to
  def authorized_groups
540 541
    union = Gitlab::SQL::Union
      .new([groups.select(:id), authorized_projects.select(:namespace_id)])
542

543
    Group.where("namespaces.id IN (#{union.to_sql})") # rubocop:disable GitlabSecurity/SqlInjection
544 545
  end

546 547
  # Returns a relation of groups the user has access to, including their parent
  # and child groups (recursively).
548
  def all_expanded_groups
549
    Gitlab::GroupHierarchy.new(groups).all_groups
550 551 552 553 554 555
  end

  def expanded_groups_requiring_two_factor_authentication
    all_expanded_groups.where(require_two_factor_authentication: true)
  end

556
  def refresh_authorized_projects
557 558 559 560
    Users::RefreshAuthorizedProjectsService.new(self).execute
  end

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

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

    if min_access_level
570 571
      projects = projects
        .where('project_authorizations.access_level >= ?', min_access_level)
572
    end
573 574 575 576 577 578

    projects
  end

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

581 582 583 584 585 586 587 588 589 590
  # 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

591
  def owned_projects
592
    @owned_projects ||=
593 594
      Project.where('namespace_id IN (?) OR namespace_id = ?',
                    owned_groups.select(:id), namespace.id).joins(:namespace)
595 596
  end

597 598 599 600
  # 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
F
Felipe Artur 已提交
601
    authorized_projects(Gitlab::Access::REPORTER).non_archived.with_issues_enabled
602 603
  end

D
Dmitriy Zaporozhets 已提交
604
  def require_ssh_key?
605
    keys.count == 0 && Gitlab::ProtocolAccess.allowed?('ssh')
D
Dmitriy Zaporozhets 已提交
606 607
  end

608 609
  def require_password_creation?
    password_automatically_set? && allow_password_authentication?
610 611
  end

612
  def require_personal_access_token_creation_for_git_auth?
613
    return false if current_application_settings.password_authentication_enabled? || ldap_user?
614 615

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

618 619 620 621
  def allow_password_authentication?
    !ldap_user? && current_application_settings.password_authentication_enabled?
  end

622
  def can_change_username?
623
    gitlab_config.username_changing_enabled
624 625
  end

D
Dmitriy Zaporozhets 已提交
626
  def can_create_project?
627
    projects_limit_left > 0
D
Dmitriy Zaporozhets 已提交
628 629 630
  end

  def can_create_group?
631
    can?(:create_group)
D
Dmitriy Zaporozhets 已提交
632 633
  end

634 635 636 637
  def can_select_namespace?
    several_namespaces? || admin
  end

638
  def can?(action, subject = :global)
H
http://jneen.net/ 已提交
639
    Ability.allowed?(self, action, subject)
D
Dmitriy Zaporozhets 已提交
640 641 642 643 644 645
  end

  def first_name
    name.split.first unless name.blank?
  end

646
  def projects_limit_left
647 648 649 650 651
    projects_limit - personal_projects_count
  end

  def personal_projects_count
    @personal_projects_count ||= personal_projects.count
652 653
  end

654 655
  def recent_push(project = nil)
    service = Users::LastPushEventService.new(self)
D
Dmitriy Zaporozhets 已提交
656

657 658 659 660
    if project
      service.last_event_for_project(project)
    else
      service.last_event_for_user
661
    end
D
Dmitriy Zaporozhets 已提交
662 663 664
  end

  def several_namespaces?
665
    owned_groups.any? || masters_groups.any?
D
Dmitriy Zaporozhets 已提交
666 667 668 669 670
  end

  def namespace_id
    namespace.try :id
  end
671

672 673 674
  def name_with_username
    "#{name} (#{username})"
  end
D
Dmitriy Zaporozhets 已提交
675

676
  def already_forked?(project)
677 678 679
    !!fork_of(project)
  end

680
  def fork_of(project)
681 682 683 684
    links = ForkedProjectLink.where(
      forked_from_project_id: project,
      forked_to_project_id: personal_projects.unscope(:order)
    )
685 686 687 688 689 690
    if links.any?
      links.first.forked_to_project
    else
      nil
    end
  end
691 692

  def ldap_user?
693 694 695 696 697
    identities.exists?(["provider LIKE ? AND extern_uid IS NOT NULL", "ldap%"])
  end

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

700
  def project_deploy_keys
701
    DeployKey.unscoped.in_projects(authorized_projects.pluck(:id)).distinct(:id)
702 703
  end

704
  def accessible_deploy_keys
705 706 707 708 709
    @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
710
  end
711 712

  def created_by
S
skv 已提交
713
    User.find_by(id: created_by_id) if created_by_id
714
  end
715 716

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

723
  def set_notification_email
724 725
    if notification_email.blank? || !all_emails.include?(notification_email)
      self.notification_email = email
726 727 728
    end
  end

729
  def set_public_email
730
    if public_email.blank? || !all_emails.include?(public_email)
731
      self.public_email = ''
732 733 734
    end
  end

735
  def update_secondary_emails!
736 737 738
    set_notification_email
    set_public_email
    save if notification_email_changed? || public_email_changed?
739 740
  end

741
  def set_projects_limit
742 743 744
    # `User.select(:id)` raises
    # `ActiveModel::MissingAttributeError: missing attribute: projects_limit`
    # without this safeguard!
745
    return unless has_attribute?(:projects_limit)
746

747
    connection_default_value_defined = new_record? && !projects_limit_changed?
748
    return unless projects_limit.nil? || connection_default_value_defined
749 750 751 752

    self.projects_limit = current_application_settings.default_projects_limit
  end

753
  def requires_ldap_check?
754 755 756
    if !Gitlab.config.ldap.enabled
      false
    elsif ldap_user?
757 758 759 760 761 762
      !last_credential_check_at || (last_credential_check_at + 1.hour) < Time.now
    else
      false
    end
  end

J
Jacob Vosmaer 已提交
763 764 765 766 767 768 769
  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

770 771 772 773 774
  def solo_owned_groups
    @solo_owned_groups ||= owned_groups.select do |group|
      group.owners == [self]
    end
  end
775 776

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

    self
782
  end
783

784 785 786 787
  def can_leave_project?(project)
    project.namespace != namespace &&
      project.project_member(self)
  end
788

J
Jerome Dalbert 已提交
789
  def full_website_url
790
    return "http://#{website_url}" if website_url !~ /\Ahttps?:\/\//
J
Jerome Dalbert 已提交
791 792 793 794 795

    website_url
  end

  def short_website_url
796
    website_url.sub(/\Ahttps?:\/\//, '')
J
Jerome Dalbert 已提交
797
  end
G
GitLab 已提交
798

799
  def all_ssh_keys
800
    keys.map(&:publishable_key)
801
  end
802 803

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

807 808 809
  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
810
    avatar_path(args) || GravatarService.new.execute(email, size, scale, username: username)
811
  end
D
Dmitriy Zaporozhets 已提交
812

813
  def all_emails
814
    all_emails = []
815 816
    all_emails << email unless temp_oauth_email?
    all_emails.concat(emails.map(&:email))
817
    all_emails
818 819
  end

K
Kirill Zaitsev 已提交
820 821 822 823
  def hook_attrs
    {
      name: name,
      username: username,
824
      avatar_url: avatar_url(only_path: false)
K
Kirill Zaitsev 已提交
825 826 827
    }
  end

D
Dmitriy Zaporozhets 已提交
828 829
  def ensure_namespace_correct
    # Ensure user has namespace
830
    create_namespace!(path: username, name: username) unless namespace
D
Dmitriy Zaporozhets 已提交
831

832
    if username_changed?
833 834 835 836 837 838
      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
D
Dmitriy Zaporozhets 已提交
839 840 841 842
    end
  end

  def post_destroy_hook
843
    log_info("User \"#{name}\" (#{email})  was removed")
D
Dmitriy Zaporozhets 已提交
844 845 846
    system_hook_service.execute_hooks_for(self, :destroy)
  end

N
Nick Thomas 已提交
847 848 849 850 851
  def delete_async(deleted_by:, params: {})
    block if params[:hard_delete]
    DeleteUserWorker.perform_async(deleted_by.id, id, params)
  end

D
Dmitriy Zaporozhets 已提交
852
  def notification_service
D
Dmitriy Zaporozhets 已提交
853 854 855
    NotificationService.new
  end

856
  def log_info(message)
D
Dmitriy Zaporozhets 已提交
857 858 859 860 861 862
    Gitlab::AppLogger.info message
  end

  def system_hook_service
    SystemHooksService.new
  end
C
Ciro Santilli 已提交
863 864

  def starred?(project)
V
Valery Sizov 已提交
865
    starred_projects.exists?(project.id)
C
Ciro Santilli 已提交
866 867 868
  end

  def toggle_star(project)
869
    UsersStarProject.transaction do
870 871
      user_star_project = users_star_projects
          .where(project: project, user: self).lock(true).first
872 873 874 875 876 877

      if user_star_project
        user_star_project.destroy
      else
        UsersStarProject.create!(project: project, user: self)
      end
C
Ciro Santilli 已提交
878 879
    end
  end
880 881

  def manageable_namespaces
G
Guilherme Garnier 已提交
882
    @manageable_namespaces ||= [namespace] + owned_groups + masters_groups
883
  end
D
Dmitriy Zaporozhets 已提交
884

885 886 887 888 889 890
  def namespaces
    namespace_ids = groups.pluck(:id)
    namespace_ids.push(namespace.id)
    Namespace.where(id: namespace_ids)
  end

D
Dmitriy Zaporozhets 已提交
891
  def oauth_authorized_tokens
892
    Doorkeeper::AccessToken.where(resource_owner_id: id, revoked_at: nil)
D
Dmitriy Zaporozhets 已提交
893
  end
894

895 896 897 898 899 900 901 902 903
  # 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
904
  # ms on a database with a similar size to GitLab.com's database. On the other
905 906
  # hand, using a subquery means we can get the exact same data in about 40 ms.
  def contributed_projects
907 908 909 910 911
    events = Event.select(:project_id)
      .contributions.where(author_id: self)
      .where("created_at > ?", Time.now - 1.year)
      .uniq
      .reorder(nil)
912 913

    Project.where(id: events)
914
  end
915

916 917 918
  def can_be_removed?
    !solo_owned_groups.present?
  end
919 920

  def ci_authorized_runners
K
Kamil Trzcinski 已提交
921
    @ci_authorized_runners ||= begin
922
      runner_ids = Ci::RunnerProject
923
        .where("ci_runner_projects.project_id IN (#{ci_projects_union.to_sql})") # rubocop:disable GitlabSecurity/SqlInjection
924
        .select(:runner_id)
K
Kamil Trzcinski 已提交
925 926
      Ci::Runner.specific.where(id: runner_ids)
    end
927
  end
928

929 930 931 932
  def notification_settings_for(source)
    notification_settings.find_or_initialize_by(source: source)
  end

933 934 935
  # Lazy load global notification setting
  # Initializes User setting with Participating level if setting not persisted
  def global_notification_setting
936 937 938 939 940 941
    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
942 943
  end

944
  def assigned_open_merge_requests_count(force: false)
945
    Rails.cache.fetch(['users', id, 'assigned_open_merge_requests_count'], force: force, expires_in: 20.minutes) do
946
      MergeRequestsFinder.new(self, assignee_id: self.id, state: 'opened').execute.count
947 948 949
    end
  end

J
Josh Frye 已提交
950
  def assigned_open_issues_count(force: false)
951
    Rails.cache.fetch(['users', id, 'assigned_open_issues_count'], force: force, expires_in: 20.minutes) do
952
      IssuesFinder.new(self, assignee_id: self.id, state: 'opened').execute.count
953
    end
954 955
  end

J
Josh Frye 已提交
956
  def update_cache_counts
957
    assigned_open_merge_requests_count(force: true)
J
Josh Frye 已提交
958 959 960
    assigned_open_issues_count(force: true)
  end

961
  def invalidate_cache_counts
962 963 964 965 966
    invalidate_issue_cache_counts
    invalidate_merge_request_cache_counts
  end

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

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

P
Paco Guzman 已提交
974 975
  def todos_done_count(force: false)
    Rails.cache.fetch(['users', id, 'todos_done_count'], force: force) do
976
      TodosFinder.new(self, state: :done).execute.count
P
Paco Guzman 已提交
977 978 979 980 981
    end
  end

  def todos_pending_count(force: false)
    Rails.cache.fetch(['users', id, 'todos_pending_count'], force: force) do
982
      TodosFinder.new(self, state: :pending).execute.count
P
Paco Guzman 已提交
983 984 985 986 987 988 989 990
    end
  end

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

991 992 993 994 995 996 997 998 999 1000 1001 1002
  # 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
J
James Lopez 已提交
1003
      Users::UpdateService.new(self, user: self).execute(validate: false)
1004 1005 1006
    end
  end

1007 1008 1009 1010 1011 1012 1013 1014 1015
  def access_level
    if admin?
      :admin
    else
      :regular
    end
  end

  def access_level=(new_level)
D
Douwe Maan 已提交
1016 1017
    new_level = new_level.to_s
    return unless %w(admin regular).include?(new_level)
1018

D
Douwe Maan 已提交
1019 1020
    self.admin = (new_level == 'admin')
  end
1021

1022 1023 1024 1025 1026 1027
  # Does the user have access to all private groups & projects?
  # Overridden in EE to also check auditor?
  def full_private_access?
    admin?
  end

1028
  def update_two_factor_requirement
1029
    periods = expanded_groups_requiring_two_factor_authentication.pluck(:two_factor_grace_period)
1030

1031
    self.require_two_factor_authentication_from_group = periods.any?
1032 1033 1034 1035 1036
    self.two_factor_grace_period = periods.min || User.column_defaults['two_factor_grace_period']

    save
  end

A
Alexis Reigel 已提交
1037 1038 1039 1040 1041 1042 1043
  # 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

A
Alexis Reigel 已提交
1044 1045 1046 1047
  def verified_email?(email)
    self.email == email
  end

1048 1049 1050 1051 1052 1053 1054 1055 1056 1057 1058 1059 1060 1061 1062 1063
  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

1064 1065 1066 1067 1068 1069 1070 1071
  protected

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

1072 1073
  private

1074 1075 1076 1077 1078 1079 1080 1081
  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
V
Valery Sizov 已提交
1082 1083 1084

  # Added according to https://github.com/plataformatec/devise/blob/7df57d5081f9884849ca15e4fde179ef164a575f/README.md#activejob-integration
  def send_devise_notification(notification, *args)
1085
    return true unless can?(:receive_notifications)
1086
    devise_mailer.__send__(notification, self, *args).deliver_later # rubocop:disable GitlabSecurity/PublicSend
V
Valery Sizov 已提交
1087
  end
Z
Zeger-Jan van de Weg 已提交
1088

1089 1090 1091 1092 1093 1094 1095 1096 1097
  # 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

1098 1099 1100 1101 1102
  def ensure_user_rights_and_limits
    if external?
      self.can_create_group = false
      self.projects_limit   = 0
    else
T
Tiago Botelho 已提交
1103
      self.can_create_group = gitlab_config.default_can_create_group
1104 1105
      self.projects_limit = current_application_settings.default_projects_limit
    end
Z
Zeger-Jan van de Weg 已提交
1106
  end
1107

1108 1109 1110 1111 1112 1113
  def signup_domain_valid?
    valid = true
    error = nil

    if current_application_settings.domain_blacklist_enabled?
      blocked_domains = current_application_settings.domain_blacklist
1114
      if domain_matches?(blocked_domains, email)
1115 1116 1117 1118 1119
        error = 'is not from an allowed domain.'
        valid = false
      end
    end

1120
    allowed_domains = current_application_settings.domain_whitelist
1121
    unless allowed_domains.blank?
1122
      if domain_matches?(allowed_domains, email)
1123 1124
        valid = true
      else
D
dev-chris 已提交
1125
        error = "domain is not authorized for sign-up"
1126 1127 1128 1129
        valid = false
      end
    end

1130
    errors.add(:email, error) unless valid
1131 1132 1133

    valid
  end
1134

1135
  def domain_matches?(email_domains, email)
1136 1137 1138 1139 1140 1141 1142
    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
1143 1144 1145 1146

  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.
1147
      SecureRandom.hex.to_i(16).to_s(36)
1148 1149 1150 1151
    else
      super
    end
  end
1152

1153 1154 1155 1156 1157 1158
  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
1159
    # exclusive lease to ensure than this block is never run concurrently.
1160
    lease_key = "user:unique_internal:#{username}"
1161 1162 1163 1164 1165 1166 1167 1168
    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

1169
    # Recheck if the user is already present. One might have been
1170 1171
    # added between the time we last checked (first line of this method)
    # and the time we acquired the lock.
1172 1173
    existing_user = uncached { scope.first }
    return existing_user if existing_user.present?
1174 1175 1176

    uniquify = Uniquify.new

1177
    username = uniquify.string(username) { |s| User.find_by_username(s) }
1178

1179
    email = uniquify.string(-> (n) { Kernel.sprintf(email_pattern, n) }) do |s|
1180 1181 1182
      User.find_by_email(s)
    end

1183
    user = scope.build(
1184 1185 1186
      username: username,
      email: email,
      &creation_block
1187
    )
J
James Lopez 已提交
1188

J
James Lopez 已提交
1189
    Users::UpdateService.new(user, user: user).execute(validate: false)
1190
    user
1191 1192 1193
  ensure
    Gitlab::ExclusiveLease.cancel(lease_key, uuid)
  end
G
gitlabhq 已提交
1194
end