user.rb 23.4 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 9
  include Referable
  include Sortable
10
  include CaseSensitivity
11 12 13
  include TokenAuthenticatable

  add_authentication_token_field :authentication_token
14

15
  default_value_for :admin, false
Zeger-Jan van de Weg's avatar
Zeger-Jan van de Weg committed
16
  default_value_for :external, false
17
  default_value_for :can_create_group, gitlab_config.default_can_create_group
18 19
  default_value_for :can_create_team, false
  default_value_for :hide_no_ssh_key, false
20
  default_value_for :hide_no_password, false
21
  default_value_for :theme_id, gitlab_config.default_theme
22

23
  devise :two_factor_authenticatable,
24
         otp_secret_encryption_key: Gitlab::Application.config.secret_key_base
25
  alias_attribute :two_factor_enabled, :otp_required_for_login
26

27
  devise :two_factor_backupable, otp_number_of_backup_codes: 10
28 29
  serialize :otp_backup_codes, JSON

30 31
  devise :lockable, :async, :recoverable, :rememberable, :trackable,
    :validatable, :omniauthable, :confirmable, :registerable
gitlabhq's avatar
gitlabhq committed
32

33
  attr_accessor :force_random_password
gitlabhq's avatar
gitlabhq committed
34

35 36 37
  # Virtual attribute for authenticating by either username or email
  attr_accessor :login

38 39 40 41
  #
  # Relations
  #

42
  # Namespace for personal projects
43
  has_one :namespace, -> { where type: nil }, dependent: :destroy, foreign_key: :owner_id, class_name: "Namespace"
44 45 46

  # Profile
  has_many :keys, dependent: :destroy
47
  has_many :emails, dependent: :destroy
48
  has_many :identities, dependent: :destroy, autosave: true
49 50

  # Groups
51 52 53 54
  has_many :members, dependent: :destroy
  has_many :project_members, source: 'ProjectMember'
  has_many :group_members, source: 'GroupMember'
  has_many :groups, through: :group_members
55 56
  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
57

58
  # Projects
59 60
  has_many :groups_projects,          through: :groups, source: :projects
  has_many :personal_projects,        through: :namespace, source: :projects
61
  has_many :projects,                 through: :project_members
62
  has_many :created_projects,         foreign_key: :creator_id, class_name: 'Project'
Ciro Santilli's avatar
Ciro Santilli committed
63 64
  has_many :users_star_projects, dependent: :destroy
  has_many :starred_projects, through: :users_star_projects, source: :project
65

66
  has_many :snippets,                 dependent: :destroy, foreign_key: :author_id, class_name: "Snippet"
67
  has_many :project_members,          dependent: :destroy, class_name: 'ProjectMember'
68 69 70 71
  has_many :issues,                   dependent: :destroy, foreign_key: :author_id
  has_many :notes,                    dependent: :destroy, foreign_key: :author_id
  has_many :merge_requests,           dependent: :destroy, foreign_key: :author_id
  has_many :events,                   dependent: :destroy, foreign_key: :author_id,   class_name: "Event"
72
  has_many :subscriptions,            dependent: :destroy
73
  has_many :recent_events, -> { order "id DESC" }, foreign_key: :author_id,   class_name: "Event"
74 75
  has_many :assigned_issues,          dependent: :destroy, foreign_key: :assignee_id, class_name: "Issue"
  has_many :assigned_merge_requests,  dependent: :destroy, foreign_key: :assignee_id, class_name: "MergeRequest"
Valery Sizov's avatar
Valery Sizov committed
76
  has_many :oauth_applications, class_name: 'Doorkeeper::Application', as: :owner, dependent: :destroy
77
  has_one  :abuse_report,             dependent: :destroy
78
  has_many :spam_logs,                dependent: :destroy
79
  has_many :builds,                   dependent: :nullify, class_name: 'Ci::Build'
80
  has_many :todos,                    dependent: :destroy
81
  has_many :notification_settings,    dependent: :destroy
82
  has_many :award_emoji, 							as: :awardable, dependent: :destroy
83

84 85 86
  #
  # Validations
  #
Cyril's avatar
Cyril committed
87
  validates :name, presence: true
88 89
  validates :notification_email, presence: true, email: true
  validates :public_email, presence: true, uniqueness: true, email: true, allow_blank: true
90
  validates :bio, length: { maximum: 255 }, allow_blank: true
