user.rb 36.7 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
  include CreatedAtFilterable
16

17 18
  DEFAULT_NOTIFICATION_LEVEL = :participating

19 20
  ignore_column :authorized_projects_populated

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

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

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

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

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

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

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

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

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

61
  attr_accessor :force_random_password
gitlabhq's avatar
gitlabhq committed
62

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

66 67 68 69
  #
  # Relations
  #

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

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

81 82 83 84 85
  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
86 87

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

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

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

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

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

145
  validate :namespace_uniq, if: :username_changed?
146 147
  validate :namespace_move_dir_allowed, if: :username_changed?

148
  validate :avatar_type, if: ->(user) { user.avatar.present? && user.avatar_changed? }
149 150 151
  validate :unique_email, if: :email_changed?
  validate :owns_notification_email, if: :notification_email_changed?
  validate :owns_public_email, if: :public_email_changed?
152
  validate :signup_domain_valid?, on: :create, if: ->(user) { !user.created_by_id }
153
  validates :avatar, file_size: { maximum: 200.kilobytes.to_i }
154

155
  before_validation :sanitize_attrs
156 157
  before_validation :set_notification_email, if: :email_changed?
  before_validation :set_public_email, if: :public_email_changed?
158

159
  after_update :update_emails_with_primary_email, if: :email_changed?
Alexis Reigel's avatar
Alexis Reigel committed
160
  before_save :ensure_authentication_token, :ensure_incoming_email_token
161
  before_save :ensure_user_rights_and_limits, if: :external_changed?
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
162
  after_save :ensure_namespace_correct
163
  after_commit :update_invalid_gpg_signatures, on: :update, if: -> { previous_changes.key?('email') }
164
  after_initialize :set_projects_limit
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
165 166
  after_destroy :post_destroy_hook

167
  # User's Layout preference
168
  enum layout: [:fixed, :fluid]
169

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

174
  # User's Project preference
175 176 177 178 179 180 181
  #
  # 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 }
182

Nihad Abbasov's avatar
Nihad Abbasov committed
183
  alias_attribute :private_token, :authentication_token
184

185
  delegate :path, to: :namespace, allow_nil: true, prefix: true
186

187 188 189
  state_machine :state, initial: :active do
    event :block do
      transition active: :blocked
190
      transition ldap_blocked: :blocked
191 192
    end

193 194 195 196
    event :ldap_block do
      transition active: :ldap_blocked
    end

197 198
    event :activate do
      transition blocked: :active
199
      transition ldap_blocked: :active
200
    end
201 202 203 204 205

    state :blocked, :ldap_blocked do
      def blocked?
        true
      end
206 207 208 209 210 211 212 213 214

      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
215
    end
216 217
  end

218
  mount_uploader :avatar, AvatarUploader
219
  has_many :uploads, as: :model, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
220

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

  def self.with_two_factor
232 233
    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])
234 235 236
  end

  def self.without_two_factor
237 238
    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)
239
  end
Andrey Kumanyaev's avatar
Andrey Kumanyaev committed
240

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

Valery Sizov's avatar
Valery Sizov committed
255 256
    def sort(method)
      case method.to_s
257 258
      when 'recent_sign_in' then order_recent_sign_in
      when 'oldest_sign_in' then order_oldest_sign_in
259 260
      else
        order_by(method)
Valery Sizov's avatar
Valery Sizov committed
261 262 263
      end
    end

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

      User.find_by_sql([sql, { email: email }]).first
276
    end
277

278
    def filter(filter_name)
Andrey Kumanyaev's avatar
Andrey Kumanyaev committed
279
      case filter_name
280
      when 'admins'
281
        admins
282
      when 'blocked'
283
        blocked
284
      when 'two_factor_disabled'
285
        without_two_factor
286
      when 'two_factor_enabled'
287
        with_two_factor
288
      when 'wop'
289
        without_projects
290
      when 'external'
291
        external
Andrey Kumanyaev's avatar
Andrey Kumanyaev committed
292
      else
