user.rb 17.8 KB
Newer Older
1 2 3 4
# == Schema Information
#
# Table name: users
#
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43
#  id                       :integer          not null, primary key
#  email                    :string(255)      default(""), not null
#  encrypted_password       :string(255)      default(""), not null
#  reset_password_token     :string(255)
#  reset_password_sent_at   :datetime
#  remember_created_at      :datetime
#  sign_in_count            :integer          default(0)
#  current_sign_in_at       :datetime
#  last_sign_in_at          :datetime
#  current_sign_in_ip       :string(255)
#  last_sign_in_ip          :string(255)
#  created_at               :datetime
#  updated_at               :datetime
#  name                     :string(255)
#  admin                    :boolean          default(FALSE), not null
#  projects_limit           :integer          default(10)
#  skype                    :string(255)      default(""), not null
#  linkedin                 :string(255)      default(""), not null
#  twitter                  :string(255)      default(""), not null
#  authentication_token     :string(255)
#  theme_id                 :integer          default(1), not null
#  bio                      :string(255)
#  failed_attempts          :integer          default(0)
#  locked_at                :datetime
#  username                 :string(255)
#  can_create_group         :boolean          default(TRUE), not null
#  can_create_team          :boolean          default(TRUE), not null
#  state                    :string(255)
#  color_scheme_id          :integer          default(1), not null
#  notification_level       :integer          default(1), not null
#  password_expires_at      :datetime
#  created_by_id            :integer
#  avatar                   :string(255)
#  confirmation_token       :string(255)
#  confirmed_at             :datetime
#  confirmation_sent_at     :datetime
#  unconfirmed_email        :string(255)
#  hide_no_ssh_key          :boolean          default(FALSE)
#  website_url              :string(255)      default(""), not null
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
44 45
#  last_credential_check_at :datetime
#  github_access_token      :string(255)
46
#  notification_email       :string(255)
47 48
#

49 50 51
require 'carrierwave/orm/activerecord'
require 'file_size_validator'

gitlabhq's avatar
gitlabhq committed
52
class User < ActiveRecord::Base
53
  include Sortable
54
  include Gitlab::ConfigHelper
55
  include TokenAuthenticatable
56 57
  extend Gitlab::ConfigHelper
  extend Gitlab::CurrentSettings
58

59
  default_value_for :admin, false
60
  default_value_for :can_create_group, gitlab_config.default_can_create_group
61 62
  default_value_for :can_create_team, false
  default_value_for :hide_no_ssh_key, false
63
  default_value_for :projects_limit, current_application_settings.default_projects_limit
64
  default_value_for :theme_id, gitlab_config.default_theme
65

66
  devise :database_authenticatable, :lockable, :async,
67
         :recoverable, :rememberable, :trackable, :validatable, :omniauthable, :confirmable, :registerable
gitlabhq's avatar
gitlabhq committed
68

69
  attr_accessor :force_random_password
gitlabhq's avatar
gitlabhq committed
70

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

74 75 76 77
  #
  # Relations
  #

78
  # Namespace for personal projects
79
  has_one :namespace, -> { where type: nil }, dependent: :destroy, foreign_key: :owner_id, class_name: "Namespace"
80 81 82

  # Profile
  has_many :keys, dependent: :destroy
83
  has_many :emails, dependent: :destroy
84
  has_many :identities, dependent: :destroy
85 86

  # Groups
87 88 89 90
  has_many :members, dependent: :destroy
  has_many :project_members, source: 'ProjectMember'
  has_many :group_members, source: 'GroupMember'
  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 :projects,                 through: :project_members
98
  has_many :created_projects,         foreign_key: :creator_id, class_name: 'Project'
Ciro Santilli's avatar
Ciro Santilli committed
99 100
  has_many :users_star_projects, dependent: :destroy
  has_many :starred_projects, through: :users_star_projects, source: :project
101

102
  has_many :snippets,                 dependent: :destroy, foreign_key: :author_id, class_name: "Snippet"
103
  has_many :project_members,          dependent: :destroy, class_name: 'ProjectMember'
104 105 106 107
  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"
108
  has_many :recent_events, -> { order "id DESC" }, foreign_key: :author_id,   class_name: "Event"
109 110
  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
111
  has_many :oauth_applications, class_name: 'Doorkeeper::Application', as: :owner, dependent: :destroy
112

113

114 115 116
  #
  # Validations
  #
Cyril's avatar
Cyril committed
117
  validates :name, presence: true
118
  validates :email, presence: true, email: { strict_mode: true }, uniqueness: true
119
  validates :notification_email, presence: true, email: { strict_mode: true }
120
  validates :bio, length: { maximum: 255 }, allow_blank: true
121
  validates :projects_limit, presence: true, numericality: { greater_than_or_equal_to: 0 }
