Read about our upcoming Code of Conduct on this issue

user.rb 33.3 KB
Newer Older
Steven Thonus's avatar
Steven Thonus committed
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
13
  include TokenAuthenticatable

14
15
  DEFAULT_NOTIFICATION_LEVEL = :participating

16
  add_authentication_token_field :authentication_token
17
  add_authentication_token_field :incoming_email_token
18

19
  default_value_for :admin, false
20
  default_value_for(:external) { current_application_settings.user_default_external }
21
  default_value_for :can_create_group, gitlab_config.default_can_create_group
22
23
  default_value_for :can_create_team, false
  default_value_for :hide_no_ssh_key, false
24
  default_value_for :hide_no_password, false
25
  default_value_for :project_view, :files
26
  default_value_for :notified_of_own_activity, false
27
  default_value_for :preferred_language, I18n.default_locale
28

29
  attr_encrypted :otp_secret,
30
    key:       Gitlab::Application.secrets.otp_key_base,
31
    mode:      :per_attribute_iv_and_salt,
32
    insecure_mode: true,
33
34
    algorithm: 'aes-256-cbc'

35
  devise :two_factor_authenticatable,
36
         otp_secret_encryption_key: Gitlab::Application.secrets.otp_key_base
Robert Speicher's avatar
Robert Speicher committed
37

38
  devise :two_factor_backupable, otp_number_of_backup_codes: 10
Robert Speicher's avatar
Robert Speicher committed
39
40
  serialize :otp_backup_codes, JSON

41
  devise :lockable, :recoverable, :rememberable, :trackable,
42
    :validatable, :omniauthable, :confirmable, :registerable
gitlabhq's avatar
gitlabhq committed
43

44
45
  # Override Devise::Models::Trackable#update_tracked_fields!
  # to limit database writes to at most once every hour
46
  def update_tracked_fields!(request)
47
48
    update_tracked_fields(request)

49
50
51
    lease = Gitlab::ExclusiveLease.new("user_update_tracked_fields:#{id}", timeout: 1.hour.to_i)
    return unless lease.try_obtain

52
    save(validate: false)
53
54
  end

55
  attr_accessor :force_random_password
gitlabhq's avatar
gitlabhq committed
56

57
58
59
  # Virtual attribute for authenticating by either username or email
  attr_accessor :login

60
61
62
63
  #
  # Relations
  #

64
  # Namespace for personal projects
65
  has_one :namespace, -> { where type: nil }, dependent: :destroy, foreign_key: :owner_id
66
67

  # Profile
68
69
70
71
72
73
  has_many :keys, -> do
    type = Key.arel_table[:type]
    where(type.not_eq('DeployKey').or(type.eq(nil)))
  end, dependent: :destroy
  has_many :deploy_keys, -> { where(type: 'DeployKey') }, dependent: :destroy

74
  has_many :emails, dependent: :destroy
75
  has_many :personal_access_tokens, dependent: :destroy
76
  has_many :identities, dependent: :destroy, autosave: true
77
  has_many :u2f_registrations, dependent: :destroy
78
  has_many :chat_names, dependent: :destroy
79
80

  # Groups
81
  has_many :members, dependent: :destroy
82
  has_many :group_members, -> { where(requested_at: nil) }, dependent: :destroy, source: 'GroupMember'
83
  has_many :groups, through: :group_members
84
85
  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
86

87
  # Projects
88
89
  has_many :groups_projects,          through: :groups, source: :projects
  has_many :personal_projects,        through: :namespace, source: :projects
90
  has_many :project_members, -> { where(requested_at: nil) }, dependent: :destroy
91
  has_many :projects,                 through: :project_members
92
  has_many :created_projects,         foreign_key: :creator_id, class_name: 'Project'
Ciro Santilli's avatar
Ciro Santilli committed
93
94
  has_many :users_star_projects, dependent: :destroy
  has_many :starred_projects, through: :users_star_projects, source: :project
95
  has_many :project_authorizations