91
  validates :projects_limit, presence: true, numericality: { greater_than_or_equal_to: 0 }
92
  validates :username,
93
    namespace: true,
94
    presence: true,
95
    uniqueness: { case_sensitive: false }
96

97
  validates :notification_level, presence: true
98
  validate :namespace_uniq, if: ->(user) { user.username_changed? }
99
  validate :avatar_type, if: ->(user) { user.avatar.present? && user.avatar_changed? }
100
  validate :unique_email, if: ->(user) { user.email_changed? }
101
  validate :owns_notification_email, if: ->(user) { user.notification_email_changed? }
102
  validate :owns_public_email, if: ->(user) { user.public_email_changed? }
103
  validates :avatar, file_size: { maximum: 200.kilobytes.to_i }
104

105
  before_validation :generate_password, on: :create
106
  before_validation :restricted_signup_domains, on: :create
107
  before_validation :sanitize_attrs
108
  before_validation :set_notification_email, if: ->(user) { user.email_changed? }
109
  before_validation :set_public_email, if: ->(user) { user.public_email_changed? }
110

111
  after_update :update_emails_with_primary_email, if: ->(user) { user.email_changed? }
Nihad Abbasov's avatar
Nihad Abbasov committed
112
  before_save :ensure_authentication_token
Zeger-Jan van de Weg's avatar
Zeger-Jan van de Weg committed
113
  before_save :ensure_external_user_rights
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
114
  after_save :ensure_namespace_correct
115
  after_initialize :set_projects_limit
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
116 117 118
  after_create :post_create_hook
  after_destroy :post_destroy_hook

119
  # User's Layout preference
120
  enum layout: [:fixed, :fluid]
121

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

126 127
  # User's Project preference
  # Note: When adding an option, it MUST go on the end of the array.
128
  enum project_view: [:readme, :activity, :files]
129

130 131
  # Notification level
  # Note: When adding an option, it MUST go on the end of the array.
132 133 134
  #
  # TODO: Add '_prefix: :notification' to enum when update to Rails 5. https://github.com/rails/rails/pull/19813
  # Because user.notification_disabled? is much better than user.disabled?
135 136
  enum notification_level: [:disabled, :participating, :watch, :global, :mention]

Nihad Abbasov's avatar
Nihad Abbasov committed
137
  alias_attribute :private_token, :authentication_token
138

139
  delegate :path, to: :namespace, allow_nil: true, prefix: true
140

141 142 143
  state_machine :state, initial: :active do
    event :block do
      transition active: :blocked
144
      transition ldap_blocked: :blocked
145 146
    end

147 148 149 150
    event :ldap_block do
      transition active: :ldap_blocked
    end

151 152
    event :activate do
      transition blocked: :active
153
      transition ldap_blocked: :active
154
    end
155 156 157 158 159 160

    state :blocked, :ldap_blocked do
      def blocked?
        true
      end
    end
161 162
  end

163
  mount_uploader :avatar, AvatarUploader
164

Andrey Kumanyaev's avatar
Andrey Kumanyaev committed
165
  # Scopes
166
  scope :admins, -> { where(admin: true) }
167
  scope :blocked, -> { with_states(:blocked, :ldap_blocked) }
168
  scope :external, -> { where(external: true) }
169
  scope :active, -> { with_state(:active) }
skv's avatar
skv committed
170
  scope :not_in_project, ->(project) { project.users.present? ? where("id not in (:ids)", ids: project.users.map(&:id) ) : all }
171
  scope :without_projects, -> { where('id NOT IN (SELECT DISTINCT(user_id) FROM members)') }
172 173
  scope :with_two_factor,    -> { where(two_factor_enabled: true) }
  scope :without_two_factor, -> { where(two_factor_enabled: false) }
Andrey Kumanyaev's avatar
Andrey Kumanyaev committed
174

175 176 177
  #
  # Class methods
  #
Andrey Kumanyaev's avatar
Andrey Kumanyaev committed
178
  class << self
179
    # Devise method overridden to allow sign in with email or username
180 181 182
    def find_for_database_authentication(warden_conditions)
      conditions = warden_conditions.dup
      if login = conditions.delete(:login)
Gabriel Mazetto's avatar
Gabriel Mazetto committed
183
        where(conditions).find_by("lower(username) = :value OR lower(email) = :value", value: login.downcase)
184
      else
Gabriel Mazetto's avatar
Gabriel Mazetto committed
185
        find_by(conditions)
