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

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

  include Gitlab::ConfigHelper
7
  include Gitlab::CurrentSettings
8
  include Avatarable
9 10
  include Referable
  include Sortable
11
  include CaseSensitivity
12
  include TokenAuthenticatable
13
  include IgnorableColumn
14
  include FeatureGate
15

16 17
  DEFAULT_NOTIFICATION_LEVEL = :participating

18 19
  ignore_column :authorized_projects_populated

20
  add_authentication_token_field :authentication_token
21
  add_authentication_token_field :incoming_email_token
22
  add_authentication_token_field :rss_token
23

24
  default_value_for :admin, false
25
  default_value_for(:external) { current_application_settings.user_default_external }
26
  default_value_for :can_create_group, gitlab_config.default_can_create_group
27 28
  default_value_for :can_create_team, false
  default_value_for :hide_no_ssh_key, false
29
  default_value_for :hide_no_password, false
30
  default_value_for :project_view, :files
31
  default_value_for :notified_of_own_activity, false
32
  default_value_for :preferred_language, I18n.default_locale
33

34
  attr_encrypted :otp_secret,
35
    key:       Gitlab::Application.secrets.otp_key_base,
36
    mode:      :per_attribute_iv_and_salt,
37
    insecure_mode: true,
38 39
    algorithm: 'aes-256-cbc'

40
  devise :two_factor_authenticatable,
41
         otp_secret_encryption_key: Gitlab::Application.secrets.otp_key_base
42

43
  devise :two_factor_backupable, otp_number_of_backup_codes: 10
44
  serialize :otp_backup_codes, JSON # rubocop:disable Cop/ActiveRecordSerialize
45

46
  devise :lockable, :recoverable, :rememberable, :trackable,
47
    :validatable, :omniauthable, :confirmable, :registerable
gitlabhq's avatar
gitlabhq committed
48

49 50
  # Override Devise::Models::Trackable#update_tracked_fields!
  # to limit database writes to at most once every hour
51
  def update_tracked_fields!(request)
52 53
    update_tracked_fields(request)

54 55 56
    lease = Gitlab::ExclusiveLease.new("user_update_tracked_fields:#{id}", timeout: 1.hour.to_i)
    return unless lease.try_obtain

57
    Users::UpdateService.new(self).execute(validate: false)
58 59
  end

60
  attr_accessor :force_random_password
gitlabhq's avatar
gitlabhq committed
61

62 63 64
  # Virtual attribute for authenticating by either username or email
  attr_accessor :login

65 66 67 68
  #
  # Relations
  #

69
  # Namespace for personal projects
70
  has_one :namespace, -> { where type: nil }, dependent: :destroy, foreign_key: :owner_id, autosave: true # rubocop:disable Cop/ActiveRecordDependent
71 72

  # Profile
73 74 75
  has_many :keys, -> do
    type = Key.arel_table[:type]
    where(type.not_eq('DeployKey').or(type.eq(nil)))
76 77
  end, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
  has_many :deploy_keys, -> { where(type: 'DeployKey') }, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
78

79 80 81 82 83
  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
84 85

  # Groups
86 87
  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
88
  has_many :groups, through: :group_members
89 90
  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
91

92
  # Projects
93 94
  has_many :groups_projects,          through: :groups, source: :projects
  has_many :personal_projects,        through: :namespace, source: :projects
95
  has_many :project_members, -> { where(requested_at: nil) }, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
96
  has_many :projects,                 through: :project_members
97
  has_many :created_projects,         foreign_key: :creator_id, class_name: 'Project'
98
  has_many :users_star_projects, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
Ciro Santilli's avatar
Ciro Santilli committed
99
  has_many :starred_projects, through: :users_star_projects, source: :project
100
  has_many :project_authorizations
101
  has_many :authorized_projects, through: :project_authorizations, source: :project
102

103 104 105 106 107 108
  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
109
  has_many :recent_events, -> { order "id DESC" }, foreign_key: :author_id,   class_name: "Event"
110 111 112 113 114 115 116 117 118 119
  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
120

121 122
  has_many :issue_assignees
  has_many :assigned_issues, class_name: "Issue", through: :issue_assignees, source: :issue
