user.rb 37.2 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

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
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?
166

167
  after_update :update_emails_with_primary_email, if: :email_changed?
Alexis Reigel's avatar
Alexis Reigel committed
168
  before_save :ensure_authentication_token, :ensure_incoming_email_token
169
  before_save :ensure_user_rights_and_limits, if: :external_changed?
170
  before_save :skip_reconfirmation!, if: ->(user) { user.email_changed? && user.read_only_attribute?(:email) }
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
171
  after_save :ensure_namespace_correct
172
  after_commit :update_invalid_gpg_signatures, on: :update, if: -> { previous_changes.key?('email') }
173
  after_initialize :set_projects_limit
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
174 175
  after_destroy :post_destroy_hook

176
  # User's Layout preference
177
  enum layout: [:fixed, :fluid]
178

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

183
  # User's Project preference
184 185 186 187 188 189 190
  #
  # 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 }
191

Nihad Abbasov's avatar
Nihad Abbasov committed
192
  alias_attribute :private_token, :authentication_token
193

194
  delegate :path, to: :namespace, allow_nil: true, prefix: true
195

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

202 203 204 205
    event :ldap_block do
      transition active: :ldap_blocked
    end

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

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

      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
224
    end
225 226
  end

227
  mount_uploader :avatar, AvatarUploader
228
  has_many :uploads, as: :model, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
229

Andrey Kumanyaev's avatar
Andrey Kumanyaev committed
230
  # Scopes
231
  scope :admins, -> { where(admin: true) }
232
  scope :blocked, -> { with_states(:blocked, :ldap_blocked) }
233
  scope :external, -> { where(external: true) }
James Lopez's avatar
James Lopez committed
234
  scope :active, -> { with_state(:active).non_internal }
235
  scope :without_projects, -> { where('id NOT IN (SELECT DISTINCT(user_id) FROM members WHERE user_id IS NOT NULL AND requested_at IS NULL)') }
236
  scope :todo_authors, ->(user_id, state) { where(id: Todo.where(user_id: user_id, state: state).select(:author_id)) }
237 238
  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')) }
239 240

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

  def self.without_two_factor
246 247
    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)
248
  end
Andrey Kumanyaev's avatar
Andrey Kumanyaev committed
249

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

Valery Sizov's avatar
Valery Sizov committed
264
    def sort(method)
265 266 267
      order_method = method || 'id_desc'

      case order_method.to_s
268 269
      when 'recent_sign_in' then order_recent_sign_in
      when 'oldest_sign_in' then order_oldest_sign_in
270
      else
271
        order_by(order_method)
Valery Sizov's avatar
Valery Sizov committed
272 273 274
      end
    end

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

      User.find_by_sql([sql, { email: email }]).first
287
    end
288

289
    def filter(filter_name)
Andrey Kumanyaev's avatar
Andrey Kumanyaev committed
290
      case filter_name
291
      when 'admins'
292
        admins
293
      when 'blocked'
294
        blocked
295
      when 'two_factor_disabled'
296
        without_two_factor
297
      when 'two_factor_enabled'
298
        with_two_factor
299
      when 'wop'
300
        without_projects
301
      when 'external'
302
        external
Andrey Kumanyaev's avatar
Andrey Kumanyaev committed
303
      else
304
        active
Andrey Kumanyaev's avatar
Andrey Kumanyaev committed
305
      end
306 307
    end

308 309 310 311 312 313 314
    # 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.
315
    def search(query)
316
      table   = arel_table
317
      pattern = User.to_pattern(query)
318

319 320 321 322 323 324 325 326 327
      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

328
      where(
329 330 331
        table[:name].matches(pattern)
          .or(table[:email].matches(pattern))
          .or(table[:username].matches(pattern))
332
      ).reorder(order % { query: ActiveRecord::Base.connection.quote(query) }, :name)
Andrey Kumanyaev's avatar
Andrey Kumanyaev committed
333
    end
