member.rb 13.7 KB
Newer Older
1 2
# frozen_string_literal: true

3
class Member < ApplicationRecord
4
  include EachBatch
D
Douwe Maan 已提交
5
  include AfterCommitQueue
6
  include Sortable
7
  include Importable
8
  include Expirable
D
Dmitriy Zaporozhets 已提交
9
  include Gitlab::Access
T
TM Lee 已提交
10
  include Presentable
11
  include Gitlab::Utils::StrongMemoize
12
  include FromUnion
13
  include UpdateHighestRole
D
Dmitriy Zaporozhets 已提交
14

15 16
  attr_accessor :raw_invite_token

17
  belongs_to :created_by, class_name: "User"
D
Dmitriy Zaporozhets 已提交
18
  belongs_to :user
19
  belongs_to :source, polymorphic: true # rubocop:disable Cop/PolymorphicAssociations
D
Dmitriy Zaporozhets 已提交
20

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

D
Douwe Maan 已提交
23
  validates :user, presence: true, unless: :invite?
D
Dmitriy Zaporozhets 已提交
24
  validates :source, presence: true
25
  validates :user_id, uniqueness: { scope: [:source_type, :source_id],
D
Douwe Maan 已提交
26 27
                                    message: "already exists in source",
                                    allow_nil: true }
28
  validates :access_level, inclusion: { in: Gitlab::Access.all_values }, presence: true
29
  validate :higher_access_level_than_group, unless: :importing?
D
Douwe Maan 已提交
30 31 32 33
  validates :invite_email,
    presence: {
      if: :invite?
    },
34
    devise_email: {
D
Douwe Maan 已提交
35 36 37 38 39 40
      allow_nil: true
    },
    uniqueness: {
      scope: [:source_type, :source_id],
      allow_nil: true
    }
D
Dmitriy Zaporozhets 已提交
41

42 43 44 45 46 47 48 49 50 51 52
  # 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)

53 54 55 56 57 58 59 60 61
    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.
62
  scope :active_without_invites_and_requests, -> do
63 64
    left_join_users
      .where(users: { state: 'active' })
65
      .non_request
66
      .reorder(nil)
67 68
  end

R
Rémy Coutable 已提交
69
  scope :invite, -> { where.not(invite_token: nil) }
R
Rémy Coutable 已提交
70
  scope :non_invite, -> { where(invite_token: nil) }
R
Rémy Coutable 已提交
71
  scope :request, -> { where.not(requested_at: nil) }
72
  scope :non_request, -> { where(requested_at: nil) }
73

74 75
  scope :not_accepted_invitations_by_user, -> (user) { invite.where(invite_accepted_at: nil, created_by: user) }

76 77 78 79 80
  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) }
81
  scope :maintainers, -> { active.where(access_level: MAINTAINER) }
82
  scope :non_guests, -> { where('members.access_level > ?', GUEST) }
83
  scope :owners, -> { active.where(access_level: OWNER) }
84
  scope :owners_and_maintainers, -> { active.where(access_level: [OWNER, MAINTAINER]) }
85
  scope :with_user, -> (user) { where(user: user) }
86

87
  scope :with_source_id, ->(source_id) { where(source_id: source_id) }
88
  scope :including_source, -> { includes(:source) }
89

90 91 92 93
  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')) }
94

95 96
  scope :on_project_and_ancestors, ->(project) { where(source: [project] + project.ancestors) }

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

99
  after_create :send_invite, if: :invite?, unless: :importing?
J
James Lopez 已提交
100 101 102 103
  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?]
104
  after_destroy :destroy_notification_setting
105
  after_destroy :post_destroy_hook, unless: :pending?
106
  after_commit :refresh_member_authorized_projects
D
Douwe Maan 已提交
107

108 109
  default_value_for :notification_level, NotificationSetting.levels[:global]

110
  class << self
111 112 113 114
    def search(query)
      joins(:user).merge(User.search(query))
    end

115
    def search_invite_email(query)
116 117 118
      invite.where(['invite_email ILIKE ?', "%#{query}%"])
    end

119 120 121
    def filter_by_2fa(value)
      case value
      when 'enabled'
122
        left_join_users.merge(User.with_two_factor)
123 124 125 126 127 128 129
      when 'disabled'
        left_join_users.merge(User.without_two_factor)
      else
        all
      end
    end