123
  has_many :assigned_merge_requests,  dependent: :nullify, foreign_key: :assignee_id, class_name: "MergeRequest" # rubocop:disable Cop/ActiveRecordDependent
124

125 126 127
  #
  # Validations
  #
128
  # Note: devise :validatable above adds validations for :email and :password
Cyril's avatar
Cyril committed
129
  validates :name, presence: true
Douwe Maan's avatar
Douwe Maan committed
130
  validates :email, confirmation: true
131 132
  validates :notification_email, presence: true
  validates :notification_email, email: true, if: ->(user) { user.notification_email != user.email }
133
  validates :public_email, presence: true, uniqueness: true, email: true, allow_blank: true
134
  validates :bio, length: { maximum: 255 }, allow_blank: true
135 136 137
  validates :projects_limit,
    presence: true,
    numericality: { greater_than_or_equal_to: 0, less_than_or_equal_to: Gitlab::Database::MAX_INT_VALUE }
138
  validates :username,
139
    dynamic_path: true,
140
    presence: true,
141
    uniqueness: { case_sensitive: false }
142

143
  validate :namespace_uniq, if: :username_changed?
144
  validate :avatar_type, if: ->(user) { user.avatar.present? && user.avatar_changed? }
145 146 147
  validate :unique_email, if: :email_changed?
  validate :owns_notification_email, if: :notification_email_changed?
  validate :owns_public_email, if: :public_email_changed?
148
  validate :signup_domain_valid?, on: :create, if: ->(user) { !user.created_by_id }
149
  validates :avatar, file_size: { maximum: 200.kilobytes.to_i }
150

151
  before_validation :sanitize_attrs
152 153
  before_validation :set_notification_email, if: :email_changed?
  before_validation :set_public_email, if: :public_email_changed?
154

155
  after_update :update_emails_with_primary_email, if: :email_changed?
Alexis Reigel's avatar
Alexis Reigel committed
156
  before_save :ensure_authentication_token, :ensure_incoming_email_token
157
  before_save :ensure_user_rights_and_limits, if: :external_changed?
158
  after_save :ensure_namespace_correct
159
  after_initialize :set_projects_limit
160 161
  after_destroy :post_destroy_hook

162
  # User's Layout preference
163
  enum layout: [:fixed, :fluid]
164

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

169
  # User's Project preference
170 171 172 173 174 175 176
  #
  # 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 }
177

178
  alias_attribute :private_token, :authentication_token
179

180
  delegate :path, to: :namespace, allow_nil: true, prefix: true
181

182 183 184
  state_machine :state, initial: :active do
    event :block do
      transition active: :blocked
185
      transition ldap_blocked: :blocked
186 187
    end

188 189 190 191
    event :ldap_block do
      transition active: :ldap_blocked
    end

192 193
    event :activate do
      transition blocked: :active
194
      transition ldap_blocked: :active
195
    end
196 197 198 199 200

    state :blocked, :ldap_blocked do
      def blocked?
        true
      end
201 202 203 204 205 206 207 208 209

      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
210
    end
211 212
  end

213
  mount_uploader :avatar, AvatarUploader
214
  has_many :uploads, as: :model, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
215

Andrey Kumanyaev's avatar
Andrey Kumanyaev committed
216
  # Scopes
217
  scope :admins, -> { where(admin: true) }
218
  scope :blocked, -> { with_states(:blocked, :ldap_blocked) }
219
  scope :external, -> { where(external: true) }
James Lopez's avatar
James Lopez committed
220
  scope :active, -> { with_state(:active).non_internal }
221
  scope :without_projects, -> { where('id NOT IN (SELECT DISTINCT(user_id) FROM members WHERE user_id IS NOT NULL AND requested_at IS NULL)') }
222
  scope :todo_authors, ->(user_id, state) { where(id: Todo.where(user_id: user_id, state: state).select(:author_id)) }
223 224
  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')) }
225 226

  def self.with_two_factor
227 228
    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])
229 230 231
  end

  def self.without_two_factor
232 233
    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)
234
  end
Andrey Kumanyaev's avatar
Andrey Kumanyaev committed
235