122 123 124 125 126 127
  validates :username,
    presence: true,
    uniqueness: { case_sensitive: false },
    exclusion: { in: Gitlab::Blacklist.path },
    format: { with: Gitlab::Regex.username_regex,
              message: Gitlab::Regex.username_regex_message }
128

Andrey Kumanyaev's avatar
Andrey Kumanyaev committed
129
  validates :notification_level, inclusion: { in: Notification.notification_levels }, presence: true
130
  validate :namespace_uniq, if: ->(user) { user.username_changed? }
131
  validate :avatar_type, if: ->(user) { user.avatar_changed? }
132
  validate :unique_email, if: ->(user) { user.email_changed? }
133
  validate :owns_notification_email, if: ->(user) { user.notification_email_changed? }
134
  validates :avatar, file_size: { maximum: 200.kilobytes.to_i }
135

136
  before_validation :generate_password, on: :create
137
  before_validation :sanitize_attrs
138
  before_validation :set_notification_email, if: ->(user) { user.email_changed? }
139

Nihad Abbasov's avatar
Nihad Abbasov committed
140
  before_save :ensure_authentication_token
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
141 142 143 144
  after_save :ensure_namespace_correct
  after_create :post_create_hook
  after_destroy :post_destroy_hook

145

Nihad Abbasov's avatar
Nihad Abbasov committed
146
  alias_attribute :private_token, :authentication_token
147

148
  delegate :path, to: :namespace, allow_nil: true, prefix: true
149

150 151 152
  state_machine :state, initial: :active do
    after_transition any => :blocked do |user, transition|
      # Remove user from all projects and
153
      user.project_members.find_each do |membership|
154 155 156 157 158 159 160
        # skip owned resources
        next if membership.project.owner == user

        return false unless membership.destroy
      end

      # Remove user from all groups
161
      user.group_members.find_each do |membership|
162
        # skip owned resources
163
        next if membership.group.last_owner?(user)
164

165 166 167 168 169 170 171 172 173 174 175 176 177
        return false unless membership.destroy
      end
    end

    event :block do
      transition active: :blocked
    end

    event :activate do
      transition blocked: :active
    end
  end

178 179
  mount_uploader :avatar, AttachmentUploader

Andrey Kumanyaev's avatar
Andrey Kumanyaev committed
180
  # Scopes
Andrew8xx8's avatar
Andrew8xx8 committed
181
  scope :admins, -> { where(admin:  true) }
182 183
  scope :blocked, -> { with_state(:blocked) }
  scope :active, -> { with_state(:active) }
184 185
  scope :in_team, ->(team){ where(id: team.member_ids) }
  scope :not_in_team, ->(team){ where('users.id NOT IN (:ids)', ids: team.member_ids) }
skv's avatar
skv committed
186
  scope :not_in_project, ->(project) { project.users.present? ? where("id not in (:ids)", ids: project.users.map(&:id) ) : all }
187
  scope :without_projects, -> { where('id NOT IN (SELECT DISTINCT(user_id) FROM members)') }
188
  scope :potential_team_members, ->(team) { team.members.any? ? active.not_in_team(team) : active  }
Andrey Kumanyaev's avatar
Andrey Kumanyaev committed
189

190 191 192
  #
  # Class methods
  #
Andrey Kumanyaev's avatar
Andrey Kumanyaev committed
193
  class << self
194
    # Devise method overridden to allow sign in with email or username
195 196 197 198 199 200 201 202
    def find_for_database_authentication(warden_conditions)
      conditions = warden_conditions.dup
      if login = conditions.delete(:login)
        where(conditions).where(["lower(username) = :value OR lower(email) = :value", { value: login.downcase }]).first
      else
        where(conditions).first
      end
    end
203

Valery Sizov's avatar
Valery Sizov committed
204 205
    def sort(method)
      case method.to_s
206 207 208 209
      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
210 211 212
      end
    end

213 214 215 216 217 218
    def find_for_commit(email, name)
      # Prefer email match over name match
      User.where(email: email).first ||
        User.joins(:emails).where(emails: { email: email }).first ||
        User.where(name: name).first
    end
219

220
    def filter(filter_name)
Andrey Kumanyaev's avatar
Andrey Kumanyaev committed
221 222 223 224 225 226 227
      case filter_name
      when "admins"; self.admins
      when "blocked"; self.blocked
      when "wop"; self.without_projects
      else
        self.active
      end
228 229
    end

230
    def search(query)
231
      where("lower(name) LIKE :query OR lower(email) LIKE :query OR lower(username) LIKE :query", query: "%#{query.downcase}%")
Andrey Kumanyaev's avatar
Andrey Kumanyaev committed
232
    end
233