130
    def sort_by_attribute(method)
131
      case method.to_s
132 133
      when 'access_level_asc' then reorder(access_level: :asc)
      when 'access_level_desc' then reorder(access_level: :desc)
134 135 136 137 138 139 140 141 142
      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

143 144 145 146
    def left_join_users
      users = User.arel_table
      members = Member.arel_table

147 148 149
      member_users = members.join(users, Arel::Nodes::OuterJoin)
                             .on(members[:user_id].eq(users[:id]))
                             .join_sources
150 151 152 153

      joins(member_users)
    end

S
Stan Hu 已提交
154
    def access_for_user_ids(user_ids)
A
Adam Niedzielski 已提交
155
      where(user_id: user_ids).has_access.pluck(:user_id, :access_level).to_h
S
Stan Hu 已提交
156 157
    end

158 159 160 161 162
    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 已提交
163
    def add_user(source, user, access_level, existing_members: nil, current_user: nil, expires_at: nil, ldap: false)
164
      # rubocop: disable CodeReuse/ServiceClass
165 166
      # `user` can be either a User object, User ID or an email to be invited
      member = retrieve_member(source, user, existing_members)
167
      access_level = retrieve_access_level(access_level)
168

169 170
      return member unless can_update_member?(current_user, member)

171 172 173 174 175 176 177
      set_member_attributes(
        member,
        access_level,
        current_user: current_user,
        expires_at: expires_at,
        ldap: ldap
      )
178 179

      if member.request?
180 181 182
        ::Members::ApproveAccessRequestService.new(
          current_user,
          access_level: access_level
R
Rémy Coutable 已提交
183 184 185 186 187
        ).execute(
          member,
          skip_authorization: ldap,
          skip_log_audit_event: ldap
        )
188
      else
189
        member.save
190
      end
191

192
      member
193
      # rubocop: enable CodeReuse/ServiceClass
194
    end
195

196 197 198 199 200 201 202 203 204 205 206 207
    # Populates the attributes of a member.
    #
    # This logic resides in a separate method so that EE can extend this logic,
    # without having to patch the `add_user` method directly.
    def set_member_attributes(member, access_level, current_user: nil, expires_at: nil, ldap: false)
      member.attributes = {
        created_by: member.created_by || current_user,
        access_level: access_level,
        expires_at: expires_at
      }
    end

208 209 210
    def add_users(source, users, access_level, current_user: nil, expires_at: nil)
      return [] unless users.present?

211
      emails, users, existing_members = parse_users_list(source, users)
212

213
      self.transaction do
214
        (emails + users).map! do |user|
215 216 217 218
          add_user(
            source,
            user,
            access_level,
219
            existing_members: existing_members,
220 221 222 223 224 225 226
            current_user: current_user,
            expires_at: expires_at
          )
        end
      end
    end

227 228
    def access_levels
      Gitlab::Access.sym_options
229
    end
230 231 232

    private

233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257
    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

258 259 260 261 262
    # 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)

263 264 265
      return User.find_by(id: user) if user.is_a?(Integer)

      User.find_by(email: user) || user
266 267
    end

268 269 270 271 272 273 274 275 276 277 278 279 280 281
    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

282 283 284 285
    def retrieve_access_level(access_level)
      access_levels.fetch(access_level) { access_level.to_i }
    end

286
    def can_update_member?(current_user, member)
D
Douwe Maan 已提交
287
      # There is no current user for bulk actions, in which case anything is allowed
288
      !current_user || current_user.can?(:"update_#{member.type.underscore}", member)
289
    end
D
Douwe Maan 已提交
290 291
  end

R
Rémy Coutable 已提交
292 293 294 295
  def real_source_type
    source_type
  end

296 297 298 299
  def access_field
    access_level
  end

D
Douwe Maan 已提交
300 301 302 303
  def invite?
    self.invite_token.present?
  end

304
  def request?
R
Rémy Coutable 已提交
305
    requested_at.present?
306 307
  end

R
Rémy Coutable 已提交
308 309
  def pending?
    invite? || request?
D
Douwe Maan 已提交
310 311
  end

R
Rémy Coutable 已提交
312
  def accept_request
313 314
    return false unless request?

R
Rémy Coutable 已提交
315
    updated = self.update(requested_at: nil)
R
Rémy Coutable 已提交
316
    after_accept_request if updated