236 237 238
  #
  # Class methods
  #
Andrey Kumanyaev's avatar
Andrey Kumanyaev committed
239
  class << self
240
    # Devise method overridden to allow sign in with email or username
241 242 243
    def find_for_database_authentication(warden_conditions)
      conditions = warden_conditions.dup
      if login = conditions.delete(:login)
Gabriel Mazetto's avatar
Gabriel Mazetto committed
244
        where(conditions).find_by("lower(username) = :value OR lower(email) = :value", value: login.downcase)
245
      else
Gabriel Mazetto's avatar
Gabriel Mazetto committed
246
        find_by(conditions)
247 248
      end
    end
249

Valery Sizov's avatar
Valery Sizov committed
250 251
    def sort(method)
      case method.to_s
252 253
      when 'recent_sign_in' then order_recent_sign_in
      when 'oldest_sign_in' then order_oldest_sign_in
254 255
      else
        order_by(method)
Valery Sizov's avatar
Valery Sizov committed
256 257 258
      end
    end

259 260
    # Find a User by their primary email or any associated secondary email
    def find_by_any_email(email)
261 262 263 264 265 266 267
      sql = 'SELECT *
      FROM users
      WHERE id IN (
        SELECT id FROM users WHERE email = :email
        UNION
        SELECT emails.user_id FROM emails WHERE email = :email
      )
268 269 270
      LIMIT 1;'

      User.find_by_sql([sql, { email: email }]).first
271
    end
272

273
    def filter(filter_name)
Andrey Kumanyaev's avatar
Andrey Kumanyaev committed
274
      case filter_name
275
      when 'admins'
276
        admins
277
      when 'blocked'
278
        blocked
279
      when 'two_factor_disabled'
280
        without_two_factor
281
      when 'two_factor_enabled'
282
        with_two_factor
283
      when 'wop'
284
        without_projects
285
      when 'external'
286
        external
Andrey Kumanyaev's avatar
Andrey Kumanyaev committed
287
      else
288
        active
Andrey Kumanyaev's avatar
Andrey Kumanyaev committed
289
      end
290 291
    end

292 293 294 295 296 297 298
    # 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.
299
    def search(query)
300
      table   = arel_table
301 302
      pattern = "%#{query}%"

303 304 305 306 307 308 309 310 311
      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

312
      where(
313 314 315
        table[:name].matches(pattern)
          .or(table[:email].matches(pattern))
          .or(table[:username].matches(pattern))
316
      ).reorder(order % { query: ActiveRecord::Base.connection.quote(query) }, id: :desc)
Andrey Kumanyaev's avatar
Andrey Kumanyaev committed
317
    end
318

319 320 321 322 323 324 325 326 327 328 329
    # 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(
330 331 332 333
        table[:name].matches(pattern)
          .or(table[:email].matches(pattern))
          .or(table[:username].matches(pattern))
          .or(table[:id].in(matched_by_emails_user_ids))
334 335 336
      )
    end

337
    def by_login(login)
338 339 340 341 342 343 344
      return nil unless login

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

347 348 349 350
    def find_by_username(username)
      iwhere(username: username).take
    end

351
    def find_by_username!(username)
352
      iwhere(username: username).take!
353 354
    end

355
    def find_by_personal_access_token(token_string)
356 357
      return unless token_string

358
      PersonalAccessTokensFinder.new(state: 'active').find_by(token: token_string)&.user
359 360
    end

361 362 363 364 365
    # Returns a user for the given SSH key.
    def find_by_ssh_key_id(key_id)
      find_by(id: Key.unscoped.select(:user_id).where(id: key_id))
    end

366
    def find_by_full_path(path, follow_redirects: false)
367 368
      namespace = Namespace.for_user.find_by_full_path(path, follow_redirects: follow_redirects)
      namespace&.owner
369 370
    end

371 372 373
    def reference_prefix
      '@'
    end