96
  has_many :authorized_projects, through: :project_authorizations, source: :project
97

98
  has_many :snippets,                 dependent: :destroy, foreign_key: :author_id
99
100
  has_many :notes,                    dependent: :destroy, foreign_key: :author_id
  has_many :merge_requests,           dependent: :destroy, foreign_key: :author_id
101
  has_many :events,                   dependent: :destroy, foreign_key: :author_id
102
  has_many :subscriptions,            dependent: :destroy
103
  has_many :recent_events, -> { order "id DESC" }, foreign_key: :author_id,   class_name: "Event"
Valery Sizov's avatar
Valery Sizov committed
104
  has_many :oauth_applications, class_name: 'Doorkeeper::Application', as: :owner, dependent: :destroy
105
  has_one  :abuse_report,             dependent: :destroy, foreign_key: :user_id
106
  has_many :reported_abuse_reports,   dependent: :destroy, foreign_key: :reporter_id, class_name: "AbuseReport"
107
  has_many :spam_logs,                dependent: :destroy
108
  has_many :builds,                   dependent: :nullify, class_name: 'Ci::Build'
109
  has_many :pipelines,                dependent: :nullify, class_name: 'Ci::Pipeline'
110
  has_many :todos,                    dependent: :destroy
111
  has_many :notification_settings,    dependent: :destroy
112
  has_many :award_emoji,              dependent: :destroy
113
  has_many :triggers,                 dependent: :destroy, class_name: 'Ci::Trigger', foreign_key: :owner_id
114

115
116
117
118
  has_many :issue_assignees
  has_many :assigned_issues, class_name: "Issue", through: :issue_assignees, source: :issue
  has_many :assigned_merge_requests,  dependent: :nullify, foreign_key: :assignee_id, class_name: "MergeRequest"

119
120
121
122
123
  # Issues that a user owns are expected to be moved to the "ghost" user before
  # the user is destroyed. If the user owns any issues during deletion, this
  # should be treated as an exceptional condition.
  has_many :issues,                   dependent: :restrict_with_exception, foreign_key: :author_id

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

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

150
  before_validation :sanitize_attrs
151
  before_validation :set_notification_email, if: ->(user) { user.email_changed? }
152
  before_validation :set_public_email, if: ->(user) { user.public_email_changed? }
153

154
  after_update :update_emails_with_primary_email, if: ->(user) { user.email_changed? }
155
  before_save :ensure_authentication_token, :ensure_incoming_email_token
Zeger-Jan van de Weg's avatar
Zeger-Jan van de Weg committed
156
  before_save :ensure_external_user_rights
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
157
  after_save :ensure_namespace_correct
158
  after_initialize :set_projects_limit
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
159
160
  after_destroy :post_destroy_hook

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

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

168
169
  # User's Project preference
  # Note: When adding an option, it MUST go on the end of the array.
170
  enum project_view: [:readme, :activity, :files]
171

Nihad Abbasov's avatar
Nihad Abbasov committed
172
  alias_attribute :private_token, :authentication_token
173

174
  delegate :path, to: :namespace, allow_nil: true, prefix: true
175

176
177
178
  state_machine :state, initial: :active do
    event :block do
      transition active: :blocked
179
      transition ldap_blocked: :blocked
180
181
    end

182
183
184
185
    event :ldap_block do
      transition active: :ldap_blocked
    end

186
187
    event :activate do
      transition blocked: :active
188
      transition ldap_blocked: :active
189
    end
190
191
192
193
194

    state :blocked, :ldap_blocked do
      def blocked?
        true
      end
195
196
197
198
199
200
201
202
203

      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
204
    end
205
206
  end

207
  mount_uploader :avatar, AvatarUploader
208
  has_many :uploads, as: :model, dependent: :destroy
Steven Thonus's avatar
Steven Thonus committed
209

Andrey Kumanyaev's avatar
Andrey Kumanyaev committed
210
  # Scopes
211
  scope :admins, -> { where(admin: true) }
