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

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

12 13
  attr_accessor :raw_invite_token

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

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

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

39 40 41 42 43 44 45 46 47 48 49
  # 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)

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

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

  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) }
76 77
  scope :maintainers, -> { active.where(access_level: MAINTAINER) }
  scope :masters, -> { maintainers } # @deprecated
78
  scope :owners,  -> { active.where(access_level: OWNER) }
79
  scope :owners_and_maintainers, -> { active.where(access_level: [OWNER, MAINTAINER]) }
80
  scope :owners_and_masters,  -> { owners_and_maintainers } # @deprecated
81
  scope :with_user, -> (user) { where(user: user) }
82

83 84
  scope :with_source_id, ->(source_id) { where(source_id: source_id) }

85 86 87 88
  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')) }
89

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

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

94
  after_create :send_invite, if: :invite?, unless: :importing?
J
James Lopez 已提交
95 96 97 98
  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?]
99
  after_destroy :destroy_notification_setting
100
  after_destroy :post_destroy_hook, unless: :pending?
101
  after_commit :refresh_member_authorized_projects
D
Douwe Maan 已提交
102

103 104
  default_value_for :notification_level, NotificationSetting.levels[:global]

105
  class << self
106 107 108 109
    def search(query)
      joins(:user).merge(User.search(query))
    end

110
    def search_invite_email(query)
111 112 113
      invite.where(['invite_email ILIKE ?', "%#{query}%"])
    end

114 115 116
    def filter_by_2fa(value)
      case value
      when 'enabled'
117
        left_join_users.merge(User.with_two_factor)
118 119 120 121 122 123 124
      when 'disabled'
        left_join_users.merge(User.without_two_factor)
      else
        all
      end
    end

125
    def sort_by_attribute(method)
126
      case method.to_s
127 128
      when 'access_level_asc' then reorder(access_level: :asc)
      when 'access_level_desc' then reorder(access_level: :desc)
129 130 131 132 133 134 135 136 137
      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

138 139 140 141
    def left_join_users
      users = User.arel_table
      members = Member.arel_table

142 143 144
      member_users = members.join(users, Arel::Nodes::OuterJoin)
                             .on(members[:user_id].eq(users[:id]))
                             .join_sources
145 146 147 148

      joins(member_users)
    end

S
Stan Hu 已提交
149
    def access_for_user_ids(user_ids)
A
Adam Niedzielski 已提交
150
      where(user_id: user_ids).has_access.pluck(:user_id, :access_level).to_h
S
Stan Hu 已提交
151 152
    end

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

164 165
      return member unless can_update_member?(current_user, member)

166 167 168 169 170 171 172
      set_member_attributes(
        member,
        access_level,
        current_user: current_user,
        expires_at: expires_at,
        ldap: ldap
      )
173 174

      if member.request?
175 176 177
        ::Members::ApproveAccessRequestService.new(
          current_user,
          access_level: access_level
R
Rémy Coutable 已提交
178 179 180 181 182
        ).execute(
          member,
          skip_authorization: ldap,
          skip_log_audit_event: ldap
        )
183
      else
184
        member.save
185
      end
186

187
      member
188
      # rubocop: enable CodeReuse/ServiceClass
189
    end
190

191 192 193 194 195 196 197 198 199 200 201 202
    # 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

203 204 205
    def add_users(source, users, access_level, current_user: nil, expires_at: nil)
      return [] unless users.present?

206
      emails, users, existing_members = parse_users_list(source, users)
207

208
      self.transaction do
209
        (emails + users).map! do |user|
210 211 212 213
          add_user(
            source,
            user,
            access_level,
214
            existing_members: existing_members,
215 216 217 218 219 220 221
            current_user: current_user,
            expires_at: expires_at
          )
        end
      end
    end

222 223
    def access_levels
      Gitlab::Access.sym_options
224
    end
225 226 227

    private

228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252
    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

253 254 255 256 257 258 259 260
    # 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

261 262 263 264 265 266 267 268 269 270 271 272 273 274
    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

275 276 277 278
    def retrieve_access_level(access_level)
      access_levels.fetch(access_level) { access_level.to_i }
    end

279
    def can_update_member?(current_user, member)
D
Douwe Maan 已提交
280
      # There is no current user for bulk actions, in which case anything is allowed
281
      !current_user || current_user.can?(:"update_#{member.type.underscore}", member)
