member.rb 11.1 KB
Newer Older
D
Dmitriy Zaporozhets 已提交
1
class Member < ActiveRecord::Base
D
Douwe Maan 已提交
2
  include AfterCommitQueue
3
  include Sortable
4
  include Importable
5
  include Expirable
D
Dmitriy Zaporozhets 已提交
6
  include Gitlab::Access
T
TM Lee 已提交
7
  include Presentable
D
Dmitriy Zaporozhets 已提交
8

9 10
  attr_accessor :raw_invite_token

11
  belongs_to :created_by, class_name: "User"
D
Dmitriy Zaporozhets 已提交
12
  belongs_to :user
13
  belongs_to :source, polymorphic: true # rubocop:disable Cop/PolymorphicAssociations
D
Dmitriy Zaporozhets 已提交
14

D
Douwe Maan 已提交
15 16
  delegate :name, :username, :email, to: :user, prefix: true

D
Douwe Maan 已提交
17
  validates :user, presence: true, unless: :invite?
D
Dmitriy Zaporozhets 已提交
18
  validates :source, presence: true
19
  validates :user_id, uniqueness: { scope: [:source_type, :source_id],
D
Douwe Maan 已提交
20 21
                                    message: "already exists in source",
                                    allow_nil: true }
22
  validates :access_level, inclusion: { in: Gitlab::Access.all_values }, presence: true
D
Douwe Maan 已提交
23 24 25 26
  validates :invite_email,
    presence: {
      if: :invite?
    },
27
    email: {
D
Douwe Maan 已提交
28 29 30 31 32 33
      allow_nil: true
    },
    uniqueness: {
      scope: [:source_type, :source_id],
      allow_nil: true
    }
D
Dmitriy Zaporozhets 已提交
34

35 36 37 38 39 40 41 42 43 44 45
  # This scope encapsulates (most of) the conditions a row in the member table
  # must satisfy if it is a valid permission. Of particular note:
  #
  #   * Access requests must be excluded
  #   * Blocked users must be excluded
  #   * Invitations take effect immediately
  #   * expires_at is not implemented. A background worker purges expired rows
  scope :active, -> do
    is_external_invite = arel_table[:user_id].eq(nil).and(arel_table[:invite_token].not_eq(nil))
    user_is_active = User.arel_table[:state].eq(:active)

46 47 48 49 50 51 52 53 54
    user_ok = Arel::Nodes::Grouping.new(is_external_invite).or(user_is_active)

    left_join_users
      .where(user_ok)
      .where(requested_at: nil)
      .reorder(nil)
  end

  # Like active, but without invites. For when a User is required.
55
  scope :active_without_invites_and_requests, -> do
56 57
    left_join_users
      .where(users: { state: 'active' })
58
      .non_request
59
      .reorder(nil)
60 61
  end

R
Rémy Coutable 已提交
62
  scope :invite, -> { where.not(invite_token: nil) }
R
Rémy Coutable 已提交
63
  scope :non_invite, -> { where(invite_token: nil) }
R
Rémy Coutable 已提交
64
  scope :request, -> { where.not(requested_at: nil) }
65
  scope :non_request, -> { where(requested_at: nil) }
66 67 68 69 70 71 72 73 74

  scope :has_access, -> { active.where('access_level > 0') }

  scope :guests, -> { active.where(access_level: GUEST) }
  scope :reporters, -> { active.where(access_level: REPORTER) }
  scope :developers, -> { active.where(access_level: DEVELOPER) }
  scope :masters,  -> { active.where(access_level: MASTER) }
  scope :owners,  -> { active.where(access_level: OWNER) }
  scope :owners_and_masters,  -> { active.where(access_level: [OWNER, MASTER]) }
75

76 77 78 79
  scope :order_name_asc, -> { left_join_users.reorder(Gitlab::Database.nulls_last_order('users.name', 'ASC')) }
  scope :order_name_desc, -> { left_join_users.reorder(Gitlab::Database.nulls_last_order('users.name', 'DESC')) }
  scope :order_recent_sign_in, -> { left_join_users.reorder(Gitlab::Database.nulls_last_order('users.last_sign_in_at', 'DESC')) }
  scope :order_oldest_sign_in, -> { left_join_users.reorder(Gitlab::Database.nulls_last_order('users.last_sign_in_at', 'ASC')) }
80

D
Douwe Maan 已提交
81
  before_validation :generate_invite_token, on: :create, if: -> (member) { member.invite_email.present? }
82

83
  after_create :send_invite, if: :invite?, unless: :importing?
J
James Lopez 已提交
84 85 86 87
  after_create :send_request, if: :request?, unless: :importing?
  after_create :create_notification_setting, unless: [:pending?, :importing?]
  after_create :post_create_hook, unless: [:pending?, :importing?]
  after_update :post_update_hook, unless: [:pending?, :importing?]