234 235 236 237 238
    def by_login(login)
      where('lower(username) = :value OR lower(email) = :value',
            value: login.to_s.downcase).first
    end

239
    def by_username_or_id(name_or_id)
240
      where('users.username = ? OR users.id = ?', name_or_id.to_s, name_or_id.to_i).first
241
    end
242

243 244
    def build_user(attrs = {})
      User.new(attrs)
245
    end
246 247 248 249 250 251 252 253 254

    def clean_username(username)
      username.gsub!(/@.*\z/,             "")
      username.gsub!(/\.git\z/,           "")
      username.gsub!(/\A-/,               "")
      username.gsub!(/[^a-zA-Z0-9_\-\.]/, "")

      counter = 0
      base = username
255
      while User.by_login(username).present? || Namespace.by_path(username).present?
256 257 258 259 260 261
        counter += 1 
        username = "#{base}#{counter}"
      end

      username
    end
vsizov's avatar
vsizov committed
262
  end
randx's avatar
randx committed
263

264 265 266
  #
  # Instance methods
  #
267 268 269 270 271

  def to_param
    username
  end

Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
272 273 274 275
  def notification
    @notification ||= Notification.new(self)
  end

Andrey Kumanyaev's avatar
Andrey Kumanyaev committed
276 277 278 279
  def generate_password
    if self.force_random_password
      self.password = self.password_confirmation = Devise.friendly_token.first(8)
    end
randx's avatar
randx committed
280
  end
281

282
  def generate_reset_token
283
    @reset_token, enc = Devise.token_generator.generate(self.class, :reset_password_token)
284 285 286 287

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

288
    @reset_token
289 290
  end

291 292
  def namespace_uniq
    namespace_name = self.username
293 294
    existing_namespace = Namespace.by_path(namespace_name)
    if existing_namespace && existing_namespace != self.namespace
lol768's avatar
lol768 committed
295
      self.errors.add :username, "already exists"
296 297
    end
  end
298

299 300 301 302 303 304
  def avatar_type
    unless self.avatar.image?
      self.errors.add :avatar, "only images allowed"
    end
  end

305 306 307 308
  def unique_email
    self.errors.add(:email, 'has already been taken') if Email.exists?(email: self.email)
  end

309 310 311 312
  def owns_notification_email
    self.errors.add(:notification_email, "is not an email you own") unless self.all_emails.include?(self.notification_email)
  end

313 314
  # Groups user has access to
  def authorized_groups
315
    @authorized_groups ||= begin
316
                             group_ids = (groups.pluck(:id) + authorized_projects.pluck(:namespace_id))
317
                             Group.where(id: group_ids)
318
                           end
319 320 321 322 323
  end


  # Projects user has access to
  def authorized_projects
324
    @authorized_projects ||= begin
325
                               project_ids = personal_projects.pluck(:id)
326 327
                               project_ids.push(*groups_projects.pluck(:id))
                               project_ids.push(*projects.pluck(:id).uniq)
328
                               Project.where(id: project_ids)
329
                             end
330 331
  end

332 333 334 335 336 337
  def owned_projects
    @owned_projects ||= begin
                          Project.where(namespace_id: owned_groups.pluck(:id).push(namespace.id)).joins(:namespace)
                        end
  end

338 339
  # Team membership in authorized projects
  def tm_in_authorized_projects
340
    ProjectMember.where(source_id: authorized_projects.map(&:id), user_id: self.id)
341
  end
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
342 343 344 345 346 347 348 349 350

  def is_admin?
    admin
  end

  def require_ssh_key?
    keys.count == 0
  end

351
  def can_change_username?
352
    gitlab_config.username_changing_enabled
353 354
  end

Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
355
  def can_create_project?
356
    projects_limit_left > 0
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
357 358 359
  end

  def can_create_group?
360
    can?(:create_group, nil)
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
361 362 363
  end

  def abilities
Ciro Santilli's avatar
Ciro Santilli committed
364
    Ability.abilities
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
365 366
  end

367 368 369 370
  def can_select_namespace?
    several_namespaces? || admin
  end

371
  def can?(action, subject)
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
372 373 374 375 376 377 378 379
    abilities.allowed?(self, action, subject)
  end

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

  def cared_merge_requests
380
    MergeRequest.cared(self)
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
381 382
  end

383
  def projects_limit_left
384
    projects_limit - personal_projects.count
385 386
  end

Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
387 388
  def projects_limit_percent
    return 100 if projects_limit.zero?
389
    (personal_projects.count.to_f / projects_limit) * 100
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
390 391
  end

392
  def recent_push(project_id = nil)
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
393 394 395 396 397 398 399 400 401 402 403 404 405
    # 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

    # Take only latest one
    events = events.recent.limit(1).first
  end

  def projects_sorted_by_activity
    authorized_projects.sorted_by_activity
  end

  def several_namespaces?