212
  scope :blocked, -> { with_states(:blocked, :ldap_blocked) }
213
  scope :external, -> { where(external: true) }
James Lopez's avatar
James Lopez committed
214
  scope :active, -> { with_state(:active).non_internal }
skv's avatar
skv committed
215
  scope :not_in_project, ->(project) { project.users.present? ? where("id not in (:ids)", ids: project.users.map(&:id) ) : all }
216
  scope :without_projects, -> { where('id NOT IN (SELECT DISTINCT(user_id) FROM members WHERE user_id IS NOT NULL AND requested_at IS NULL)') }
217
  scope :todo_authors, ->(user_id, state) { where(id: Todo.where(user_id: user_id, state: state).select(:author_id)) }
218
219
  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')) }
220
221

  def self.with_two_factor
222
223
    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])
224
225
226
  end

  def self.without_two_factor
227
228
    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)
229
  end
Andrey Kumanyaev's avatar
Andrey Kumanyaev committed
230

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

Valery Sizov's avatar
Valery Sizov committed
245
246
    def sort(method)
      case method.to_s
247
248
      when 'recent_sign_in' then order_recent_sign_in
      when 'oldest_sign_in' then order_oldest_sign_in
249
250
      else
        order_by(method)
Valery Sizov's avatar
Valery Sizov committed
251
252
253
      end
    end

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

      User.find_by_sql([sql, { email: email }]).first
266
    end
267

268
    def filter(filter_name)
Andrey Kumanyaev's avatar
Andrey Kumanyaev committed
269
      case filter_name
270
      when 'admins'
271
        admins
272
      when 'blocked'
273
        blocked
274
      when 'two_factor_disabled'
275
        without_two_factor
276
      when 'two_factor_enabled'
277
        with_two_factor
278
      when 'wop'
279
        without_projects
280
      when 'external'
281
        external
Andrey Kumanyaev's avatar
Andrey Kumanyaev committed
282
      else
283
        active
Andrey Kumanyaev's avatar
Andrey Kumanyaev committed
284
      end
285
286
    end

287
288
289
290
291
292
293
    # 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.
294
    def search(query)
295
      table   = arel_table
296
297
298
      pattern = "%#{query}%"

      where(
299
300
301
        table[:name].matches(pattern).
          or(table[:email].matches(pattern)).
          or(table[:username].matches(pattern))
302
      )
Andrey Kumanyaev's avatar
Andrey Kumanyaev committed
303
    end
304

305
306
307
308
309
310
311
312
313
314
315
    # 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(
316
317
318
319
        table[:name].matches(pattern).
          or(table[:email].matches(pattern)).
          or(table[:username].matches(pattern)).
          or(table[:id].in(matched_by_emails_user_ids))
320
321
322
      )
    end

323
    def by_login(login)
324
325
326
327
328
329
330
      return nil unless login

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

333
334
335
336
    def find_by_username(username)
      iwhere(username: username).take
    end

Robert Speicher's avatar
Robert Speicher committed
337
    def find_by_username!(username)
338
      iwhere(username: username).take!
Robert Speicher's avatar
Robert Speicher committed
339
340
    end

Timothy Andrew's avatar
Timothy Andrew committed
341
    def find_by_personal_access_token(token_string)
342
343
      return unless token_string

344
      PersonalAccessTokensFinder.new(state: 'active').find_by(token: token_string)&.user
Timothy Andrew's avatar
Timothy Andrew committed
345
346
    end

Yorick Peterse's avatar
Yorick Peterse committed
347
348
349
350
351
    # 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

352
353
    def find_by_full_path(path, follow_redirects: false)
      namespace = Namespace.find_by_full_path(path, follow_redirects: follow_redirects)
Michael Kozono's avatar
Michael Kozono committed
354
      namespace&.owner
355
356
    end

357
358
359
    def reference_prefix
      '@'
    end