186 187
      end
    end
188

Valery Sizov's avatar
Valery Sizov committed
189 190
    def sort(method)
      case method.to_s
191 192 193 194
      when 'recent_sign_in' then reorder(last_sign_in_at: :desc)
      when 'oldest_sign_in' then reorder(last_sign_in_at: :asc)
      else
        order_by(method)
Valery Sizov's avatar
Valery Sizov committed
195 196 197
      end
    end

198 199
    # Find a User by their primary email or any associated secondary email
    def find_by_any_email(email)
200 201 202 203 204 205 206
      sql = 'SELECT *
      FROM users
      WHERE id IN (
        SELECT id FROM users WHERE email = :email
        UNION
        SELECT emails.user_id FROM emails WHERE email = :email
      )
207 208 209
      LIMIT 1;'

      User.find_by_sql([sql, { email: email }]).first
210
    end
211

212
    def filter(filter_name)
Andrey Kumanyaev's avatar
Andrey Kumanyaev committed
213
      case filter_name
214 215 216 217 218 219 220 221 222 223
      when 'admins'
        self.admins
      when 'blocked'
        self.blocked
      when 'two_factor_disabled'
        self.without_two_factor
      when 'two_factor_enabled'
        self.with_two_factor
      when 'wop'
        self.without_projects
224 225
      when 'external'
        self.external
Andrey Kumanyaev's avatar
Andrey Kumanyaev committed
226 227 228
      else
        self.active
      end
229 230
    end

231 232 233 234 235 236 237
    # 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.
238
    def search(query)
239
      table   = arel_table
240 241 242 243 244 245 246
      pattern = "%#{query}%"

      where(
        table[:name].matches(pattern).
          or(table[:email].matches(pattern)).
          or(table[:username].matches(pattern))
      )
Andrey Kumanyaev's avatar
Andrey Kumanyaev committed
247
    end
248

249
    def by_login(login)
250 251 252 253 254 255 256
      return nil unless login

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

259 260 261 262
    def find_by_username!(username)
      find_by!('lower(username) = ?', username.downcase)
    end

263
    def by_username_or_id(name_or_id)
Gabriel Mazetto's avatar
Gabriel Mazetto committed
264
      find_by('users.username = ? OR users.id = ?', name_or_id.to_s, name_or_id.to_i)
265
    end
266

267 268
    def build_user(attrs = {})
      User.new(attrs)
269
    end
270 271 272 273

    def reference_prefix
      '@'
    end