293
        active
Andrey Kumanyaev's avatar
Andrey Kumanyaev committed
294
      end
295 296
    end

297 298 299 300 301 302 303
    # 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.
304
    def search(query)
305
      table   = arel_table
306 307
      pattern = "%#{query}%"

308 309 310 311 312 313 314 315 316
      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

317
      where(
318 319 320
        table[:name].matches(pattern)
          .or(table[:email].matches(pattern))
          .or(table[:username].matches(pattern))
321
      ).reorder(order % { query: ActiveRecord::Base.connection.quote(query) }, :name)
Andrey Kumanyaev's avatar
Andrey Kumanyaev committed
322
    end
323

324 325 326 327 328 329 330 331 332 333 334
    # 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(
335 336 337 338
        table[:name].matches(pattern)
          .or(table[:email].matches(pattern))
          .or(table[:username].matches(pattern))
          .or(table[:id].in(matched_by_emails_user_ids))
339 340 341
      )
    end

342
    def by_login(login)
343 344 345 346 347 348 349
      return nil unless login

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

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

356
    def find_by_username!(username)
357
      iwhere(username: username).take!
358 359
    end

360
    def find_by_personal_access_token(token_string)
361 362
      return unless token_string

363
      PersonalAccessTokensFinder.new(state: 'active').find_by(token: token_string)&.user
364 365
    end

366 367 368 369 370
    # 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

371
    def find_by_full_path(path, follow_redirects: false)
372 373
      namespace = Namespace.for_user.find_by_full_path(path, follow_redirects: follow_redirects)
      namespace&.owner
374 375
    end

376 377 378
    def reference_prefix
      '@'
    end