374 375 376 377

    # Pattern used to extract `@user` user references from text
    def reference_pattern
      %r{
378
        (?<!\w)
379
        #{Regexp.escape(reference_prefix)}
380
        (?<user>#{Gitlab::PathRegex::FULL_NAMESPACE_FORMAT_REGEX})
381 382
      }x
    end
383 384 385 386

    # Return (create if necessary) the ghost user. The ghost user
    # owns records previously belonging to deleted users.
    def ghost
387 388 389 390
      unique_internal(where(ghost: true), 'ghost', 'ghost%s@example.com') do |u|
        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'
      end
391
    end
vsizov's avatar
vsizov committed
392
  end
randx's avatar
randx committed
393

Michael Kozono's avatar
Michael Kozono committed
394 395 396 397
  def full_path
    username
  end

398 399 400 401
  def self.internal_attributes
    [:ghost]
  end

402
  def internal?
403 404 405 406 407 408 409 410
    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
411
    where(Hash[internal_attributes.zip([[false, nil]] * internal_attributes.size)])
412 413
  end

414 415 416
  #
  # Instance methods
  #
417 418 419 420 421

  def to_param
    username
  end

422
  def to_reference(_from_project = nil, target_project: nil, full: nil)
423 424 425
    "#{self.class.reference_prefix}#{username}"
  end

426 427
  def skip_confirmation=(bool)
    skip_confirmation! if bool
randx's avatar
randx committed
428
  end
429

430
  def generate_reset_token
431
    @reset_token, enc = Devise.token_generator.generate(self.class, :reset_password_token)
432 433 434 435

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

436
    @reset_token
437 438
  end

439 440 441 442
  def recently_sent_password_reset?
    reset_password_sent_at.present? && reset_password_sent_at >= 1.minute.ago
  end

443
  def disable_two_factor!
444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461
    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?
462
    otp_required_for_login?
463 464 465
  end

  def two_factor_u2f_enabled?
466
    u2f_registrations.exists?
467 468
  end

469
  def namespace_uniq
470
    # Return early if username already failed the first uniqueness validation
471
    return if errors.key?(:username) &&
472
        errors[:username].include?('has already been taken')
473

474 475 476
    existing_namespace = Namespace.by_path(username)
    if existing_namespace && existing_namespace != namespace
      errors.add(:username, 'has already been taken')
477 478
    end
  end
479

480
  def avatar_type
481 482
    unless avatar.image?
      errors.add :avatar, "only images allowed"
483 484 485
    end
  end

486
  def unique_email
487 488
    if !emails.exists?(email: email) && Email.exists?(email: email)
      errors.add(:email, 'has already been taken')
489
    end
490 491
  end

492
  def owns_notification_email
493
    return if temp_oauth_email?
494

495
    errors.add(:notification_email, "is not an email you own") unless all_emails.include?(notification_email)
496 497
  end

498
  def owns_public_email
499
    return if public_email.blank?
500

501
    errors.add(:public_email, "is not an email you own") unless all_emails.include?(public_email)
502 503 504
  end

  def update_emails_with_primary_email
505
    primary_email_record = emails.find_by(email: email)
506
    if primary_email_record
James Lopez's avatar
James Lopez committed
507 508
      Emails::DestroyService.new(self, email: email).execute
      Emails::CreateService.new(self, email: email_was).execute
509 510 511
    end
  end

512 513
  # Returns the groups a user has access to
  def authorized_groups
514 515
    union = Gitlab::SQL::Union
      .new([groups.select(:id), authorized_projects.select(:namespace_id)])
516

517
    Group.where("namespaces.id IN (#{union.to_sql})")
518 519
  end

520 521
  # Returns a relation of groups the user has access to, including their parent
  # and child groups (recursively).
522
  def all_expanded_groups
523
    Gitlab::GroupHierarchy.new(groups).all_groups
524 525 526 527 528 529
  end

  def expanded_groups_requiring_two_factor_authentication
    all_expanded_groups.where(require_two_factor_authentication: true)
  end

530
  def refresh_authorized_projects
531 532 533 534
    Users::RefreshAuthorizedProjectsService.new(self).execute
  end

  def remove_project_authorizations(project_ids)
535
    project_authorizations.where(project_id: project_ids).delete_all
536 537
  end

538
  def authorized_projects(min_access_level = nil)
539 540
    # We're overriding an association, so explicitly call super with no
    # arguments or it would be passed as `force_reload` to the association
541
    projects = super()
542 543

    if min_access_level
544 545
      projects = projects
        .where('project_authorizations.access_level >= ?', min_access_level)
546
    end
547 548 549 550 551 552

    projects
  end

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

555 556 557 558 559 560 561 562 563 564
  # 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

565
  def owned_projects
566
    @owned_projects ||=
567 568
      Project.where('namespace_id IN (?) OR namespace_id = ?',
                    owned_groups.select(:id), namespace.id).joins(:namespace)
569 570
  end

571 572 573 574
  # 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
575
    authorized_projects(Gitlab::Access::REPORTER).non_archived.with_issues_enabled
576 577
  end

Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
578
  def require_ssh_key?
579
    keys.count == 0 && Gitlab::ProtocolAccess.allowed?('ssh')
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
580 581
  end

582
  def require_password?
583 584 585 586 587 588 589
    password_automatically_set? && !ldap_user? && current_application_settings.signin_enabled?
  end

  def require_personal_access_token?
    return false if current_application_settings.signin_enabled? || ldap_user?

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

592
  def can_change_username?
593
    gitlab_config.username_changing_enabled
594 595
  end

Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
596
  def can_create_project?
597
    projects_limit_left > 0
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
598 599 600
  end

  def can_create_group?
601
    can?(:create_group)
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
602 603
  end

604 605 606 607
  def can_select_namespace?
    several_namespaces? || admin
  end

608
  def can?(action, subject = :global)
609
    Ability.allowed?(self, action, subject)
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
610 611 612 613 614 615
  end

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

616
  def projects_limit_left
617
    projects_limit - personal_projects.count
618 619
  end

Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
620 621
  def projects_limit_percent
    return 100 if projects_limit.zero?
622
    (personal_projects.count.to_f / projects_limit) * 100
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
623 624
  end

625
  def recent_push(project_ids = nil)
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
626 627
    # Get push events not earlier than 2 hours ago
    events = recent_events.code_push.where("created_at > ?", Time.now - 2.hours)
628
    events = events.where(project_id: project_ids) if project_ids
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
629

630 631 632 633 634
    # Use the latest event that has not been pushed or merged recently
    events.recent.find do |event|
      project = Project.find_by_id(event.project_id)
      next unless project

635
      if project.repository.branch_exists?(event.branch_name)
636 637 638
        merge_requests = MergeRequest.where("created_at >= ?", event.created_at)
          .where(source_project_id: project.id,
                 source_branch: event.branch_name)
639 640 641
        merge_requests.empty?
      end
    end
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
642 643 644 645 646 647 648
  end

  def projects_sorted_by_activity
    authorized_projects.sorted_by_activity
  end

  def several_namespaces?
649
    owned_groups.any? || masters_groups.any?
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
650 651 652 653 654
  end

  def namespace_id
    namespace.try :id
  end
655

656 657 658
  def name_with_username
    "#{name} (#{username})"
  end
659

660
  def already_forked?(project)
661 662 663
    !!fork_of(project)
  end

664
  def fork_of(project)
665 666 667 668
    links = ForkedProjectLink.where(
      forked_from_project_id: project,
      forked_to_project_id: personal_projects.unscope(:order)
    )
669 670 671 672 673 674
    if links.any?
      links.first.forked_to_project
    else
      nil
    end
  end
675 676

  def ldap_user?
677 678 679 680 681
    identities.exists?(["provider LIKE ? AND extern_uid IS NOT NULL", "ldap%"])
  end

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

684
  def project_deploy_keys
685
    DeployKey.unscoped.in_projects(authorized_projects.pluck(:id)).distinct(:id)
686 687
  end

688
  def accessible_deploy_keys
689 690 691 692 693
    @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
694
  end
695 696

  def created_by
skv's avatar
skv committed
697
    User.find_by(id: created_by_id) if created_by_id
698
  end
699 700

  def sanitize_attrs
701 702 703
    %w[name username skype linkedin twitter].each do |attr|
      value = public_send(attr)
      public_send("#{attr}=", Sanitize.clean(value)) if value.present?
704 705
    end
  end
706

707
  def set_notification_email
708 709
    if notification_email.blank? || !all_emails.include?(notification_email)
      self.notification_email = email
710 711 712
    end
  end

713
  def set_public_email
714
    if public_email.blank? || !all_emails.include?(public_email)
715
      self.public_email = ''
716 717 718
    end
  end

719
  def update_secondary_emails!
720 721 722
    set_notification_email
    set_public_email
    save if notification_email_changed? || public_email_changed?
723 724
  end

725
  def set_projects_limit
726 727 728
    # `User.select(:id)` raises
    # `ActiveModel::MissingAttributeError: missing attribute: projects_limit`
    # without this safeguard!
729
    return unless has_attribute?(:projects_limit)
730

731
    connection_default_value_defined = new_record? && !projects_limit_changed?
732
    return unless projects_limit.nil? || connection_default_value_defined
733 734 735 736

    self.projects_limit = current_application_settings.default_projects_limit
  end

737
  def requires_ldap_check?
738 739 740
    if !Gitlab.config.ldap.enabled
      false
    elsif ldap_user?
741 742 743 744 745 746
      !last_credential_check_at || (last_credential_check_at + 1.hour) < Time.now
    else
      false
    end
  end

Jacob Vosmaer's avatar
Jacob Vosmaer committed
747 748 749 750 751 752 753
  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

754 755 756 757 758
  def solo_owned_groups
    @solo_owned_groups ||= owned_groups.select do |group|
      group.owners == [self]
    end
  end
759 760

  def with_defaults
761
    User.defaults.each do |k, v|
762
      public_send("#{k}=", v)
763
    end
764 765

    self
766
  end
767

768 769 770 771
  def can_leave_project?(project)
    project.namespace != namespace &&
      project.project_member(self)
  end
772

Jerome Dalbert's avatar
Jerome Dalbert committed
773
  def full_website_url
774
    return "http://#{website_url}" if website_url !~ /\Ahttps?:\/\//
Jerome Dalbert's avatar
Jerome Dalbert committed
775 776 777 778 779

    website_url
  end

  def short_website_url
780
    website_url.sub(/\Ahttps?:\/\//, '')
Jerome Dalbert's avatar
Jerome Dalbert committed
781
  end
GitLab's avatar
GitLab committed
782

783
  def all_ssh_keys
784
    keys.map(&:publishable_key)
785
  end
786 787

  def temp_oauth_email?
788
    email.start_with?('temp-email-for-oauth')
789 790
  end

791 792 793
  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
794
    avatar_path(args) || GravatarService.new.execute(email, size, scale, username: username)
795
  end
796

797
  def all_emails
798
    all_emails = []
799 800
    all_emails << email unless temp_oauth_email?
    all_emails.concat(emails.map(&:email))
801
    all_emails
802 803
  end

Kirill Zaitsev's avatar
Kirill Zaitsev committed
804 805 806 807 808 809 810 811
  def hook_attrs
    {
      name: name,
      username: username,
      avatar_url: avatar_url
    }
  end

812 813
  def ensure_namespace_correct
    # Ensure user has namespace
814
    create_namespace!(path: username, name: username) unless namespace
815

816 817
    if username_changed?
      namespace.update_attributes(path: username, name: username)
818 819 820 821
    end
  end

  def post_destroy_hook
822
    log_info("User \"#{name}\" (#{email})  was removed")
823 824 825
    system_hook_service.execute_hooks_for(self, :destroy)
  end

826 827 828 829 830
  def delete_async(deleted_by:, params: {})
    block if params[:hard_delete]
    DeleteUserWorker.perform_async(deleted_by.id, id, params)
  end

831
  def notification_service
832 833 834
    NotificationService.new
  end

835
  def log_info(message)
836 837 838 839 840 841
    Gitlab::AppLogger.info message
  end

  def system_hook_service
    SystemHooksService.new
  end
Ciro Santilli's avatar
Ciro Santilli committed
842 843

  def starred?(project)
844
    starred_projects.exists?(project.id)
Ciro Santilli's avatar
Ciro Santilli committed
845 846 847
  end

  def toggle_star(project)
848
    UsersStarProject.transaction do
849 850
      user_star_project = users_star_projects
          .where(project: project, user: self).lock(true).first
851 852 853 854 855 856

      if user_star_project
        user_star_project.destroy
      else
        UsersStarProject.create!(project: project, user: self)
      end
Ciro Santilli's avatar
Ciro Santilli committed
857 858
    end
  end
859 860

  def manageable_namespaces
861
    @manageable_namespaces ||= [namespace] + owned_groups + masters_groups
862
  end
863

864 865 866 867 868 869
  def namespaces
    namespace_ids = groups.pluck(:id)
    namespace_ids.push(namespace.id)
    Namespace.where(id: namespace_ids)
  end

870
  def oauth_authorized_tokens
871
    Doorkeeper::AccessToken.where(resource_owner_id: id, revoked_at: nil)
872
  end
873

874 875 876 877 878 879 880 881 882
  # 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
883
  # ms on a database with a similar size to GitLab.com's database. On the other
884 885
  # hand, using a subquery means we can get the exact same data in about 40 ms.
  def contributed_projects
886 887 888 889 890
    events = Event.select(:project_id)
      .contributions.where(author_id: self)
      .where("created_at > ?", Time.now - 1.year)
      .uniq
      .reorder(nil)
891 892

    Project.where(id: events)
893
  end
894

895 896 897
  def can_be_removed?
    !solo_owned_groups.present?
  end
898 899

  def ci_authorized_runners
900
    @ci_authorized_runners ||= begin
901 902 903
      runner_ids = Ci::RunnerProject
        .where("ci_runner_projects.project_id IN (#{ci_projects_union.to_sql})")
        .select(:runner_id)
904 905
      Ci::Runner.specific.where(id: runner_ids)
    end
906
  end
907

908 909 910 911
  def notification_settings_for(source)
    notification_settings.find_or_initialize_by(source: source)
  end

912 913 914
  # Lazy load global notification setting
  # Initializes User setting with Participating level if setting not persisted
  def global_notification_setting
915 916 917 918 919 920
    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
921 922
  end

923
  def assigned_open_merge_requests_count(force: false)
924
    Rails.cache.fetch(['users', id, 'assigned_open_merge_requests_count'], force: force, expires_in: 20.minutes) do
925
      MergeRequestsFinder.new(self, assignee_id: self.id, state: 'opened').execute.count
926 927 928
    end
  end

929
  def assigned_open_issues_count(force: false)
930
    Rails.cache.fetch(['users', id, 'assigned_open_issues_count'], force: force, expires_in: 20.minutes) do
931
      IssuesFinder.new(self, assignee_id: self.id, state: 'opened').execute.count
932
    end
933 934
  end

935
  def update_cache_counts
936
    assigned_open_merge_requests_count(force: true)
937 938 939
    assigned_open_issues_count(force: true)
  end

940
  def invalidate_cache_counts
941 942 943 944 945
    invalidate_issue_cache_counts
    invalidate_merge_request_cache_counts
  end

  def invalidate_issue_cache_counts
946 947 948
    Rails.cache.delete(['users', id, 'assigned_open_issues_count'])
  end

949 950 951 952
  def invalidate_merge_request_cache_counts
    Rails.cache.delete(['users', id, 'assigned_open_merge_requests_count'])
  end

953 954
  def todos_done_count(force: false)
    Rails.cache.fetch(['users', id, 'todos_done_count'], force: force) do
955
      TodosFinder.new(self, state: :done).execute.count
956 957 958 959 960
    end
  end

  def todos_pending_count(force: false)
    Rails.cache.fetch(['users', id, 'todos_pending_count'], force: force) do
961
      TodosFinder.new(self, state: :pending).execute.count
962 963 964 965 966 967 968 969
    end
  end

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

970 971 972 973 974 975 976 977 978 979 980 981
  # 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
982
      Users::UpdateService.new(self).execute(validate: false)
983 984 985
    end
  end

986 987 988 989 990 991 992 993 994
  def access_level
    if admin?
      :admin
    else
      :regular
    end
  end

  def access_level=(new_level)
Douwe Maan's avatar
Douwe Maan committed
995 996
    new_level = new_level.to_s
    return unless %w(admin regular).include?(new_level)
997

Douwe Maan's avatar
Douwe Maan committed
998 999
    self.admin = (new_level == 'admin')
  end
1000

1001 1002 1003 1004 1005 1006
  # Does the user have access to all private groups & projects?
  # Overridden in EE to also check auditor?
  def full_private_access?
    admin?
  end

1007
  def update_two_factor_requirement
1008
    periods = expanded_groups_requiring_two_factor_authentication.pluck(:two_factor_grace_period)
1009

1010
    self.require_two_factor_authentication_from_group = periods.any?
1011 1012 1013 1014 1015
    self.two_factor_grace_period = periods.min || User.column_defaults['two_factor_grace_period']

    save
  end

Alexis Reigel's avatar
Alexis Reigel committed
1016 1017 1018 1019 1020 1021 1022
  # 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

1023 1024 1025 1026 1027 1028 1029 1030
  protected

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

1031 1032
  private

1033 1034 1035 1036 1037 1038 1039 1040
  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
1041 1042 1043 1044 1045

  # Added according to https://github.com/plataformatec/devise/blob/7df57d5081f9884849ca15e4fde179ef164a575f/README.md#activejob-integration
  def send_devise_notification(notification, *args)
    devise_mailer.send(notification, self, *args).deliver_later
  end
Zeger-Jan van de Weg's avatar
Zeger-Jan van de Weg committed
1046

1047 1048 1049 1050 1051 1052 1053 1054 1055
  # 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

1056 1057 1058 1059 1060
  def ensure_user_rights_and_limits
    if external?
      self.can_create_group = false
      self.projects_limit   = 0
    else
1061
      self.can_create_group = gitlab_config.default_can_create_group
1062 1063
      self.projects_limit = current_application_settings.default_projects_limit
    end
Zeger-Jan van de Weg's avatar
Zeger-Jan van de Weg committed
1064
  end
1065

1066 1067 1068 1069 1070 1071
  def signup_domain_valid?
    valid = true
    error = nil

    if current_application_settings.domain_blacklist_enabled?
      blocked_domains = current_application_settings.domain_blacklist
1072
      if domain_matches?(blocked_domains, email)
1073 1074 1075 1076 1077
        error = 'is not from an allowed domain.'
        valid = false
      end
    end

1078
    allowed_domains = current_application_settings.domain_whitelist
1079
    unless allowed_domains.blank?
1080
      if domain_matches?(allowed_domains, email)
1081 1082
        valid = true
      else
1083
        error = "domain is not authorized for sign-up"
1084 1085 1086 1087
        valid = false
      end
    end

1088
    errors.add(:email, error) unless valid
1089 1090 1091

    valid
  end
1092

1093
  def domain_matches?(email_domains, email)
1094 1095 1096 1097 1098 1099 1100
    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
1101 1102 1103 1104

  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.
1105
      SecureRandom.hex.to_i(16).to_s(36)
1106 1107 1108 1109
    else
      super
    end
  end
1110

1111 1112 1113 1114 1115 1116
  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
1117
    # exclusive lease to ensure than this block is never run concurrently.
1118
    lease_key = "user:unique_internal:#{username}"
1119 1120 1121 1122 1123 1124 1125 1126
    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

1127
    # Recheck if the user is already present. One might have been
1128 1129
    # added between the time we last checked (first line of this method)
    # and the time we acquired the lock.
1130 1131
    existing_user = uncached { scope.first }
    return existing_user if existing_user.present?
1132 1133 1134

    uniquify = Uniquify.new

1135
    username = uniquify.string(username) { |s| User.find_by_username(s) }
1136

1137
    email = uniquify.string(-> (n) { Kernel.sprintf(email_pattern, n) }) do |s|
1138 1139 1140
      User.find_by_email(s)
    end

1141
    user = scope.build(
1142 1143 1144
      username: username,
      email: email,
      &creation_block
1145
    )
James Lopez's avatar
James Lopez committed
1146

1147
    Users::UpdateService.new(user).execute(validate: false)
1148
    user
1149 1150 1151
  ensure
    Gitlab::ExclusiveLease.cancel(lease_key, uuid)
  end
gitlabhq's avatar
gitlabhq committed
1152
end