274 275 276 277 278 279 280 281

    # Pattern used to extract `@user` user references from text
    def reference_pattern
      %r{
        #{Regexp.escape(reference_prefix)}
        (?<user>#{Gitlab::Regex::NAMESPACE_REGEX_STR})
      }x
    end
vsizov's avatar
vsizov committed
282
  end
randx's avatar
randx committed
283

284 285 286
  #
  # Instance methods
  #
287 288 289 290 291

  def to_param
    username
  end

292 293 294 295
  def to_reference(_from_project = nil)
    "#{self.class.reference_prefix}#{username}"
  end

Andrey Kumanyaev's avatar
Andrey Kumanyaev committed
296 297 298 299
  def generate_password
    if self.force_random_password
      self.password = self.password_confirmation = Devise.friendly_token.first(8)
    end
randx's avatar
randx committed
300
  end
301

302
  def generate_reset_token
303
    @reset_token, enc = Devise.token_generator.generate(self.class, :reset_password_token)
304 305 306 307

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

308
    @reset_token
309 310
  end

311 312 313 314
  def recently_sent_password_reset?
    reset_password_sent_at.present? && reset_password_sent_at >= 1.minute.ago
  end

315 316
  def disable_two_factor!
    update_attributes(
317 318 319 320 321 322
      two_factor_enabled:          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
323 324 325
    )
  end

326
  def namespace_uniq
327
    # Return early if username already failed the first uniqueness validation
328 329
    return if self.errors.key?(:username) &&
      self.errors[:username].include?('has already been taken')
330

331
    namespace_name = self.username
332 333
    existing_namespace = Namespace.by_path(namespace_name)
    if existing_namespace && existing_namespace != self.namespace
334
      self.errors.add(:username, 'has already been taken')
335 336
    end
  end
337

338 339 340 341 342 343
  def avatar_type
    unless self.avatar.image?
      self.errors.add :avatar, "only images allowed"
    end
  end

344
  def unique_email
345 346 347
    if !self.emails.exists?(email: self.email) && Email.exists?(email: self.email)
      self.errors.add(:email, 'has already been taken')
    end
348 349
  end

350
  def owns_notification_email
351 352
    return if self.temp_oauth_email?

353 354 355
    self.errors.add(:notification_email, "is not an email you own") unless self.all_emails.include?(self.notification_email)
  end

356
  def owns_public_email
357 358
    return if self.public_email.blank?

359 360 361 362 363 364 365 366
    self.errors.add(:public_email, "is not an email you own") unless self.all_emails.include?(self.public_email)
  end

  def update_emails_with_primary_email
    primary_email_record = self.emails.find_by(email: self.email)
    if primary_email_record
      primary_email_record.destroy
      self.emails.create(email: self.email_was)
367

368 369 370 371
      self.update_secondary_emails!
    end
  end

372 373
  # Returns the groups a user has access to
  def authorized_groups
374
    union = Gitlab::SQL::Union.
375
      new([groups.select(:id), authorized_projects.select(:namespace_id)])
376

377
    Group.where("namespaces.id IN (#{union.to_sql})")
378 379
  end

380
  # Returns projects user is authorized to access.
381 382
  def authorized_projects
    Project.where("projects.id IN (#{projects_union.to_sql})")
383 384
  end

385 386
  def viewable_starred_projects
    starred_projects.where("projects.visibility_level IN (?) OR projects.id IN (#{projects_union.to_sql})",
Sean McGivern's avatar
Sean McGivern committed
387
                           [Project::PUBLIC, Project::INTERNAL])
388 389
  end

390
  def owned_projects
391
    @owned_projects ||=
392 393
      Project.where('namespace_id IN (?) OR namespace_id = ?',
                    owned_groups.select(:id), namespace.id).joins(:namespace)
394 395
  end

396 397
  # Team membership in authorized projects
  def tm_in_authorized_projects
398
    ProjectMember.where(source_id: authorized_projects.map(&:id), user_id: self.id)
399
  end
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
400 401 402 403 404 405 406 407 408

  def is_admin?
    admin
  end

  def require_ssh_key?
    keys.count == 0
  end

409 410 411 412
  def require_password?
    password_automatically_set? && !ldap_user?
  end

413
  def can_change_username?
414
    gitlab_config.username_changing_enabled
415 416
  end

Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
417
  def can_create_project?
418
    projects_limit_left > 0
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
419 420 421
  end

  def can_create_group?
422
    can?(:create_group, nil)
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
423 424 425
  end

  def abilities
Ciro Santilli's avatar
Ciro Santilli committed
426
    Ability.abilities
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
427 428
  end

429 430 431 432
  def can_select_namespace?
    several_namespaces? || admin
  end

433
  def can?(action, subject)
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
434 435 436 437 438 439 440 441
    abilities.allowed?(self, action, subject)
  end

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

  def cared_merge_requests
442
    MergeRequest.cared(self)
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
443 444
  end

445
  def projects_limit_left
446
    projects_limit - personal_projects.count
447 448
  end

Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
449 450
  def projects_limit_percent
    return 100 if projects_limit.zero?
451
    (personal_projects.count.to_f / projects_limit) * 100
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
452 453
  end

454
  def recent_push(project_id = nil)
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
455 456 457 458
    # Get push events not earlier than 2 hours ago
    events = recent_events.code_push.where("created_at > ?", Time.now - 2.hours)
    events = events.where(project_id: project_id) if project_id

459 460 461 462 463 464 465 466 467 468 469 470 471
    # 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
      repo = project.repository

      if repo.branch_names.include?(event.branch_name)
        merge_requests = MergeRequest.where("created_at >= ?", event.created_at).
            where(source_project_id: project.id,
                  source_branch: event.branch_name)
        merge_requests.empty?
      end
    end
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
472 473 474 475 476 477 478
  end

  def projects_sorted_by_activity
    authorized_projects.sorted_by_activity
  end

  def several_namespaces?
479
    owned_groups.any? || masters_groups.any?
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
480 481 482 483 484
  end

  def namespace_id
    namespace.try :id
  end
485

486 487 488
  def name_with_username
    "#{name} (#{username})"
  end
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
489 490

  def tm_of(project)
491
    project.project_member_by_id(self.id)
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
492
  end
493

494
  def already_forked?(project)
495 496 497
    !!fork_of(project)
  end

498
  def fork_of(project)
499 500 501 502 503 504 505 506
    links = ForkedProjectLink.where(forked_from_project_id: project, forked_to_project_id: personal_projects)

    if links.any?
      links.first.forked_to_project
    else
      nil
    end
  end
507 508

  def ldap_user?
509 510 511 512 513
    identities.exists?(["provider LIKE ? AND extern_uid IS NOT NULL", "ldap%"])
  end

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

516
  def project_deploy_keys
517
    DeployKey.unscoped.in_projects(self.authorized_projects.pluck(:id)).distinct(:id)
518 519
  end

520
  def accessible_deploy_keys
521 522 523 524 525
    @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
526
  end
527 528

  def created_by
skv's avatar
skv committed
529
    User.find_by(id: created_by_id) if created_by_id
530
  end
531 532

  def sanitize_attrs
533
    %w(name username skype linkedin twitter).each do |attr|
534 535 536 537
      value = self.send(attr)
      self.send("#{attr}=", Sanitize.clean(value)) if value.present?
    end
  end
538

539 540
  def set_notification_email
    if self.notification_email.blank? || !self.all_emails.include?(self.notification_email)
541
      self.notification_email = self.email
542 543 544
    end
  end

545 546
  def set_public_email
    if self.public_email.blank? || !self.all_emails.include?(self.public_email)
547
      self.public_email = ''
548 549 550
    end
  end

551 552 553 554 555 556
  def update_secondary_emails!
    self.set_notification_email
    self.set_public_email
    self.save if self.notification_email_changed? || self.public_email_changed?
  end

557 558 559 560 561 562 563
  def set_projects_limit
    connection_default_value_defined = new_record? && !projects_limit_changed?
    return unless self.projects_limit.nil? || connection_default_value_defined

    self.projects_limit = current_application_settings.default_projects_limit
  end

564
  def requires_ldap_check?
565 566 567
    if !Gitlab.config.ldap.enabled
      false
    elsif ldap_user?
568 569 570 571 572 573
      !last_credential_check_at || (last_credential_check_at + 1.hour) < Time.now
    else
      false
    end
  end

Jacob Vosmaer's avatar
Jacob Vosmaer committed
574 575 576 577 578 579 580
  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

581 582 583 584 585
  def solo_owned_groups
    @solo_owned_groups ||= owned_groups.select do |group|
      group.owners == [self]
    end
  end
586 587

  def with_defaults
588 589
    User.defaults.each do |k, v|
      self.send("#{k}=", v)
590
    end
591 592

    self
593
  end
594

595 596 597 598
  def can_leave_project?(project)
    project.namespace != namespace &&
      project.project_member(self)
  end
599 600 601 602 603 604 605 606 607 608 609 610 611 612

  # Reset project events cache related to this user
  #
  # Since we do cache @event we need to reset cache in special cases:
  # * when the user changes their avatar
  # Events cache stored like  events/23-20130109142513.
  # The cache key includes updated_at timestamp.
  # Thus it will automatically generate a new fragment
  # when the event is updated because the key changes.
  def reset_events_cache
    Event.where(author_id: self.id).
      order('id DESC').limit(1000).
      update_all(updated_at: Time.now)
  end
Jerome Dalbert's avatar
Jerome Dalbert committed
613 614

  def full_website_url
615
    return "http://#{website_url}" if website_url !~ /\Ahttps?:\/\//
Jerome Dalbert's avatar
Jerome Dalbert committed
616 617 618 619 620

    website_url
  end

  def short_website_url
621
    website_url.sub(/\Ahttps?:\/\//, '')
Jerome Dalbert's avatar
Jerome Dalbert committed
622
  end
GitLab's avatar
GitLab committed
623

624
  def all_ssh_keys
625
    keys.map(&:publishable_key)
626
  end
627 628

  def temp_oauth_email?
629
    email.start_with?('temp-email-for-oauth')
630 631
  end

632
  def avatar_url(size = nil, scale = 2)
633
    if avatar.present?
634
      [gitlab_config.url, avatar.url].join
635
    else
636
      GravatarService.new.execute(email, size, scale)
637 638
    end
  end
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
639

640
  def all_emails
641 642 643 644
    all_emails = []
    all_emails << self.email unless self.temp_oauth_email?
    all_emails.concat(self.emails.map(&:email))
    all_emails
645 646
  end

Kirill Zaitsev's avatar
Kirill Zaitsev committed
647 648 649 650 651 652 653 654
  def hook_attrs
    {
      name: name,
      username: username,
      avatar_url: avatar_url
    }
  end

Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
655 656 657 658 659 660 661 662 663 664 665
  def ensure_namespace_correct
    # Ensure user has namespace
    self.create_namespace!(path: self.username, name: self.username) unless self.namespace

    if self.username_changed?
      self.namespace.update_attributes(path: self.username, name: self.username)
    end
  end

  def post_create_hook
    log_info("User \"#{self.name}\" (#{self.email}) was created")
666
    notification_service.new_user(self, @reset_token) if self.created_by_id
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
667 668 669 670 671 672 673 674
    system_hook_service.execute_hooks_for(self, :create)
  end

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

Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
675
  def notification_service
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
676 677 678
    NotificationService.new
  end

679
  def log_info(message)
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
680 681 682 683 684 685
    Gitlab::AppLogger.info message
  end

  def system_hook_service
    SystemHooksService.new
  end
Ciro Santilli's avatar
Ciro Santilli committed
686 687

  def starred?(project)
688
    starred_projects.exists?(project.id)
Ciro Santilli's avatar
Ciro Santilli committed
689 690 691
  end

  def toggle_star(project)
692 693 694 695 696 697 698 699 700
    UsersStarProject.transaction do
      user_star_project = users_star_projects.
          where(project: project, user: self).lock(true).first

      if user_star_project
        user_star_project.destroy
      else
        UsersStarProject.create!(project: project, user: self)
      end
Ciro Santilli's avatar
Ciro Santilli committed
701 702
    end
  end
703 704

  def manageable_namespaces
705
    @manageable_namespaces ||= [namespace] + owned_groups + masters_groups
706
  end
707

708 709 710 711 712 713
  def namespaces
    namespace_ids = groups.pluck(:id)
    namespace_ids.push(namespace.id)
    Namespace.where(id: namespace_ids)
  end

714 715 716
  def oauth_authorized_tokens
    Doorkeeper::AccessToken.where(resource_owner_id: self.id, revoked_at: nil)
  end
717

718 719 720 721 722 723 724 725 726
  # 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
727
  # ms on a database with a similar size to GitLab.com's database. On the other
728 729 730 731
  # hand, using a subquery means we can get the exact same data in about 40 ms.
  def contributed_projects
    events = Event.select(:project_id).
      contributions.where(author_id: self).
732
      where("created_at > ?", Time.now - 1.year).
733
      uniq.
734 735 736
      reorder(nil)

    Project.where(id: events)
737
  end
738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760

  def restricted_signup_domains
    email_domains = current_application_settings.restricted_signup_domains

    unless email_domains.blank?
      match_found = email_domains.any? do |domain|
        escaped = Regexp.escape(domain).gsub('\*','.*?')
        regexp = Regexp.new "^#{escaped}$", Regexp::IGNORECASE
        email_domain = Mail::Address.new(self.email).domain
        email_domain =~ regexp
      end

      unless match_found
        self.errors.add :email,
                        'is not whitelisted. ' +
                        'Email domains valid for registration are: ' +
                        email_domains.join(', ')
        return false
      end
    end

    true
  end
761 762 763 764

  def can_be_removed?
    !solo_owned_groups.present?
  end
765 766

  def ci_authorized_runners
767
    @ci_authorized_runners ||= begin
768 769
      runner_ids = Ci::RunnerProject.
        where("ci_runner_projects.gl_project_id IN (#{ci_projects_union.to_sql})").
770
        select(:runner_id)
771 772
      Ci::Runner.specific.where(id: runner_ids)
    end
773
  end
774

775 776 777 778
  def notification_settings_for(source)
    notification_settings.find_or_initialize_by(source: source)
  end

779 780 781
  private

  def projects_union
782 783
    Gitlab::SQL::Union.new([personal_projects.select(:id),
                            groups_projects.select(:id),
784 785
                            projects.select(:id),
                            groups.joins(:shared_projects).select(:project_id)])
786
  end
787 788 789 790 791 792 793 794 795

  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
796 797 798 799 800

  # 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
801 802 803 804 805 806 807

  def ensure_external_user_rights
    return unless self.external?

    self.can_create_group   = false
    self.projects_limit     = 0
  end
gitlabhq's avatar
gitlabhq committed
808
end