BigW Consortium Gitlab

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

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 AfterCommitQueue
11
  include Avatarable
12 13
  include Referable
  include Sortable
14
  include CaseSensitivity
15
  include TokenAuthenticatable
16
  include IgnorableColumn
17
  include FeatureGate
18
  include CreatedAtFilterable
19
  include IgnorableColumn
20
  include BulkMemberAccessLoad
21

22 23
  DEFAULT_NOTIFICATION_LEVEL = :participating

24 25
  ignore_column :external_email
  ignore_column :email_provider
26
  ignore_column :authentication_token
27

28
  add_authentication_token_field :incoming_email_token
29
  add_authentication_token_field :rss_token
30

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

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

48
  devise :two_factor_authenticatable,
49
         otp_secret_encryption_key: Gitlab::Application.secrets.otp_key_base
50

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

54
  devise :lockable, :recoverable, :rememberable, :trackable,
55
    :validatable, :omniauthable, :confirmable, :registerable
gitlabhq committed
56

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

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

65
    Users::UpdateService.new(self, user: self).execute(validate: false)
66 67
  end

68
  attr_accessor :force_random_password
gitlabhq committed
69

70 71 72
  # Virtual attribute for authenticating by either username or email
  attr_accessor :login

73 74 75 76
  #
  # Relations
  #

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

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

88 89 90 91 92
  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
93
  has_one :user_synced_attributes_metadata, autosave: true
94 95

  # Groups
96 97
  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
98
  has_many :groups, through: :group_members
99 100
  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
101

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

113 114 115 116 117 118
  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
119
  has_many :recent_events, -> { order "id DESC" }, foreign_key: :author_id,   class_name: "Event"
120 121 122 123 124 125 126 127 128 129
  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
130

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

135 136
  has_many :custom_attributes, class_name: 'UserCustomAttribute'

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

155
  validate :namespace_uniq, if: :username_changed?
156 157
  validate :namespace_move_dir_allowed, if: :username_changed?

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

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

179
  after_initialize :set_projects_limit
180

181
  # User's Layout preference
182
  enum layout: [:fixed, :fluid]
183

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

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

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
227

Andrey Kumanyaev committed
228
  # Scopes
229
  scope :admins, -> { where(admin: true) }
230
  scope :blocked, -> { with_states(:blocked, :ldap_blocked) }
231
  scope :external, -> { where(external: true) }
James Lopez committed
232
  scope :active, -> { with_state(:active).non_internal }
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)') }
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
Andrey Kumanyaev committed
247

248 249 250
  #
  # Class methods
  #
Andrey Kumanyaev committed
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)
256
        where(conditions).find_by("lower(username) = :value OR lower(email) = :value", value: login.downcase)
257
      else
258
        find_by(conditions)
259 260
      end
    end
261

Valery Sizov committed
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)
Valery Sizov committed
270 271 272
      end
    end

273
    def for_github_id(id)
274
      joins(:identities).merge(Identity.with_extern_uid(:github, id))
275 276
    end

277 278
    # Find a User by their primary email or any associated secondary email
    def find_by_any_email(email)
279 280 281 282 283 284 285 286 287 288
      by_any_email(email).take
    end

    # Returns a relation containing all the users for the given Email address
    def by_any_email(email)
      users = where(email: email)
      emails = joins(:emails).where(emails: { email: email })
      union = Gitlab::SQL::Union.new([users, emails])

      from("(#{union.to_sql}) #{table_name}")
289
    end
290

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

310 311 312 313 314 315 316
    # 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.
317
    def search(query)
318 319
      query = query.downcase

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

329
      where(
330 331 332
        fuzzy_arel_match(:name, query)
          .or(fuzzy_arel_match(:username, query))
          .or(arel_table[:email].eq(query))
333
      ).reorder(order % { query: ActiveRecord::Base.connection.quote(query) }, :name)
Andrey Kumanyaev committed
334
    end
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)
341 342
      query = query.downcase

343
      email_table = Email.arel_table
344 345
      matched_by_emails_user_ids = email_table
        .project(email_table[:user_id])
346
        .where(email_table[:email].eq(query))