334

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

353
    def by_login(login)
354 355 356 357 358 359 360
      return nil unless login

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

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

367
    def find_by_username!(username)
368
      iwhere(username: username).take!
369 370
    end

371
    def find_by_personal_access_token(token_string)
372 373
      return unless token_string

374
      PersonalAccessTokensFinder.new(state: 'active').find_by(token: token_string)&.user
375 376
    end

377 378
    # Returns a user for the given SSH key.
    def find_by_ssh_key_id(key_id)
379
      Key.find_by(id: key_id)&.user
380 381
    end

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

387 388 389
    def reference_prefix
      '@'
    end
390 391 392 393

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

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

Michael Kozono's avatar
Michael Kozono committed
412 413 414 415
  def full_path
    username
  end

416 417 418 419
  def self.internal_attributes
    [:ghost]
  end

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

432 433 434
  #
  # Instance methods
  #
435 436 437 438 439

  def to_param
    username
  end

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

444 445
  def skip_confirmation=(bool)
    skip_confirmation! if bool
randx's avatar
randx committed
446
  end
447

448
  def generate_reset_token
449
    @reset_token, enc = Devise.token_generator.generate(self.class, :reset_password_token)
450 451 452 453

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

454
    @reset_token
455 456
  end

457 458 459 460
  def recently_sent_password_reset?
    reset_password_sent_at.present? && reset_password_sent_at >= 1.minute.ago
  end

461
  def disable_two_factor!
462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479
    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?
480
    otp_required_for_login?
481 482 483
  end

  def two_factor_u2f_enabled?
484
    u2f_registrations.exists?
485 486
  end

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

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

498 499 500 501 502 503
  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

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

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

516
  def owns_notification_email
517
    return if temp_oauth_email?
518

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

522
  def owns_public_email
523
    return if public_email.blank?
524

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

  def update_emails_with_primary_email
529
    primary_email_record = emails.find_by(email: email)
530
    if primary_email_record
James Lopez's avatar
James Lopez committed
531 532
      Emails::DestroyService.new(self, user: self, email: email).execute
      Emails::CreateService.new(self, user: self, email: email_was).execute
533 534 535
    end
  end

536
  def update_invalid_gpg_signatures
537
    gpg_keys.each(&:update_invalid_gpg_signatures)
538 539
  end

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

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

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

  def expanded_groups_requiring_two_factor_authentication
    all_expanded_groups.where(require_two_factor_authentication: true)
  end

558
  def refresh_authorized_projects
559 560 561 562
    Users::RefreshAuthorizedProjectsService.new(self).execute
  end

  def remove_project_authorizations(project_ids)
563
    project_authorizations.where(project_id: project_ids).delete_all
564 565
  end

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

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

    projects
  end

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

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

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

599 600 601 602
  # 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
603
    authorized_projects(Gitlab::Access::REPORTER).non_archived.with_issues_enabled
604 605
  end

Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
606
  def require_ssh_key?
607
    keys.count == 0 && Gitlab::ProtocolAccess.allowed?('ssh')
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
608 609
  end

610 611
  def require_password_creation?
    password_automatically_set? && allow_password_authentication?
612 613
  end

614
  def require_personal_access_token_creation_for_git_auth?
615
    return false if current_application_settings.password_authentication_enabled? || ldap_user?
616 617

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

620 621 622 623
  def allow_password_authentication?
    !ldap_user? && current_application_settings.password_authentication_enabled?
  end

624
  def can_change_username?
625
    gitlab_config.username_changing_enabled
626 627
  end

Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
628
  def can_create_project?
629
    projects_limit_left > 0
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
630 631 632
  end

  def can_create_group?
633
    can?(:create_group)
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
634 635
  end

636 637 638 639
  def can_select_namespace?
    several_namespaces? || admin
  end

640
  def can?(action, subject = :global)