88
  after_destroy :destroy_notification_setting
89
  after_destroy :post_destroy_hook, unless: :pending?
90
  after_commit :refresh_member_authorized_projects
D
Douwe Maan 已提交
91

92 93
  default_value_for :notification_level, NotificationSetting.levels[:global]

94
  class << self
95 96 97 98
    def search(query)
      joins(:user).merge(User.search(query))
    end

99 100 101 102 103 104 105 106 107 108 109
    def filter_by_2fa(value)
      case value
      when 'enabled'
        left_join_users.merge(User.with_two_factor_indistinct)
      when 'disabled'
        left_join_users.merge(User.without_two_factor)
      else
        all
      end
    end

110
    def sort_by_attribute(method)
111
      case method.to_s
112 113
      when 'access_level_asc' then reorder(access_level: :asc)
      when 'access_level_desc' then reorder(access_level: :desc)
114 115 116 117 118 119 120 121 122
      when 'recent_sign_in' then order_recent_sign_in
      when 'oldest_sign_in' then order_oldest_sign_in
      when 'last_joined' then order_created_desc
      when 'oldest_joined' then order_created_asc
      else
        order_by(method)
      end
    end

123 124 125 126
    def left_join_users
      users = User.arel_table
      members = Member.arel_table

127 128 129
      member_users = members.join(users, Arel::Nodes::OuterJoin)
                             .on(members[:user_id].eq(users[:id]))
                             .join_sources
130 131 132 133

      joins(member_users)
    end

S
Stan Hu 已提交
134
    def access_for_user_ids(user_ids)
A
Adam Niedzielski 已提交
135
      where(user_id: user_ids).has_access.pluck(:user_id, :access_level).to_h
S
Stan Hu 已提交
136 137
    end

138 139 140 141 142
    def find_by_invite_token(invite_token)
      invite_token = Devise.token_generator.digest(self, :invite_token, invite_token)
      find_by(invite_token: invite_token)
    end

R
Rémy Coutable 已提交
143
    def add_user(source, user, access_level, existing_members: nil, current_user: nil, expires_at: nil, ldap: false)
144 145
      # `user` can be either a User object, User ID or an email to be invited
      member = retrieve_member(source, user, existing_members)
146
      access_level = retrieve_access_level(access_level)
147

148 149 150 151 152 153 154 155 156
      return member unless can_update_member?(current_user, member)

      member.attributes = {
        created_by: member.created_by || current_user,
        access_level: access_level,
        expires_at: expires_at
      }

      if member.request?
157 158 159
        ::Members::ApproveAccessRequestService.new(
          current_user,
          access_level: access_level
R
Rémy Coutable 已提交
160 161 162 163 164
        ).execute(
          member,
          skip_authorization: ldap,
          skip_log_audit_event: ldap
        )
165
      else
166
        member.save
167
      end
168

169 170
      member
    end
171

172 173 174
    def add_users(source, users, access_level, current_user: nil, expires_at: nil)
      return [] unless users.present?

175
      emails, users, existing_members = parse_users_list(source, users)
176

177
      self.transaction do
178
        (emails + users).map! do |user|
179 180 181 182
          add_user(
            source,
            user,
            access_level,
183
            existing_members: existing_members,
184 185 186 187 188 189 190
            current_user: current_user,
            expires_at: expires_at
          )
        end
      end
    end

191 192
    def access_levels
      Gitlab::Access.sym_options
193
    end
194 195 196

    private

197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221
    def parse_users_list(source, list)
      emails, user_ids, users = [], [], []
      existing_members = {}

      list.each do |item|
        case item
        when User
          users << item
        when Integer
          user_ids << item
        when /\A\d+\Z/
          user_ids << item.to_i
        when Devise.email_regexp
          emails << item
        end
      end

      if user_ids.present?
        users.concat(User.where(id: user_ids))
        existing_members = source.members_and_requesters.where(user_id: user_ids).index_by(&:user_id)
      end

      [emails, users, existing_members]
    end

222 223 224 225 226 227 228 229
    # This method is used to find users that have been entered into the "Add members" field.
    # These can be the User objects directly, their IDs, their emails, or new emails to be invited.
    def retrieve_user(user)
      return user if user.is_a?(User)

      User.find_by(id: user) || User.find_by(email: user) || user
    end

230 231 232 233 234 235 236 237 238 239 240 241 242 243
    def retrieve_member(source, user, existing_members)
      user = retrieve_user(user)

      if user.is_a?(User)
        if existing_members
          existing_members[user.id] || source.members.build(user_id: user.id)
        else
          source.members_and_requesters.find_or_initialize_by(user_id: user.id)
        end
      else
        source.members.build(invite_email: user)
      end
    end