347 348

      where(
349 350
        fuzzy_arel_match(:name, query)
          .or(fuzzy_arel_match(:username, query))
351
          .or(arel_table[:email].eq(query))
352
          .or(arel_table[:id].in(matched_by_emails_user_ids))
353 354 355
      )
    end

356
    def by_login(login)
357 358 359 360 361 362 363
      return nil unless login

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

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

370
    def find_by_username!(username)
371
      iwhere(username: username).take!
372 373
    end

374
    def find_by_personal_access_token(token_string)
375 376
      return unless token_string

377
      PersonalAccessTokensFinder.new(state: 'active').find_by(token: token_string)&.user
378 379
    end

380 381
    # Returns a user for the given SSH key.
    def find_by_ssh_key_id(key_id)
382
      Key.find_by(id: key_id)&.user
383 384
    end

385
    def find_by_full_path(path, follow_redirects: false)
386 387
      namespace = Namespace.for_user.find_by_full_path(path, follow_redirects: follow_redirects)
      namespace&.owner
388 389
    end

390 391 392
    def reference_prefix
      '@'
    end
393 394 395 396

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

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

Michael Kozono committed
415 416 417 418
  def full_path
    username
  end

419 420 421 422
  def self.internal_attributes
    [:ghost]
  end

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

435 436 437
  #
  # Instance methods
  #
438 439 440 441 442

  def to_param
    username
  end

443
  def to_reference(_from = nil, target_project: nil, full: nil)
444 445 446
    "#{self.class.reference_prefix}#{username}"
  end

447 448
  def skip_confirmation=(bool)
    skip_confirmation! if bool
449 450 451 452
  end

  def skip_reconfirmation=(bool)
    skip_reconfirmation! if bool
randx committed
453
  end
454

455
  def generate_reset_token
456
    @reset_token, enc = Devise.token_generator.generate(self.class, :reset_password_token)
457 458 459 460

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

461
    @reset_token
462 463
  end

464 465 466 467
  def recently_sent_password_reset?
    reset_password_sent_at.present? && reset_password_sent_at >= 1.minute.ago
  end

468 469 470 471 472 473 474 475
  def remember_me!
    super if ::Gitlab::Database.read_write?
  end

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

476
  def disable_two_factor!
477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494
    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?
495
    otp_required_for_login?
496 497 498
  end

  def two_factor_u2f_enabled?
499 500 501 502 503
    if u2f_registrations.loaded?
      u2f_registrations.any?
    else
      u2f_registrations.exists?
    end
504 505
  end

506
  def namespace_uniq
507
    # Return early if username already failed the first uniqueness validation
508
    return if errors.key?(:username) &&
509
        errors[:username].include?('has already been taken')
510

511 512 513
    existing_namespace = Namespace.by_path(username)
    if existing_namespace && existing_namespace != namespace
      errors.add(:username, 'has already been taken')
514 515
    end
  end
516

517 518 519 520 521 522
  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

523
  def avatar_type
524 525
    unless avatar.image?
      errors.add :avatar, "only images allowed"
526 527 528
    end
  end

529
  def unique_email
530 531
    if !emails.exists?(email: email) && Email.exists?(email: email)
      errors.add(:email, 'has already been taken')
532
    end
533 534
  end

535
  def owns_notification_email
536
    return if temp_oauth_email?
537

538
    errors.add(:notification_email, "is not an email you own") unless all_emails.include?(notification_email)
539 540
  end

541
  def owns_public_email
542
    return if public_email.blank?
543

544
    errors.add(:public_email, "is not an email you own") unless all_emails.include?(public_email)
545 546
  end

547 548 549 550 551
  # 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

552
  # Note: the use of the Emails services will cause `saves` on the user object, running
553
  # through the callbacks again and can have side effects, such as the `previous_changes`
554 555 556
  # 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
557
  def update_emails_with_primary_email
558
    previous_email = previous_changes[:email][0]  # grab this before the DestroyService is called
559
    primary_email_record = emails.find_by(email: email)
560
    Emails::DestroyService.new(self, user: self).execute(primary_email_record) if primary_email_record
561

562 563
    # 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
564
    Emails::CreateService.new(self, user: self, email: previous_email).execute(confirmed_at: confirmed_at)