360
361
362
363
364

    # Pattern used to extract `@user` user references from text
    def reference_pattern
      %r{
        #{Regexp.escape(reference_prefix)}
365
        (?<user>#{Gitlab::Regex::FULL_NAMESPACE_REGEX_STR})
366
367
      }x
    end
368
369
370
371

    # Return (create if necessary) the ghost user. The ghost user
    # owns records previously belonging to deleted users.
    def ghost
372
373
374
375
      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
376
    end
vsizov's avatar
vsizov committed
377
  end
randx's avatar
randx committed
378

Michael Kozono's avatar
Michael Kozono committed
379
380
381
382
  def full_path
    username
  end

383
384
385
386
  def self.internal_attributes
    [:ghost]
  end

387
  def internal?
388
389
390
391
392
393
394
395
    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
396
    where(Hash[internal_attributes.zip([[false, nil]] * internal_attributes.size)])
397
398
  end

399
400
401
  #
  # Instance methods
  #
402
403
404
405
406

  def to_param
    username
  end

407
  def to_reference(_from_project = nil, target_project: nil, full: nil)
408
409
410
    "#{self.class.reference_prefix}#{username}"
  end

411
412
  def skip_confirmation=(bool)
    skip_confirmation! if bool
randx's avatar
randx committed
413
  end
414

415
  def generate_reset_token
Marin Jankovski's avatar
Marin Jankovski committed
416
    @reset_token, enc = Devise.token_generator.generate(self.class, :reset_password_token)
417
418
419
420

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

Marin Jankovski's avatar
Marin Jankovski committed
421
    @reset_token
422
423
  end

424
425
426
427
  def recently_sent_password_reset?
    reset_password_sent_at.present? && reset_password_sent_at >= 1.minute.ago
  end

Robert Speicher's avatar
Robert Speicher committed
428
  def disable_two_factor!
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
    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?
447
    otp_required_for_login?
448
449
450
  end

  def two_factor_u2f_enabled?
451
    u2f_registrations.exists?
Robert Speicher's avatar
Robert Speicher committed
452
453
  end

454
  def namespace_uniq
455
    # Return early if username already failed the first uniqueness validation
456
    return if errors.key?(:username) &&
457
        errors[:username].include?('has already been taken')
458

459
460
461
    existing_namespace = Namespace.by_path(username)
    if existing_namespace && existing_namespace != namespace
      errors.add(:username, 'has already been taken')
462
463
    end
  end
464

465
  def avatar_type
466
467
    unless avatar.image?
      errors.add :avatar, "only images allowed"
468
469
470
    end
  end

471
  def unique_email
472
473
    if !emails.exists?(email: email) && Email.exists?(email: email)
      errors.add(:email, 'has already been taken')
474
    end
475
476
  end

477
  def owns_notification_email
478
    return if temp_oauth_email?
479

480
    errors.add(:notification_email, "is not an email you own") unless all_emails.include?(notification_email)
481
482
  end

483
  def owns_public_email
484
    return if public_email.blank?
485

486
    errors.add(:public_email, "is not an email you own") unless all_emails.include?(public_email)
487
488
489
  end

  def update_emails_with_primary_email
490
    primary_email_record = emails.find_by(email: email)
491
492
    if primary_email_record
      primary_email_record.destroy
493
      emails.create(email: email_was)
494

495
      update_secondary_emails!
496
497
498
    end
  end

499
500
  # Returns the groups a user has access to
  def authorized_groups
501
502
    union = Gitlab::SQL::Union.
      new([groups.select(:id), authorized_projects.select(:namespace_id)])
503

504
    Group.where("namespaces.id IN (#{union.to_sql})")
505
506
  end

507
508
509
510
  def nested_groups
    Group.member_descendants(id)
  end

511
512
513
514
515
516
517
518
  def all_expanded_groups
    Group.member_hierarchy(id)
  end

  def expanded_groups_requiring_two_factor_authentication
    all_expanded_groups.where(require_two_factor_authentication: true)
  end

519
  def nested_groups_projects
520
521
    Project.joins(:namespace).where('namespaces.parent_id IS NOT NULL').
      member_descendants(id)
522
523
  end

524
  def refresh_authorized_projects
525
526
527
528
    Users::RefreshAuthorizedProjectsService.new(self).execute
  end

  def remove_project_authorizations(project_ids)
529
    project_authorizations.where(project_id: project_ids).delete_all
530
531
532
533
534
  end

  def set_authorized_projects_column
    unless authorized_projects_populated
      update_column(:authorized_projects_populated, true)
535
536
537
    end
  end

538
  def authorized_projects(min_access_level = nil)
539
540
541
542
543
544
545
546
547
548
549
    refresh_authorized_projects unless authorized_projects_populated

    # We're overriding an association, so explicitly call super with no arguments or it would be passed as `force_reload` to the association
    projects = super()
    projects = projects.where('project_authorizations.access_level >= ?', min_access_level) if min_access_level

    projects
  end

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

552
553
554
555
556
557
558
559
560
561
  # 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

562
  def viewable_starred_projects
563
564
565
    starred_projects.where("projects.visibility_level IN (?) OR projects.id IN (?)",
                           [Project::PUBLIC, Project::INTERNAL],
                           authorized_projects.select(:project_id))
566
567
  end

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

574
575
576
577
  # 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
Felipe Artur's avatar
Felipe Artur committed
578
    authorized_projects(Gitlab::Access::REPORTER).non_archived.with_issues_enabled
579
580
  end

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

585
586
587
588
  def require_password?
    password_automatically_set? && !ldap_user?
  end

589
  def can_change_username?
590
    gitlab_config.username_changing_enabled
591
592
  end

Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
593
  def can_create_project?
594
    projects_limit_left > 0
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
595
596
597
  end

  def can_create_group?
598
    can?(:create_group)
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
599
600
  end

601
602
603
604
  def can_select_namespace?
    several_namespaces? || admin
  end

605
  def can?(action, subject = :global)
http://jneen.net/'s avatar
http://jneen.net/ committed
606
    Ability.allowed?(self, action, subject)
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
607
608
609
610
611
612
  end

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

613
  def projects_limit_left
614
    projects_limit - personal_projects.count
615
616
  end

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

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

627
628
629
630
631
    # 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

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

  def projects_sorted_by_activity
    authorized_projects.sorted_by_activity
  end

  def several_namespaces?
646
    owned_groups.any? || masters_groups.any?
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
647
648
649
650
651
  end

  def namespace_id
    namespace.try :id
  end
652

653
654
655
  def name_with_username
    "#{name} (#{username})"
  end
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
656

657
  def already_forked?(project)
658
659
660
    !!fork_of(project)
  end

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

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

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

681
  def project_deploy_keys
682
    DeployKey.unscoped.in_projects(authorized_projects.pluck(:id)).distinct(:id)
683
684
  end

685
  def accessible_deploy_keys
686
687
688
689
690
    @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
691
  end
692
693

  def created_by
skv's avatar
skv committed
694
    User.find_by(id: created_by_id) if created_by_id
695
  end
696
697

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

704
  def set_notification_email
705
706
    if notification_email.blank? || !all_emails.include?(notification_email)
      self.notification_email = email
707
708
709
    end
  end

710
  def set_public_email
711
    if public_email.blank? || !all_emails.include?(public_email)
712
      self.public_email = ''
713
714
715
    end
  end

716
  def update_secondary_emails!
717
718
719
    set_notification_email
    set_public_email
    save if notification_email_changed? || public_email_changed?
720
721
  end

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

728
    connection_default_value_defined = new_record? && !projects_limit_changed?
729
    return unless projects_limit.nil? || connection_default_value_defined
730
731
732
733

    self.projects_limit = current_application_settings.default_projects_limit
  end

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

Jacob Vosmaer's avatar
Jacob Vosmaer committed
744
745
746
747
748
749
750
  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

751
752
753
754
755
  def solo_owned_groups
    @solo_owned_groups ||= owned_groups.select do |group|
      group.owners == [self]
    end
  end
756
757

  def with_defaults
758
    User.defaults.each do |k, v|
759
      public_send("#{k}=", v)
760
    end
761
762

    self
763
  end
764

765
766
767
768
  def can_leave_project?(project)
    project.namespace != namespace &&
      project.project_member(self)
  end
769

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

    website_url
  end

  def short_website_url
777
    website_url.sub(/\Ahttps?:\/\//, '')
Jerome Dalbert's avatar
Jerome Dalbert committed
778
  end
GitLab's avatar
GitLab committed
779

780
  def all_ssh_keys
781
    keys.map(&:publishable_key)
782
  end
783
784

  def temp_oauth_email?
785
    email.start_with?('temp-email-for-oauth')
786
787
  end

788
789
790
791
  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
    avatar_path(args) || GravatarService.new.execute(email, size, scale)
792
  end
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
793

794
  def all_emails
795
    all_emails = []
796
797
    all_emails << email unless temp_oauth_email?
    all_emails.concat(emails.map(&:email))
798
    all_emails
799
800
  end

Kirill Zaitsev's avatar
Kirill Zaitsev committed
801
802
803
804
805
806
807
808
  def hook_attrs
    {
      name: name,
      username: username,
      avatar_url: avatar_url
    }
  end

Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
809
810
  def ensure_namespace_correct
    # Ensure user has namespace
811
    create_namespace!(path: username, name: username) unless namespace
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
812

813
814
    if username_changed?
      namespace.update_attributes(path: username, name: username)
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
815
816
817
818
    end
  end

  def post_destroy_hook
819
    log_info("User \"#{name}\" (#{email})  was removed")
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
820
821
822
    system_hook_service.execute_hooks_for(self, :destroy)
  end

Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
823
  def notification_service
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
824
825
826
    NotificationService.new
  end

827
  def log_info(message)
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
828
829
830
831
832
833
    Gitlab::AppLogger.info message
  end

  def system_hook_service
    SystemHooksService.new
  end
Ciro Santilli's avatar
Ciro Santilli committed
834
835

  def starred?(project)
836
    starred_projects.exists?(project.id)
Ciro Santilli's avatar
Ciro Santilli committed
837
838
839
  end

  def toggle_star(project)
840
    UsersStarProject.transaction do
841
842
      user_star_project = users_star_projects.
          where(project: project, user: self).lock(true).first
843
844
845
846
847
848

      if user_star_project
        user_star_project.destroy
      else
        UsersStarProject.create!(project: project, user: self)
      end
Ciro Santilli's avatar
Ciro Santilli committed
849
850
    end
  end
851
852

  def manageable_namespaces
853
    @manageable_namespaces ||= [namespace] + owned_groups + masters_groups
854
  end
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
855

856
857
858
859
860
861
  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
862
  def oauth_authorized_tokens
863
    Doorkeeper::AccessToken.where(resource_owner_id: id, revoked_at: nil)
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
864
  end
865

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

    Project.where(id: events)
885
  end
886

887
888
889
  def can_be_removed?
    !solo_owned_groups.present?
  end
890
891

  def ci_authorized_runners
892
    @ci_authorized_runners ||= begin
893
      runner_ids = Ci::RunnerProject.
Kamil Trzciński's avatar
Kamil Trzciński committed
894
        where("ci_runner_projects.project_id IN (#{ci_projects_union.to_sql})").
895
        select(:runner_id)
896
897
      Ci::Runner.specific.where(id: runner_ids)
    end
898
  end
899

900
901
902
903
  def notification_settings_for(source)
    notification_settings.find_or_initialize_by(source: source)
  end

904
905
906
  # Lazy load global notification setting
  # Initializes User setting with Participating level if setting not persisted
  def global_notification_setting
907
908
909
910
911
912
    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
913
914
  end

915
916
  def assigned_open_merge_requests_count(force: false)
    Rails.cache.fetch(['users', id, 'assigned_open_merge_requests_count'], force: force) do