379 380 381 382

    # Pattern used to extract `@user` user references from text
    def reference_pattern
      %r{
383
        (?<!\w)
384
        #{Regexp.escape(reference_prefix)}
385
        (?<user>#{Gitlab::PathRegex::FULL_NAMESPACE_FORMAT_REGEX})
386 387
      }x
    end
388 389 390 391

    # Return (create if necessary) the ghost user. The ghost user
    # owns records previously belonging to deleted users.
    def ghost
392 393
      email = 'ghost%s@example.com'
      unique_internal(where(ghost: true), 'ghost', email) do |u|
394 395
        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'
396
        u.notification_email = email
397
      end
398
    end
vsizov's avatar
vsizov committed
399
  end
randx's avatar
randx committed
400

Michael Kozono's avatar
Michael Kozono committed
401 402 403 404
  def full_path
    username
  end

405 406 407 408
  def self.internal_attributes
    [:ghost]
  end

409
  def internal?
410 411 412 413 414 415 416 417
    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
418
    where(Hash[internal_attributes.zip([[false, nil]] * internal_attributes.size)])
419 420
  end

421 422 423
  #
  # Instance methods
  #
424 425 426 427 428

  def to_param
    username
  end

429
  def to_reference(_from_project = nil, target_project: nil, full: nil)
430 431 432
    "#{self.class.reference_prefix}#{username}"
  end

433 434
  def skip_confirmation=(bool)
    skip_confirmation! if bool
randx's avatar
randx committed
435
  end
436

437
  def generate_reset_token
438
    @reset_token, enc = Devise.token_generator.generate(self.class, :reset_password_token)
439 440 441 442

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

443
    @reset_token
444 445
  end

446 447 448 449
  def recently_sent_password_reset?
    reset_password_sent_at.present? && reset_password_sent_at >= 1.minute.ago
  end

450
  def disable_two_factor!
451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468
    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?
469
    otp_required_for_login?
470 471 472
  end

  def two_factor_u2f_enabled?
473
    u2f_registrations.exists?
474 475
  end

476
  def namespace_uniq
477
    # Return early if username already failed the first uniqueness validation
478
    return if errors.key?(:username) &&
479
        errors[:username].include?('has already been taken')
480

481 482 483
    existing_namespace = Namespace.by_path(username)
    if existing_namespace && existing_namespace != namespace
      errors.add(:username, 'has already been taken')
484 485
    end
  end
486

487 488 489 490 491 492
  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

493
  def avatar_type
494 495
    unless avatar.image?
      errors.add :avatar, "only images allowed"
496 497 498
    end
  end

499
  def unique_email
500 501
    if !emails.exists?(email: email) && Email.exists?(email: email)
      errors.add(:email, 'has already been taken')
502
    end
503 504
  end

505
  def owns_notification_email
506
    return if temp_oauth_email?
507

508
    errors.add(:notification_email, "is not an email you own") unless all_emails.include?(notification_email)
509 510
  end

511
  def owns_public_email
512
    return if public_email.blank?
513

514
    errors.add(:public_email, "is not an email you own") unless all_emails.include?(public_email)
515 516 517
  end

  def update_emails_with_primary_email
518
    primary_email_record = emails.find_by(email: email)
519
    if primary_email_record
James Lopez's avatar
James Lopez committed
520 521
      Emails::DestroyService.new(self, email: email).execute
      Emails::CreateService.new(self, email: email_was).execute
522 523 524
    end
  end

525
  def update_invalid_gpg_signatures
526
    gpg_keys.each(&:update_invalid_gpg_signatures)
527 528
  end

529 530
  # Returns the groups a user has access to
  def authorized_groups
531 532
    union = Gitlab::SQL::Union
      .new([groups.select(:id), authorized_projects.select(:namespace_id)])
533

534
    Group.where("namespaces.id IN (#{union.to_sql})") # rubocop:disable GitlabSecurity/SqlInjection
535 536
  end

537 538
  # Returns a relation of groups the user has access to, including their parent
  # and child groups (recursively).
539
  def all_expanded_groups
540
    Gitlab::GroupHierarchy.new(groups).all_groups
541 542 543 544 545 546
  end

  def expanded_groups_requiring_two_factor_authentication
    all_expanded_groups.where(require_two_factor_authentication: true)
  end

547
  def refresh_authorized_projects
548 549 550 551
    Users::RefreshAuthorizedProjectsService.new(self).execute
  end

  def remove_project_authorizations(project_ids)
552
    project_authorizations.where(project_id: project_ids).delete_all
553 554
  end

555
  def authorized_projects(min_access_level = nil)
556 557
    # We're overriding an association, so explicitly call super with no
    # arguments or it would be passed as `force_reload` to the association
558
    projects = super()
559 560

    if min_access_level
561 562
      projects = projects
        .where('project_authorizations.access_level >= ?', min_access_level)
563
    end
564 565 566 567 568 569

    projects
  end

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

572 573 574 575 576 577 578 579 580 581
  # 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

582
  def owned_projects
583
    @owned_projects ||=
584 585
      Project.where('namespace_id IN (?) OR namespace_id = ?',
                    owned_groups.select(:id), namespace.id).joins(:namespace)
586 587
  end

588 589 590 591
  # 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
592
    authorized_projects(Gitlab::Access::REPORTER).non_archived.with_issues_enabled
593 594
  end

Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
595
  def require_ssh_key?
596
    keys.count == 0 && Gitlab::ProtocolAccess.allowed?('ssh')
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
597 598
  end

599 600
  def require_password_creation?
    password_automatically_set? && allow_password_authentication?
601 602
  end

603 604
  def require_personal_access_token_creation_for_git_auth?
    return false if allow_password_authentication? || ldap_user?
605 606

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

609 610 611 612
  def allow_password_authentication?
    !ldap_user? && current_application_settings.password_authentication_enabled?
  end

613
  def can_change_username?
614
    gitlab_config.username_changing_enabled
615 616
  end

Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
617
  def can_create_project?
618
    projects_limit_left > 0
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
619 620 621
  end

  def can_create_group?
622
    can?(:create_group)
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
623 624
  end

625 626 627 628
  def can_select_namespace?
    several_namespaces? || admin
  end

629
  def can?(action, subject = :global)
630
    Ability.allowed?(self, action, subject)
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
631 632 633 634 635 636
  end

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

637
  def projects_limit_left
638 639 640 641 642
    projects_limit - personal_projects_count
  end

  def personal_projects_count
    @personal_projects_count ||= personal_projects.count
643 644
  end

Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
645 646
  def projects_limit_percent
    return 100 if projects_limit.zero?
647
    (personal_projects.count.to_f / projects_limit) * 100
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
648 649
  end

650
  def recent_push(project_ids = nil)
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
651 652
    # Get push events not earlier than 2 hours ago
    events = recent_events.code_push.where("created_at > ?", Time.now - 2.hours)
653
    events = events.where(project_id: project_ids) if project_ids
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
654

655
    # Use the latest event that has not been pushed or merged recently
656 657 658 659 660 661 662 663
    events.includes(:project).recent.find do |event|
      next unless event.project.repository.branch_exists?(event.branch_name)

      merge_requests = MergeRequest.where("created_at >= ?", event.created_at)
        .where(source_project_id: event.project.id,
               source_branch: event.branch_name)

      merge_requests.empty?
664
    end
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
665 666 667 668 669 670 671
  end

  def projects_sorted_by_activity
    authorized_projects.sorted_by_activity
  end

  def several_namespaces?
672
    owned_groups.any? || masters_groups.any?
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
673 674 675 676 677
  end

  def namespace_id
    namespace.try :id
  end
678

679 680 681
  def name_with_username
    "#{name} (#{username})"
  end
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
682

683
  def already_forked?(project)
684 685 686
    !!fork_of(project)
  end

687
  def fork_of(project)
688 689 690 691
    links = ForkedProjectLink.where(
      forked_from_project_id: project,
      forked_to_project_id: personal_projects.unscope(:order)
    )
692 693 694 695 696 697
    if links.any?
      links.first.forked_to_project
    else
      nil
    end
  end
698 699

  def ldap_user?
700 701 702 703 704
    identities.exists?(["provider LIKE ? AND extern_uid IS NOT NULL", "ldap%"])
  end

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

707
  def project_deploy_keys
708
    DeployKey.unscoped.in_projects(authorized_projects.pluck(:id)).distinct(:id)
709 710
  end

711
  def accessible_deploy_keys
712 713 714 715 716
    @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
717
  end
718 719

  def created_by
skv's avatar
skv committed
720
    User.find_by(id: created_by_id) if created_by_id
721
  end
722 723

  def sanitize_attrs
724 725 726
    %i[skype linkedin twitter].each do |attr|
      value = self[attr]
      self[attr] = Sanitize.clean(value) if value.present?
727 728
    end
  end
729

730
  def set_notification_email
731 732
    if notification_email.blank? || !all_emails.include?(notification_email)
      self.notification_email = email
733 734 735
    end
  end

736
  def set_public_email
737
    if public_email.blank? || !all_emails.include?(public_email)
738
      self.public_email = ''
739 740 741
    end
  end

742
  def update_secondary_emails!
743 744 745
    set_notification_email
    set_public_email
    save if notification_email_changed? || public_email_changed?
746 747
  end

748
  def set_projects_limit
749 750 751
    # `User.select(:id)` raises
    # `ActiveModel::MissingAttributeError: missing attribute: projects_limit`
    # without this safeguard!
752
    return unless has_attribute?(:projects_limit)
753

754
    connection_default_value_defined = new_record? && !projects_limit_changed?
755
    return unless projects_limit.nil? || connection_default_value_defined
756 757 758 759

    self.projects_limit = current_application_settings.default_projects_limit
  end

760
  def requires_ldap_check?
761 762 763
    if !Gitlab.config.ldap.enabled
      false
    elsif ldap_user?
764 765 766 767 768 769
      !last_credential_check_at || (last_credential_check_at + 1.hour) < Time.now
    else
      false
    end
  end

Jacob Vosmaer's avatar
Jacob Vosmaer committed
770 771 772 773 774 775 776
  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

777 778 779 780 781
  def solo_owned_groups
    @solo_owned_groups ||= owned_groups.select do |group|
      group.owners == [self]
    end
  end
782 783

  def with_defaults
784
    User.defaults.each do |k, v|
785
      public_send("#{k}=", v) # rubocop:disable GitlabSecurity/PublicSend
786
    end
787 788

    self
789
  end
790

791 792 793 794
  def can_leave_project?(project)
    project.namespace != namespace &&
      project.project_member(self)
  end
795

Jerome Dalbert's avatar
Jerome Dalbert committed
796
  def full_website_url
797
    return "http://#{website_url}" if website_url !~ /\Ahttps?:\/\//
Jerome Dalbert's avatar
Jerome Dalbert committed
798 799 800 801 802

    website_url
  end

  def short_website_url
803
    website_url.sub(/\Ahttps?:\/\//, '')
Jerome Dalbert's avatar
Jerome Dalbert committed
804
  end
GitLab's avatar
GitLab committed
805

806
  def all_ssh_keys
807
    keys.map(&:publishable_key)
808
  end
809 810

  def temp_oauth_email?
811
    email.start_with?('temp-email-for-oauth')
812 813
  end

814 815 816
  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
817
    avatar_path(args) || GravatarService.new.execute(email, size, scale, username: username)
818
  end
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
819

820
  def all_emails
821
    all_emails = []
822 823
    all_emails << email unless temp_oauth_email?
    all_emails.concat(emails.map(&:email))
824
    all_emails
825 826
  end

Kirill Zaitsev's avatar
Kirill Zaitsev committed
827 828 829 830
  def hook_attrs
    {
      name: name,
      username: username,
831
      avatar_url: avatar_url(only_path: false)
Kirill Zaitsev's avatar
Kirill Zaitsev committed
832 833 834
    }
  end

Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
835 836
  def ensure_namespace_correct
    # Ensure user has namespace
837
    create_namespace!(path: username, name: username) unless namespace
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
838

839 840
    if username_changed?
      namespace.update_attributes(path: username, name: username)
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
841 842 843 844
    end
  end

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

849 850 851 852 853
  def delete_async(deleted_by:, params: {})
    block if params[:hard_delete]
    DeleteUserWorker.perform_async(deleted_by.id, id, params)
  end

Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
854
  def notification_service
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
855 856 857
    NotificationService.new
  end

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

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

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

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

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

  def manageable_namespaces
884
    @manageable_namespaces ||= [namespace] + owned_groups + masters_groups
885
  end
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
886

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

Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
893
  def oauth_authorized_tokens
894
    Doorkeeper::AccessToken.where(resource_owner_id: id, revoked_at: nil)
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
895
  end
896

897 898 899 900 901 902 903 904 905
  # Returns the projects a user contributed to in the last year.
  #
  # This method relies on a subquery as this performs significantly better
  # compared to a JOIN when coupled with, for example,
  # `Project.visible_to_user`. That is, consider the following code:
  #
  #     some_user.contributed_projects.visible_to_user(other_user)
  #
  # If this method were to use a JOIN the resulting query would take roughly 200
906
  # ms on a database with a similar size to GitLab.com's database. On the other
907 908
  # hand, using a subquery means we can get the exact same data in about 40 ms.
  def contributed_projects
909 910 911 912 913
    events = Event.select(:project_id)
      .contributions.where(author_id: self)
      .where("created_at > ?", Time.now - 1.year)
      .uniq
      .reorder(nil)
914 915

    Project.where(id: events)
916
  end
917

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

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

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

935 936 937
  # Lazy load global notification setting
  # Initializes User setting with Participating level if setting not persisted
  def global_notification_setting
938 939 940 941 942 943
    return @global_notification_setting if defined?(@global_notification_setting)

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

    @global_notification_setting
944 945
  end

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

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

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

963
  def invalidate_cache_counts
964 965 966 967 968
    invalidate_issue_cache_counts
    invalidate_merge_request_cache_counts
  end

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

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

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

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

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

993 994 995 996 997 998 999 1000 1001 1002 1003 1004
  # This is copied from Devise::Models::Lockable#valid_for_authentication?, as our auth
  # flow means we don't call that automatically (and can't conveniently do so).
  #
  # See:
  #   <https://github.com/plataformatec/devise/blob/v4.0.0/lib/devise/models/lockable.rb#L92>
  #
  def increment_failed_attempts!
    self.failed_attempts ||= 0
    self.failed_attempts += 1
    if attempts_exceeded?
      lock_access! unless access_locked?
    else
1005
      Users::UpdateService.new(self).execute(validate: false)
1006 1007 1008
    end
  end

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

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

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

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

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

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

    save
  end

Alexis Reigel's avatar
Alexis Reigel committed
1039 1040 1041 1042 1043 1044 1045
  # each existing user needs to have an `rss_token`.
  # we do this on read since migrating all existing users is not a feasible
  # solution.
  def rss_token
    ensure_rss_token!
  end

1046 1047 1048 1049 1050 1051 1052 1053
  protected

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

1054 1055
  private

1056 1057 1058 1059 1060 1061 1062 1063
  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
1064 1065 1066

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

1071 1072 1073 1074 1075 1076 1077 1078 1079
  # 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

1080 1081 1082 1083 1084
  def ensure_user_rights_and_limits
    if external?
      self.can_create_group = false
      self.projects_limit   = 0
    else
1085
      self.can_create_group = gitlab_config.default_can_create_group
1086 1087
      self.projects_limit = current_application_settings.default_projects_limit
    end
Zeger-Jan van de Weg's avatar
Zeger-Jan van de Weg committed
1088
  end
1089

1090 1091 1092 1093 1094 1095
  def signup_domain_valid?
    valid = true
    error = nil

    if current_application_settings.domain_blacklist_enabled?
      blocked_domains = current_application_settings.domain_blacklist
1096
      if domain_matches?(blocked_domains, email)
1097 1098 1099 1100 1101
        error = 'is not from an allowed domain.'
        valid = false
      end
    end

1102
    allowed_domains = current_application_settings.domain_whitelist
1103
    unless allowed_domains.blank?
1104
      if domain_matches?(allowed_domains, email)
1105 1106
        valid = true
      else
1107
        error = "domain is not authorized for sign-up"
1108 1109 1110 1111
        valid = false
      end
    end

1112
    errors.add(:email, error) unless valid
1113 1114 1115

    valid
  end
1116

1117
  def domain_matches?(email_domains, email)
1118 1119 1120 1121 1122 1123 1124
    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
1125 1126 1127 1128

  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.
1129
      SecureRandom.hex.to_i(16).to_s(36)
1130 1131 1132 1133
    else
      super
    end
  end
1134

1135 1136 1137 1138 1139 1140
  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
1141
    # exclusive lease to ensure than this block is never run concurrently.
1142
    lease_key = "user:unique_internal:#{username}"
1143 1144 1145 1146 1147 1148 1149 1150
    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

1151
    # Recheck if the user is already present. One might have been
1152 1153
    # added between the time we last checked (first line of this method)
    # and the time we acquired the lock.
1154 1155
    existing_user = uncached { scope.first }
    return existing_user if existing_user.present?
1156 1157 1158

    uniquify = Uniquify.new

1159
    username = uniquify.string(username) { |s| User.find_by_username(s) }
1160

1161
    email = uniquify.string(-> (n) { Kernel.sprintf(email_pattern, n) }) do |s|
1162 1163 1164
      User.find_by_email(s)
    end

1165
    user = scope.build(
1166 1167 1168
      username: username,
      email: email,
      &creation_block
1169
    )
James Lopez's avatar
James Lopez committed
1170

1171
    Users::UpdateService.new(user).execute(validate: false)
1172
    user
1173 1174 1175
  ensure
    Gitlab::ExclusiveLease.cancel(lease_key, uuid)
  end
gitlabhq's avatar
gitlabhq committed
1176
end