565 566
  end

567
  def update_invalid_gpg_signatures
568
    gpg_keys.each(&:update_invalid_gpg_signatures)
569 570
  end

571 572
  # Returns the groups a user has access to
  def authorized_groups
573 574
    union = Gitlab::SQL::Union
      .new([groups.select(:id), authorized_projects.select(:namespace_id)])
575

576
    Group.where("namespaces.id IN (#{union.to_sql})") # rubocop:disable GitlabSecurity/SqlInjection
577 578
  end

579 580
  # Returns a relation of groups the user has access to, including their parent
  # and child groups (recursively).
581
  def all_expanded_groups
582
    Gitlab::GroupHierarchy.new(groups).all_groups
583 584 585 586 587 588
  end

  def expanded_groups_requiring_two_factor_authentication
    all_expanded_groups.where(require_two_factor_authentication: true)
  end

589
  def refresh_authorized_projects
590 591 592 593
    Users::RefreshAuthorizedProjectsService.new(self).execute
  end

  def remove_project_authorizations(project_ids)
594
    project_authorizations.where(project_id: project_ids).delete_all
595 596
  end

597
  def authorized_projects(min_access_level = nil)
598 599
    # We're overriding an association, so explicitly call super with no
    # arguments or it would be passed as `force_reload` to the association
600
    projects = super()
601 602

    if min_access_level
603 604
      projects = projects
        .where('project_authorizations.access_level >= ?', min_access_level)
605
    end
606 607 608 609 610 611

    projects
  end

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

614 615 616 617 618 619 620 621 622 623
  # 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

624
  def owned_projects
625
    @owned_projects ||=
626 627
      Project.where('namespace_id IN (?) OR namespace_id = ?',
                    owned_groups.select(:id), namespace.id).joins(:namespace)
628 629
  end

630 631 632 633
  # 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
634
    authorized_projects(Gitlab::Access::REPORTER).non_archived.with_issues_enabled
635 636
  end

Dmitriy Zaporozhets committed
637
  def require_ssh_key?
638 639 640
    count = Users::KeysCountService.new(self).count

    count.zero? && Gitlab::ProtocolAccess.allowed?('ssh')
Dmitriy Zaporozhets committed
641 642
  end

643 644 645 646 647 648
  def require_password_creation_for_web?
    allow_password_authentication_for_web? && password_automatically_set?
  end

  def require_password_creation_for_git?
    allow_password_authentication_for_git? && password_automatically_set?
649 650
  end

651
  def require_personal_access_token_creation_for_git_auth?
652
    return false if allow_password_authentication_for_git? || ldap_user?
653 654

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

657 658 659 660
  def require_extra_setup_for_git_auth?
    require_password_creation_for_git? || require_personal_access_token_creation_for_git_auth?
  end

661
  def allow_password_authentication?
662 663 664 665 666 667 668 669 670
    allow_password_authentication_for_web? || allow_password_authentication_for_git?
  end

  def allow_password_authentication_for_web?
    current_application_settings.password_authentication_enabled_for_web? && !ldap_user?
  end

  def allow_password_authentication_for_git?
    current_application_settings.password_authentication_enabled_for_git? && !ldap_user?
671 672
  end

673
  def can_change_username?
674
    gitlab_config.username_changing_enabled
675 676
  end

Dmitriy Zaporozhets committed
677
  def can_create_project?
678
    projects_limit_left > 0
Dmitriy Zaporozhets committed
679 680 681
  end

  def can_create_group?
682
    can?(:create_group)
Dmitriy Zaporozhets committed
683 684
  end

685 686 687 688
  def can_select_namespace?
    several_namespaces? || admin
  end

689
  def can?(action, subject = :global)
690
    Ability.allowed?(self, action, subject)
Dmitriy Zaporozhets committed
691 692
  end

693 694 695 696
  def confirm_deletion_with_password?
    !password_automatically_set? && allow_password_authentication?
  end

Dmitriy Zaporozhets committed
697 698 699 700
  def first_name
    name.split.first unless name.blank?
  end

701
  def projects_limit_left