317

R
Rémy Coutable 已提交
318
    updated
319 320
  end

D
Douwe Maan 已提交
321
  def accept_invite!(new_user)
D
Douwe Maan 已提交
322
    return false unless invite?
323

D
Douwe Maan 已提交
324
    self.invite_token = nil
325
    self.invite_accepted_at = Time.current.utc
D
Douwe Maan 已提交
326 327 328 329 330 331 332 333 334 335

    self.user = new_user

    saved = self.save

    after_accept_invite if saved

    saved
  end

D
Douwe Maan 已提交
336 337 338 339 340 341 342 343 344 345
  def decline_invite!
    return false unless invite?

    destroyed = self.destroy

    after_decline_invite if destroyed

    destroyed
  end

D
Douwe Maan 已提交
346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363
  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

364
  def create_notification_setting
365
    user.notification_settings.find_or_create_for(source)
366 367
  end

368 369 370 371
  def destroy_notification_setting
    notification_setting&.destroy
  end

372
  def notification_setting
373
    @notification_setting ||= user&.notification_settings_for(source)
374 375
  end

376
  # rubocop: disable CodeReuse/ServiceClass
H
http://jneen.net/ 已提交
377
  def notifiable?(type, opts = {})
378 379 380
    # always notify when there isn't a user yet
    return true if user.blank?

381
    NotificationRecipients::BuildService.notifiable?(user, type, notifiable_options.merge(opts))
382
  end
383
  # rubocop: enable CodeReuse/ServiceClass
384

385 386 387 388 389 390 391 392 393
  # Find the user's group member with a highest access level
  def highest_group_member
    strong_memoize(:highest_group_member) do
      next unless user_id && source&.ancestors&.any?

      GroupMember.where(source: source.ancestors, user_id: user_id).order(:access_level).last
    end
  end

D
Douwe Maan 已提交
394 395 396 397 398 399
  private

  def send_invite
    # override in subclass
  end

R
Rémy Coutable 已提交
400
  def send_request
R
Rémy Coutable 已提交
401
    notification_service.new_access_request(self)
D
Douwe Maan 已提交
402 403 404 405 406 407 408
  end

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

  def post_update_hook
409
    system_hook_service.execute_hooks_for(self, :update)
D
Douwe Maan 已提交
410 411 412 413 414 415
  end

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

416 417 418 419 420 421
  # 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.
422
  # rubocop: disable CodeReuse/ServiceClass
423
  def refresh_member_authorized_projects
424 425 426
    # 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
427 428 429 430
    return if destroyed_by_association.present?

    UserProjectAccessChangedService.new(user_id).execute
  end
431
  # rubocop: enable CodeReuse/ServiceClass
432

D
Douwe Maan 已提交
433 434 435 436
  def after_accept_invite
    post_create_hook
  end

D
Douwe Maan 已提交
437 438 439 440
  def after_decline_invite
    # override in subclass
  end

R
Rémy Coutable 已提交
441
  def after_accept_request
D
Douwe Maan 已提交
442 443 444
    post_create_hook
  end

445
  # rubocop: disable CodeReuse/ServiceClass
D
Douwe Maan 已提交
446 447 448
  def system_hook_service
    SystemHooksService.new
  end
449
  # rubocop: enable CodeReuse/ServiceClass
D
Douwe Maan 已提交
450

451
  # rubocop: disable CodeReuse/ServiceClass
D
Douwe Maan 已提交
452 453 454
  def notification_service
    NotificationService.new
  end
455
  # rubocop: enable CodeReuse/ServiceClass
456

457 458
  def notifiable_options
    {}
459
  end
460 461

  def higher_access_level_than_group
462
    if highest_group_member && highest_group_member.access_level > access_level
463 464
      error_parameters = { access: highest_group_member.human_access, group_name: highest_group_member.group.name }

465
      errors.add(:access_level, s_("should be greater than or equal to %{access} inherited membership from group %{group_name}") % error_parameters)
466 467
    end
  end
468

469
  def update_highest_role?
470 471
    return unless user_id.present?

472 473 474 475 476
    previous_changes[:access_level].present?
  end

  def update_highest_role_attribute
    user_id
477
  end
D
Dmitriy Zaporozhets 已提交
478
end
479 480

Member.prepend_if_ee('EE::Member')