406
    owned_groups.any? || masters_groups.any?
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
407 408 409 410 411
  end

  def namespace_id
    namespace.try :id
  end
412

413 414 415
  def name_with_username
    "#{name} (#{username})"
  end
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
416 417 418 419

  def tm_of(project)
    project.team_member_by_id(self.id)
  end
420

421
  def already_forked?(project)
422 423 424
    !!fork_of(project)
  end

425
  def fork_of(project)
426 427 428 429 430 431 432 433
    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
434 435

  def ldap_user?
436 437 438 439 440
    identities.exists?(["provider LIKE ? AND extern_uid IS NOT NULL", "ldap%"])
  end

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

443
  def accessible_deploy_keys
444
    DeployKey.in_projects(self.authorized_projects.pluck(:id)).uniq
445
  end
446 447

  def created_by
skv's avatar
skv committed
448
    User.find_by(id: created_by_id) if created_by_id
449
  end
450 451 452 453 454 455 456

  def sanitize_attrs
    %w(name username skype linkedin twitter bio).each do |attr|
      value = self.send(attr)
      self.send("#{attr}=", Sanitize.clean(value)) if value.present?
    end
  end
457

458 459 460 461 462 463
  def set_notification_email
    if self.notification_email.blank? || !self.all_emails.include?(self.notification_email)
      self.notification_email = self.email 
    end
  end

464
  def requires_ldap_check?
465 466 467
    if !Gitlab.config.ldap.enabled
      false
    elsif ldap_user?
468 469 470 471 472 473
      !last_credential_check_at || (last_credential_check_at + 1.hour) < Time.now
    else
      false
    end
  end

474 475 476 477 478
  def solo_owned_groups
    @solo_owned_groups ||= owned_groups.select do |group|
      group.owners == [self]
    end
  end
479 480

  def with_defaults
481 482
    User.defaults.each do |k, v|
      self.send("#{k}=", v)
483
    end
484 485

    self
486
  end
487

488 489 490 491
  def can_leave_project?(project)
    project.namespace != namespace &&
      project.project_member(self)
  end
492 493 494 495 496 497 498 499 500 501 502 503 504 505

  # 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
506 507 508 509 510 511 512 513 514 515

  def full_website_url
    return "http://#{website_url}" if website_url !~ /^https?:\/\//

    website_url
  end

  def short_website_url
    website_url.gsub(/https?:\/\//, '')
  end
GitLab's avatar
GitLab committed
516

517
  def all_ssh_keys
GitLab's avatar
GitLab committed
518
    keys.map(&:key)
519
  end
520 521

  def temp_oauth_email?
522
    email.start_with?('temp-email-for-oauth')
523 524
  end

525 526 527
  def public_profile?
    authorized_projects.public_only.any?
  end
528 529 530

  def avatar_url(size = nil)
    if avatar.present?
531
      [gitlab_config.url, avatar.url].join
532
    else
533
      GravatarService.new.execute(email, size)
534 535
    end
  end
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
536

537 538 539 540
  def all_emails
    [self.email, *self.emails.map(&:email)]
  end

Kirill Zaitsev's avatar
Kirill Zaitsev committed
541 542 543 544 545 546 547 548
  def hook_attrs
    {
      name: name,
      username: username,
      avatar_url: avatar_url
    }
  end

Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
549 550 551 552 553 554 555 556 557 558 559
  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")
560
    notification_service.new_user(self, @reset_token)
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
561 562 563 564 565 566 567 568
    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
569
  def notification_service
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
570 571 572
    NotificationService.new
  end

573
  def log_info(message)
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
574 575 576 577 578 579
    Gitlab::AppLogger.info message
  end

  def system_hook_service
    SystemHooksService.new
  end
Ciro Santilli's avatar
Ciro Santilli committed
580 581 582 583 584 585

  def starred?(project)
    starred_projects.exists?(project)
  end

  def toggle_star(project)
586 587
    user_star_project = users_star_projects.
      where(project: project, user: self).take
Ciro Santilli's avatar
Ciro Santilli committed
588 589 590 591 592 593
    if user_star_project
      user_star_project.destroy
    else
      UsersStarProject.create!(project: project, user: self)
    end
  end
594 595 596 597 598 599 600 601 602 603

  def manageable_namespaces
    @manageable_namespaces ||=
      begin
        namespaces = []
        namespaces << namespace
        namespaces += owned_groups
        namespaces += masters_groups
      end
  end
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
604 605 606 607

  def oauth_authorized_tokens
    Doorkeeper::AccessToken.where(resource_owner_id: self.id, revoked_at: nil)
  end
gitlabhq's avatar
gitlabhq committed
608
end