702 703 704 705 706
    projects_limit - personal_projects_count
  end

  def personal_projects_count
    @personal_projects_count ||= personal_projects.count
707 708
  end

709 710
  def recent_push(project = nil)
    service = Users::LastPushEventService.new(self)
Dmitriy Zaporozhets committed
711

712 713 714 715
    if project
      service.last_event_for_project(project)
    else
      service.last_event_for_user
716
    end
Dmitriy Zaporozhets committed
717 718 719
  end

  def several_namespaces?
720
    owned_groups.any? || masters_groups.any?
Dmitriy Zaporozhets committed
721 722 723 724 725
  end

  def namespace_id
    namespace.try :id
  end
726

727 728 729
  def name_with_username
    "#{name} (#{username})"
  end
730

731
  def already_forked?(project)
732 733 734
    !!fork_of(project)
  end

735
  def fork_of(project)
736
    namespace.find_fork_of(project)
737
  end
738 739

  def ldap_user?
740 741 742 743 744
    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
745 746 747 748
  end

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

751
  def project_deploy_keys
752
    DeployKey.unscoped.in_projects(authorized_projects.pluck(:id)).distinct(:id)
753 754
  end

755
  def accessible_deploy_keys
756 757 758 759 760
    @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
761
  end
762 763

  def created_by
skv committed
764
    User.find_by(id: created_by_id) if created_by_id
765
  end
766 767

  def sanitize_attrs
768 769 770
    %i[skype linkedin twitter].each do |attr|
      value = self[attr]
      self[attr] = Sanitize.clean(value) if value.present?
771 772
    end
  end
773

774
  def set_notification_email
775 776
    if notification_email.blank? || !all_emails.include?(notification_email)
      self.notification_email = email
777 778 779
    end
  end

780
  def set_public_email
781
    if public_email.blank? || !all_emails.include?(public_email)
782
      self.public_email = ''
783 784 785
    end
  end

786
  def update_secondary_emails!
787 788 789
    set_notification_email
    set_public_email
    save if notification_email_changed? || public_email_changed?
790 791
  end

792
  def set_projects_limit
793 794 795
    # `User.select(:id)` raises
    # `ActiveModel::MissingAttributeError: missing attribute: projects_limit`
    # without this safeguard!
796
    return unless has_attribute?(:projects_limit)
797

798
    connection_default_value_defined = new_record? && !projects_limit_changed?
799
    return unless projects_limit.nil? || connection_default_value_defined
800 801 802 803

    self.projects_limit = current_application_settings.default_projects_limit
  end

804
  def requires_ldap_check?
805 806 807
    if !Gitlab.config.ldap.enabled
      false
    elsif ldap_user?
808 809 810 811 812 813
      !last_credential_check_at || (last_credential_check_at + 1.hour) < Time.now
    else
      false
    end
  end

Jacob Vosmaer committed
814 815 816 817 818 819 820
  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

821 822 823 824 825
  def solo_owned_groups
    @solo_owned_groups ||= owned_groups.select do |group|
      group.owners == [self]
    end
  end
826 827

  def with_defaults
828
    User.defaults.each do |k, v|
829
      public_send("#{k}=", v) # rubocop:disable GitlabSecurity/PublicSend
830
    end
831 832

    self
833
  end
834

835 836 837 838
  def can_leave_project?(project)
    project.namespace != namespace &&
      project.project_member(self)
  end
839

Jerome Dalbert committed
840
  def full_website_url
841
    return "http://#{website_url}" if website_url !~ /\Ahttps?:\/\//
Jerome Dalbert committed
842 843 844 845 846

    website_url
  end

  def short_website_url