282
    end
D
Douwe Maan 已提交
283 284
  end

R
Rémy Coutable 已提交
285 286 287 288
  def real_source_type
    source_type
  end

289 290 291 292
  def access_field
    access_level
  end

D
Douwe Maan 已提交
293 294 295 296
  def invite?
    self.invite_token.present?
  end

297
  def request?
R
Rémy Coutable 已提交
298
    requested_at.present?
299 300
  end

R
Rémy Coutable 已提交
301 302
  def pending?
    invite? || request?
D
Douwe Maan 已提交
303 304
  end

R
Rémy Coutable 已提交
305
  def accept_request
306 307
    return false unless request?

R
Rémy Coutable 已提交
308
    updated = self.update(requested_at: nil)
R
Rémy Coutable 已提交
309
    after_accept_request if updated
310

R
Rémy Coutable 已提交
311
    updated
312 313
  end

D
Douwe Maan 已提交
314
  def accept_invite!(new_user)
D
Douwe Maan 已提交
315
    return false unless invite?
316

D
Douwe Maan 已提交
317 318 319 320 321 322 323 324 325 326 327 328
    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 已提交
329 330 331 332 333 334 335 336 337 338
  def decline_invite!
    return false unless invite?

    destroyed = self.destroy

    after_decline_invite if destroyed

    destroyed
  end

D
Douwe Maan 已提交
339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356
  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

357
  def create_notification_setting
358
    user.notification_settings.find_or_create_for(source)
359 360
  end

361 362 363 364
  def destroy_notification_setting
    notification_setting&.destroy
  end

365
  def notification_setting
366
    @notification_setting ||= user&.notification_settings_for(source)
367 368
  end

369
  # rubocop: disable CodeReuse/ServiceClass
H
http://jneen.net/ 已提交
370
  def notifiable?(type, opts = {})
371 372 373 374 375
    # always notify when there isn't a user yet
    return true if user.blank?

    NotificationRecipientService.notifiable?(user, type, notifiable_options.merge(opts))
  end
376
  # rubocop: enable CodeReuse/ServiceClass
377

378 379 380 381 382 383 384 385 386
  # 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 已提交
387 388 389 390 391 392
  private

  def send_invite
    # override in subclass
  end

R
Rémy Coutable 已提交
393
  def send_request
R
Rémy Coutable 已提交
394
    notification_service.new_access_request(self)
D
Douwe Maan 已提交
395 396 397 398 399 400 401
  end

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

  def post_update_hook
402
    # override in sub class
D
Douwe Maan 已提交
403 404 405 406 407 408
  end

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

409 410 411 412 413 414
  # 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.
415
  # rubocop: disable CodeReuse/ServiceClass
416
  def refresh_member_authorized_projects
417 418 419
    # 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
420 421 422 423
    return if destroyed_by_association.present?

    UserProjectAccessChangedService.new(user_id).execute
  end
424
  # rubocop: enable CodeReuse/ServiceClass
425

D
Douwe Maan 已提交
426 427 428 429
  def after_accept_invite
    post_create_hook
  end

D
Douwe Maan 已提交
430 431 432 433
  def after_decline_invite
    # override in subclass
  end

R
Rémy Coutable 已提交
434
  def after_accept_request
D
Douwe Maan 已提交
435 436 437
    post_create_hook
  end

438
  # rubocop: disable CodeReuse/ServiceClass
D
Douwe Maan 已提交
439 440 441
  def system_hook_service
    SystemHooksService.new
  end
442
  # rubocop: enable CodeReuse/ServiceClass
D
Douwe Maan 已提交
443

444
  # rubocop: disable CodeReuse/ServiceClass
D
Douwe Maan 已提交
445 446 447
  def notification_service
    NotificationService.new
  end
448
  # rubocop: enable CodeReuse/ServiceClass
449

450 451
  def notifiable_options
    {}
452
  end
453 454

  def higher_access_level_than_group
455
    if highest_group_member && highest_group_member.access_level > access_level
456 457
      error_parameters = { access: highest_group_member.human_access, group_name: highest_group_member.group.name }

458
      errors.add(:access_level, s_("should be greater than or equal to %{access} inherited membership from group %{group_name}") % error_parameters)
459 460
    end
  end
D
Dmitriy Zaporozhets 已提交
461
end