244 245 246 247
    def retrieve_access_level(access_level)
      access_levels.fetch(access_level) { access_level.to_i }
    end

248
    def can_update_member?(current_user, member)
D
Douwe Maan 已提交
249
      # There is no current user for bulk actions, in which case anything is allowed
250
      !current_user || current_user.can?(:"update_#{member.type.underscore}", member)
251
    end
D
Douwe Maan 已提交
252 253
  end

R
Rémy Coutable 已提交
254 255 256 257
  def real_source_type
    source_type
  end

258 259 260 261
  def access_field
    access_level
  end

D
Douwe Maan 已提交
262 263 264 265
  def invite?
    self.invite_token.present?
  end

266
  def request?
R
Rémy Coutable 已提交
267
    requested_at.present?
268 269
  end

R
Rémy Coutable 已提交
270 271
  def pending?
    invite? || request?
D
Douwe Maan 已提交
272 273
  end

R
Rémy Coutable 已提交
274
  def accept_request
275 276
    return false unless request?

R
Rémy Coutable 已提交
277
    updated = self.update(requested_at: nil)
R
Rémy Coutable 已提交
278
    after_accept_request if updated
279

R
Rémy Coutable 已提交
280
    updated
281 282
  end

D
Douwe Maan 已提交
283
  def accept_invite!(new_user)
D
Douwe Maan 已提交
284
    return false unless invite?
285

D
Douwe Maan 已提交
286 287 288 289 290 291 292 293 294 295 296 297
    self.invite_token = nil
    self.invite_accepted_at = Time.now.utc

    self.user = new_user

    saved = self.save

    after_accept_invite if saved

    saved
  end

D
Douwe Maan 已提交
298 299 300 301 302 303 304 305 306 307
  def decline_invite!
    return false unless invite?

    destroyed = self.destroy

    after_decline_invite if destroyed

    destroyed
  end

D
Douwe Maan 已提交
308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325
  def generate_invite_token
    raw, enc = Devise.token_generator.generate(self.class, :invite_token)
    @raw_invite_token = raw
    self.invite_token = enc
  end

  def generate_invite_token!
    generate_invite_token && save(validate: false)
  end

  def resend_invite
    return unless invite?

    generate_invite_token! unless @raw_invite_token

    send_invite
  end

326
  def create_notification_setting
327
    user.notification_settings.find_or_create_for(source)
328 329
  end

330 331 332 333
  def destroy_notification_setting
    notification_setting&.destroy
  end

334
  def notification_setting
335
    @notification_setting ||= user&.notification_settings_for(source)
336 337
  end

H
http://jneen.net/ 已提交
338
  def notifiable?(type, opts = {})
339 340 341 342 343 344
    # always notify when there isn't a user yet
    return true if user.blank?

    NotificationRecipientService.notifiable?(user, type, notifiable_options.merge(opts))
  end

D
Douwe Maan 已提交
345 346 347 348 349 350
  private

  def send_invite
    # override in subclass
  end

R
Rémy Coutable 已提交
351
  def send_request
R
Rémy Coutable 已提交
352
    notification_service.new_access_request(self)
D
Douwe Maan 已提交
353 354 355 356 357 358 359
  end

  def post_create_hook
    system_hook_service.execute_hooks_for(self, :create)
  end

  def post_update_hook
360
    # override in sub class
D
Douwe Maan 已提交
361 362 363 364 365 366
  end

  def post_destroy_hook
    system_hook_service.execute_hooks_for(self, :destroy)
  end

367 368 369 370 371 372
  # Refreshes authorizations of the current member.
  #
  # This method schedules a job using Sidekiq and as such **must not** be called
  # in a transaction. Doing so can lead to the job running before the
  # transaction has been committed, resulting in the job either throwing an
  # error or not doing any meaningful work.
373
  def refresh_member_authorized_projects
374 375 376
    # If user/source is being destroyed, project access are going to be
    # destroyed eventually because of DB foreign keys, so we shouldn't bother
    # with refreshing after each member is destroyed through association
377 378 379 380 381
    return if destroyed_by_association.present?

    UserProjectAccessChangedService.new(user_id).execute
  end

D
Douwe Maan 已提交
382 383 384 385
  def after_accept_invite
    post_create_hook
  end

D
Douwe Maan 已提交
386 387 388 389
  def after_decline_invite
    # override in subclass
  end

R
Rémy Coutable 已提交
390
  def after_accept_request
D
Douwe Maan 已提交
391 392 393 394 395 396 397 398 399 400
    post_create_hook
  end

  def system_hook_service
    SystemHooksService.new
  end

  def notification_service
    NotificationService.new
  end
401

402 403
  def notifiable_options
    {}
404
  end
D
Dmitriy Zaporozhets 已提交
405
end