641
    Ability.allowed?(self, action, subject)
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
642 643 644 645 646 647
  end

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

648
  def projects_limit_left
649 650 651 652 653
    projects_limit - personal_projects_count
  end

  def personal_projects_count
    @personal_projects_count ||= personal_projects.count
654 655
  end

656 657
  def recent_push(project = nil)
    service = Users::LastPushEventService.new(self)
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
658

659 660 661 662
    if project
      service.last_event_for_project(project)
    else
      service.last_event_for_user
663
    end
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
664 665 666
  end

  def several_namespaces?
667
    owned_groups.any? || masters_groups.any?
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
668 669 670 671 672
  end

  def namespace_id
    namespace.try :id
  end
673

674 675 676
  def name_with_username
    "#{name} (#{username})"
  end
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
677

678
  def already_forked?(project)
679 680 681
    !!fork_of(project)
  end

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

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

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

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

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

  def created_by
skv's avatar
skv committed
715
    User.find_by(id: created_by_id) if created_by_id
716
  end
717 718

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

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

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

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

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

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

    self.projects_limit = current_application_settings.default_projects_limit
  end

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

Jacob Vosmaer's avatar
Jacob Vosmaer committed
765 766 767 768 769 770 771
  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

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

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

    self
784
  end
785

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

Jerome Dalbert's avatar
Jerome Dalbert committed
791
  def full_website_url
792
    return "http://#{website_url}" if website_url !~ /\Ahttps?:\/\//
Jerome Dalbert's avatar
Jerome Dalbert committed
793 794 795 796 797

    website_url
  end

  def short_website_url