847
    website_url.sub(/\Ahttps?:\/\//, '')
Jerome Dalbert committed
848
  end
GitLab committed
849

850
  def all_ssh_keys
851
    keys.map(&:publishable_key)
852
  end
853 854

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

858 859 860
  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
861
    avatar_path(args) || GravatarService.new.execute(email, size, scale, username: username)
862
  end
863

864 865 866 867
  def primary_email_verified?
    confirmed? && !temp_oauth_email?
  end

868
  def all_emails
869
    all_emails = []
870 871
    all_emails << email unless temp_oauth_email?
    all_emails.concat(emails.map(&:email))
872
    all_emails
873 874
  end

875
  def verified_emails
876
    verified_emails = []
877
    verified_emails << email if primary_email_verified?
878
    verified_emails.concat(emails.confirmed.pluck(:email))
879 880 881
    verified_emails
  end

882
  def verified_email?(check_email)
883
    downcased = check_email.downcase
884
    email == downcased ? primary_email_verified? : emails.confirmed.where(email: downcased).exists?
885 886
  end

Kirill Zaitsev committed
887 888 889 890
  def hook_attrs
    {
      name: name,
      username: username,
891
      avatar_url: avatar_url(only_path: false)
Kirill Zaitsev committed
892 893 894
    }
  end

895 896
  def ensure_namespace_correct
    # Ensure user has namespace
897
    create_namespace!(path: username, name: username) unless namespace
898

899
    if username_changed?
900 901 902 903 904 905
      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
906 907 908
    end
  end

909 910 911 912
  def username_changed_hook
    system_hook_service.execute_hooks_for(self, :rename)
  end

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

916 917 918
    system_hook_service.execute_hooks_for(self, :destroy)
  end

919 920 921 922
  def remove_key_cache
    Users::KeysCountService.new(self).delete_cache
  end

923 924 925 926 927
  def delete_async(deleted_by:, params: {})
    block if params[:hard_delete]
    DeleteUserWorker.perform_async(deleted_by.id, id, params)
  end

928
  def notification_service
929 930 931
    NotificationService.new
  end

932
  def log_info(message)
933 934 935 936 937 938
    Gitlab::AppLogger.info message
  end

  def system_hook_service
    SystemHooksService.new
  end
Ciro Santilli committed
939 940

  def starred?(project)
941
    starred_projects.exists?(project.id)
Ciro Santilli committed
942 943 944
  end

  def toggle_star(project)
945
    UsersStarProject.transaction do
946 947
      user_star_project = users_star_projects
          .where(project: project, user: self).lock(true).first
948 949 950 951 952 953

      if user_star_project
        user_star_project.destroy
      else
        UsersStarProject.create!(project: project, user: self)
      end
Ciro Santilli committed
954 955
    end
  end
956 957

  def manageable_namespaces
958 959 960 961 962 963 964 965 966 967
    @manageable_namespaces ||= [namespace] + manageable_groups
  end

  def manageable_groups
    union = Gitlab::SQL::Union.new([owned_groups.select(:id),
                                    masters_groups.select(:id)])
    arel_union = Arel::Nodes::SqlLiteral.new(union.to_sql)
    owned_and_master_groups = Group.where(Group.arel_table[:id].in(arel_union))

    Gitlab::GroupHierarchy.new(owned_and_master_groups).base_and_descendants
968
  end
969

970 971 972 973 974 975
  def namespaces
    namespace_ids = groups.pluck(:id)
    namespace_ids.push(namespace.id)
    Namespace.where(id: namespace_ids)
  end

976
  def oauth_authorized_tokens
977
    Doorkeeper::AccessToken.where(resource_owner_id: id, revoked_at: nil)
978
  end
979

980 981 982 983 984 985 986 987 988
  # 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
989
  # ms on a database with a similar size to GitLab.com's database. On the other
990 991
  # hand, using a subquery means we can get the exact same data in about 40 ms.
  def contributed_projects
992 993 994 995 996
    events = Event.select(:project_id)
      .contributions.where(author_id: self)
      .where("created_at > ?", Time.now - 1.year)
      .uniq
      .reorder(nil)
997 998

    Project.where(id: events)
999
  end
1000

1001 1002 1003
  def can_be_removed?
    !solo_owned_groups.present?
  end
1004 1005

  def ci_authorized_runners
1006
    @ci_authorized_runners ||= begin
1007
      runner_ids = Ci::RunnerProject
1008
        .where("ci_runner_projects.project_id IN (#{ci_projects_union.to_sql})") # rubocop:disable GitlabSecurity/SqlInjection
1009
        .select(:runner_id)
1010 1011
      Ci::Runner.specific.where(id: runner_ids)
    end
1012
  end
1013

1014
  def notification_settings_for(source)
1015 1016 1017 1018 1019
    if notification_settings.loaded?
      notification_settings.find { |notification| notification.source == source }
    else
      notification_settings.find_or_initialize_by(source: source)
    end
1020 1021
  end

1022 1023 1024
  # Lazy load global notification setting
  # Initializes User setting with Participating level if setting not persisted
  def global_notification_setting
1025 1026 1027 1028 1029 1030
    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
1031 1032
  end

1033
  def assigned_open_merge_requests_count(force: false)
1034
    Rails.cache.fetch(['users', id, 'assigned_open_merge_requests_count'], force: force, expires_in: 20.minutes) do
1035
      MergeRequestsFinder.new(self, assignee_id: self.id, state: 'opened').execute.count
1036 1037 1038
    end
  end

1039
  def assigned_open_issues_count(force: false)
1040
    Rails.cache.fetch(['users', id, 'assigned_open_issues_count'], force: force, expires_in: 20.minutes) do
1041
      IssuesFinder.new(self, assignee_id: self.id, state: 'opened').execute.count
1042
    end
1043 1044
  end

1045
  def update_cache_counts
1046
    assigned_open_merge_requests_count(force: true)
1047 1048 1049
    assigned_open_issues_count(force: true)
  end

1050
  def invalidate_cache_counts
1051 1052 1053 1054 1055
    invalidate_issue_cache_counts
    invalidate_merge_request_cache_counts
  end

  def invalidate_issue_cache_counts
1056 1057 1058
    Rails.cache.delete(['users', id, 'assigned_open_issues_count'])
  end

1059 1060 1061 1062
  def invalidate_merge_request_cache_counts
    Rails.cache.delete(['users', id, 'assigned_open_merge_requests_count'])
  end

1063
  def todos_done_count(force: false)
1064
    Rails.cache.fetch(['users', id, 'todos_done_count'], force: force, expires_in: 20.minutes) do
1065
      TodosFinder.new(self, state: :done).execute.count
1066 1067 1068 1069
    end
  end

  def todos_pending_count(force: false)
1070
    Rails.cache.fetch(['users', id, 'todos_pending_count'], force: force, expires_in: 20.minutes) do
1071
      TodosFinder.new(self, state: :pending).execute.count
1072 1073 1074 1075 1076 1077 1078 1079
    end
  end

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

1080 1081 1082 1083 1084 1085 1086 1087 1088 1089 1090 1091
  # 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
1092
      Users::UpdateService.new(self, user: self).execute(validate: false)
1093 1094 1095
    end
  end

1096 1097 1098 1099 1100 1101 1102 1103 1104
  def access_level
    if admin?
      :admin
    else
      :regular
    end
  end

  def access_level=(new_level)
Douwe Maan committed
1105 1106
    new_level = new_level.to_s
    return unless %w(admin regular).include?(new_level)
1107

Douwe Maan committed
1108 1109
    self.admin = (new_level == 'admin')
  end
1110

1111 1112 1113 1114 1115 1116
  # Does the user have access to all private groups & projects?
  # Overridden in EE to also check auditor?
  def full_private_access?
    admin?
  end

1117
  def update_two_factor_requirement
1118
    periods = expanded_groups_requiring_two_factor_authentication.pluck(:two_factor_grace_period)
1119

1120
    self.require_two_factor_authentication_from_group = periods.any?
1121 1122 1123 1124 1125
    self.two_factor_grace_period = periods.min || User.column_defaults['two_factor_grace_period']

    save
  end

1126 1127 1128 1129 1130 1131 1132
  # 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

1133 1134 1135 1136 1137 1138 1139 1140 1141 1142 1143 1144 1145 1146 1147 1148
  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 committed
1149 1150
  # override, from Devise
  def lock_access!
1151
    Gitlab::AppLogger.info("Account Locked: username=#{username}")
Brian Neel committed
1152 1153 1154
    super
  end

1155 1156 1157 1158 1159 1160 1161 1162 1163 1164 1165 1166 1167 1168 1169 1170 1171 1172 1173 1174 1175 1176 1177 1178 1179 1180 1181 1182
  # Determine the maximum access level for a group of projects in bulk.
  #
  # Returns a Hash mapping project ID -> maximum access level.
  def max_member_access_for_project_ids(project_ids)
    max_member_access_for_resource_ids(Project, project_ids) do |project_ids|
      project_authorizations.where(project: project_ids)
                            .group(:project_id)
                            .maximum(:access_level)
    end
  end

  def max_member_access_for_project(project_id)
    max_member_access_for_project_ids([project_id])[project_id]
  end

  # Determine the maximum access level for a group of groups in bulk.
  #
  # Returns a Hash mapping project ID -> maximum access level.
  def max_member_access_for_group_ids(group_ids)
    max_member_access_for_resource_ids(Group, group_ids) do |group_ids|
      group_members.where(source: group_ids).group(:source_id).maximum(:access_level)
    end
  end

  def max_member_access_for_group(group_id)
    max_member_access_for_group_ids([group_id])[group_id]
  end

1183 1184 1185 1186 1187
  protected

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

1189 1190 1191
    super
  end

1192 1193
  private

1194 1195 1196 1197 1198 1199 1200 1201
  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
1202 1203 1204

  # Added according to https://github.com/plataformatec/devise/blob/7df57d5081f9884849ca15e4fde179ef164a575f/README.md#activejob-integration
  def send_devise_notification(notification, *args)
1205
    return true unless can?(:receive_notifications)
1206

1207
    devise_mailer.__send__(notification, self, *args).deliver_later # rubocop:disable GitlabSecurity/PublicSend
1208
  end
Zeger-Jan van de Weg committed
1209

1210 1211 1212 1213 1214 1215 1216 1217 1218
  # 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

1219 1220 1221 1222 1223
  def ensure_user_rights_and_limits
    if external?
      self.can_create_group = false
      self.projects_limit   = 0
    else
1224 1225 1226
      # 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?
1227
    end
Zeger-Jan van de Weg committed
1228
  end
1229

1230 1231 1232 1233 1234 1235
  def signup_domain_valid?
    valid = true
    error = nil

    if current_application_settings.domain_blacklist_enabled?
      blocked_domains = current_application_settings.domain_blacklist
1236
      if domain_matches?(blocked_domains, email)
1237 1238 1239 1240 1241
        error = 'is not from an allowed domain.'
        valid = false
      end
    end

1242
    allowed_domains = current_application_settings.domain_whitelist
1243
    unless allowed_domains.blank?
1244
      if domain_matches?(allowed_domains, email)
1245 1246
        valid = true
      else
1247
        error = "domain is not authorized for sign-up"
1248 1249 1250 1251
        valid = false
      end
    end

1252
    errors.add(:email, error) unless valid
1253 1254 1255

    valid
  end
1256

1257
  def domain_matches?(email_domains, email)
1258 1259 1260 1261 1262 1263 1264
    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
1265 1266 1267 1268

  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.
1269
      SecureRandom.hex.to_i(16).to_s(36)
1270 1271 1272 1273
    else
      super
    end
  end
1274

1275 1276 1277 1278 1279 1280
  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
1281
    # exclusive lease to ensure than this block is never run concurrently.
1282
    lease_key = "user:unique_internal:#{username}"
1283 1284 1285 1286 1287 1288 1289 1290
    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

1291
    # Recheck if the user is already present. One might have been
1292 1293
    # added between the time we last checked (first line of this method)
    # and the time we acquired the lock.
1294 1295
    existing_user = uncached { scope.first }
    return existing_user if existing_user.present?
1296 1297 1298

    uniquify = Uniquify.new

1299
    username = uniquify.string(username) { |s| User.find_by_username(s) }
1300

1301
    email = uniquify.string(-> (n) { Kernel.sprintf(email_pattern, n) }) do |s|
1302 1303 1304
      User.find_by_email(s)
    end

1305
    user = scope.build(
1306 1307 1308
      username: username,
      email: email,
      &creation_block
1309
    )
James Lopez committed
1310

1311
    Users::UpdateService.new(user, user: user).execute(validate: false)
1312
    user
1313 1314 1315
  ensure
    Gitlab::ExclusiveLease.cancel(lease_key, uuid)
  end
gitlabhq committed
1316
end