798
    website_url.sub(/\Ahttps?:\/\//, '')
Jerome Dalbert's avatar
Jerome Dalbert committed
799
  end
GitLab's avatar
GitLab committed
800

801
  def all_ssh_keys
802
    keys.map(&:publishable_key)
803
  end
804 805

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

809 810 811
  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
812
    avatar_path(args) || GravatarService.new.execute(email, size, scale, username: username)
813
  end
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
814

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

Kirill Zaitsev's avatar
Kirill Zaitsev committed
822 823 824 825
  def hook_attrs
    {
      name: name,
      username: username,
826
      avatar_url: avatar_url(only_path: false)
Kirill Zaitsev's avatar
Kirill Zaitsev committed
827 828 829
    }
  end

Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
830 831
  def ensure_namespace_correct
    # Ensure user has namespace
832
    create_namespace!(path: username, name: username) unless namespace
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
833

834
    if username_changed?
835 836 837 838 839 840
      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
841 842 843 844
    end
  end

  def post_destroy_hook
845
    log_info("User \"#{name}\" (#{email})  was removed")
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
846 847 848
    system_hook_service.execute_hooks_for(self, :destroy)
  end

849 850 851 852 853
  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
854
  def notification_service
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
855 856 857
    NotificationService.new
  end

858
  def log_info(message)
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
859 860 861 862 863 864
    Gitlab::AppLogger.info message
  end

  def system_hook_service
    SystemHooksService.new
  end
Ciro Santilli's avatar
Ciro Santilli committed
865 866

  def starred?(project)
867
    starred_projects.exists?(project.id)
Ciro Santilli's avatar
Ciro Santilli committed
868 869 870
  end

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

      if user_star_project
        user_star_project.destroy
      else
        UsersStarProject.create!(project: project, user: self)
      end
Ciro Santilli's avatar
Ciro Santilli committed
880 881
    end
  end
882 883

  def manageable_namespaces
884
    @manageable_namespaces ||= [namespace] + owned_groups + masters_groups
885
  end
886

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

893
  def oauth_authorized_tokens
894
    Doorkeeper::AccessToken.where(resource_owner_id: id, revoked_at: nil)
895
  end
896

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

    Project.where(id: events)
916
  end
917

918 919 920
  def can_be_removed?
    !solo_owned_groups.present?
  end
921 922

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

931 932 933 934
  def notification_settings_for(source)
    notification_settings.find_or_initialize_by(source: source)
  end

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

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

952
  def assigned_open_issues_count(force: false)
953
    Rails.cache.fetch(['users', id, 'assigned_open_issues_count'], force: force, expires_in: 20.minutes) do
954
      IssuesFinder.new(self, assignee_id: self.id, state: 'opened').execute.count
955
    end
956 957
  end

958
  def update_cache_counts
959
    assigned_open_merge_requests_count(force: true)
960 961 962
    assigned_open_issues_count(force: true)
  end

963
  def invalidate_cache_counts
964 965 966 967 968
    invalidate_issue_cache_counts
    invalidate_merge_request_cache_counts
  end

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

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

976 977
  def todos_done_count(force: false)
    Rails.cache.fetch(['users', id, 'todos_done_count'], force: force) do
978
      TodosFinder.new(self, state: :done).execute.count
979 980 981 982 983
    end
  end

  def todos_pending_count(force: false)
    Rails.cache.fetch(['users', id, 'todos_pending_count'], force: force) do
984
      TodosFinder.new(self, state: :pending).execute.count
985 986 987 988 989 990 991 992
    end
  end

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

993 994 995 996 997 998 999 1000 1001 1002 1003 1004
  # 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
1005
      Users::UpdateService.new(self, user: self).execute(validate: false)
1006 1007 1008
    end
  end

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

  def access_level=(new_level)
Douwe Maan's avatar
Douwe Maan committed
1018 1019
    new_level = new_level.to_s
    return unless %w(admin regular).include?(new_level)
1020

Douwe Maan's avatar
Douwe Maan committed
1021 1022
    self.admin = (new_level == 'admin')
  end
1023

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

1030
  def update_two_factor_requirement
1031
    periods = expanded_groups_requiring_two_factor_authentication.pluck(:two_factor_grace_period)
1032

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

    save
  end

Alexis Reigel's avatar
Alexis Reigel committed
1039 1040 1041 1042 1043 1044 1045
  # 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

1046 1047 1048 1049
  def verified_email?(email)
    self.email == email
  end

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

1066 1067 1068 1069 1070 1071 1072 1073
  protected

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

1074 1075
  private

1076 1077 1078 1079 1080 1081 1082 1083
  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
1084 1085 1086

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

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

1100 1101 1102 1103 1104
  def ensure_user_rights_and_limits
    if external?
      self.can_create_group = false
      self.projects_limit   = 0
    else
1105
      self.can_create_group = gitlab_config.default_can_create_group
1106 1107
      self.projects_limit = current_application_settings.default_projects_limit
    end
Zeger-Jan van de Weg's avatar
Zeger-Jan van de Weg committed
1108
  end
1109

1110 1111 1112 1113 1114 1115
  def signup_domain_valid?
    valid = true
    error = nil

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

1122
    allowed_domains = current_application_settings.domain_whitelist
1123
    unless allowed_domains.blank?
1124
      if domain_matches?(allowed_domains, email)
1125 1126
        valid = true
      else
1127
        error = "domain is not authorized for sign-up"
1128 1129 1130 1131
        valid = false
      end
    end

1132
    errors.add(:email, error) unless valid
1133 1134 1135

    valid
  end
1136

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

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

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

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

    uniquify = Uniquify.new

1179
    username = uniquify.string(username) { |s| User.find_by_username(s) }
1180

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

1185
    user = scope.build(
1186 1187 1188
      username: username,
      email: email,
      &creation_block
1189
    )
James Lopez's avatar
James Lopez committed
1190

1191
    Users::UpdateService.new(user, user: user).execute(validate: false)
1192
    user
1193 1194 1195
  ensure
    Gitlab::ExclusiveLease.cancel(lease_key, uuid)
  end
gitlabhq's avatar
gitlabhq committed
1196
end