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

D
Dmitriy Zaporozhets 已提交
3
class Member < ActiveRecord::Base
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
    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 80
  scope :owners_and_maintainers,  -> { active.where(access_level: [OWNER, MAINTAINER]) }
  scope :owners_and_masters,  -> { owners_and_maintainers } # @deprecated
81

82 83 84 85
  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')) }
86

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

89
  after_create :send_invite, if: :invite?, unless: :importing?
J
James Lopez 已提交
90 91 92 93
  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?]
94
  after_destroy :destroy_notification_setting
95
  after_destroy :post_destroy_hook, unless: :pending?
96
  after_commit :refresh_member_authorized_projects
D
Douwe Maan 已提交
97

98 99
  default_value_for :notification_level, NotificationSetting.levels[:global]

100
  class << self
101 102 103 104
    def search(query)
      joins(:user).merge(User.search(query))
    end

105 106 107
    def filter_by_2fa(value)
      case value
      when 'enabled'
108
        left_join_users.merge(User.with_two_factor)
109 110 111 112 113 114 115
      when 'disabled'
        left_join_users.merge(User.without_two_factor)
      else
        all
      end
    end

116
    def sort_by_attribute(method)
117
      case method.to_s
118 119
      when 'access_level_asc' then reorder(access_level: :asc)
      when 'access_level_desc' then reorder(access_level: :desc)
120 121 122 123 124 125 126 127 128
      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

129 130 131 132
    def left_join_users
      users = User.arel_table
      members = Member.arel_table

133 134 135
      member_users = members.join(users, Arel::Nodes::OuterJoin)
                             .on(members[:user_id].eq(users[:id]))
                             .join_sources
136 137 138 139

      joins(member_users)
    end

S
Stan Hu 已提交
140
    def access_for_user_ids(user_ids)
A
Adam Niedzielski 已提交
141
      where(user_id: user_ids).has_access.pluck(:user_id, :access_level).to_h
S
Stan Hu 已提交
142 143
    end

144 145 146 147 148
    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 已提交
149
    def add_user(source, user, access_level, existing_members: nil, current_user: nil, expires_at: nil, ldap: false)
150
      # rubocop: disable CodeReuse/ServiceClass
151 152
      # `user` can be either a User object, User ID or an email to be invited
      member = retrieve_member(source, user, existing_members)
153
      access_level = retrieve_access_level(access_level)
154

155 156
      return member unless can_update_member?(current_user, member)

157 158 159 160 161 162 163
      set_member_attributes(
        member,
        access_level,
        current_user: current_user,
        expires_at: expires_at,
        ldap: ldap
      )
164 165

      if member.request?
166 167 168
        ::Members::ApproveAccessRequestService.new(
          current_user,
          access_level: access_level
R
Rémy Coutable 已提交
169 170 171 172 173
        ).execute(
          member,
          skip_authorization: ldap,
          skip_log_audit_event: ldap
        )
174
      else
175
        member.save
176
      end
177

178
      member
179
      # rubocop: enable CodeReuse/ServiceClass
180
    end
181

182 183 184 185 186 187 188 189 190 191 192 193
    # 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

194 195 196
    def add_users(source, users, access_level, current_user: nil, expires_at: nil)
      return [] unless users.present?

197
      emails, users, existing_members = parse_users_list(source, users)
198

199
      self.transaction do
200
        (emails + users).map! do |user|
201 202 203 204
          add_user(
            source,
            user,
            access_level,
205
            existing_members: existing_members,
206 207 208 209 210 211 212
            current_user: current_user,
            expires_at: expires_at
          )
        end
      end
    end

213 214
    def access_levels
      Gitlab::Access.sym_options
215
    end
216 217 218

    private

219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243
    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

244 245 246 247 248 249 250 251
    # 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

252 253 254 255 256 257 258 259 260 261 262 263 264 265
    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

266 267 268 269
    def retrieve_access_level(access_level)
      access_levels.fetch(access_level) { access_level.to_i }
    end

270
    def can_update_member?(current_user, member)
D
Douwe Maan 已提交
271
      # There is no current user for bulk actions, in which case anything is allowed
272
      !current_user || current_user.can?(:"update_#{member.type.underscore}", member)
273
    end
D
Douwe Maan 已提交
274 275
  end

R
Rémy Coutable 已提交
276 277 278 279
  def real_source_type
    source_type
  end

280 281 282 283
  def access_field
    access_level
  end

D
Douwe Maan 已提交
284 285 286 287
  def invite?
    self.invite_token.present?
  end

288
  def request?
R
Rémy Coutable 已提交
289
    requested_at.present?
290 291
  end

R
Rémy Coutable 已提交
292 293
  def pending?
    invite? || request?
D
Douwe Maan 已提交
294 295
  end

R
Rémy Coutable 已提交
296
  def accept_request
297 298
    return false unless request?

R
Rémy Coutable 已提交
299
    updated = self.update(requested_at: nil)
R
Rémy Coutable 已提交
300
    after_accept_request if updated
301

R
Rémy Coutable 已提交
302
    updated
303 304
  end

D
Douwe Maan 已提交
305
  def accept_invite!(new_user)
D
Douwe Maan 已提交
306
    return false unless invite?
307

D
Douwe Maan 已提交
308 309 310 311 312 313 314 315 316 317 318 319
    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 已提交
320 321 322 323 324 325 326 327 328 329
  def decline_invite!
    return false unless invite?

    destroyed = self.destroy

    after_decline_invite if destroyed

    destroyed
  end

D
Douwe Maan 已提交
330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347
  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

348
  def create_notification_setting
349
    user.notification_settings.find_or_create_for(source)
350 351
  end

352 353 354 355
  def destroy_notification_setting
    notification_setting&.destroy
  end

356
  def notification_setting
357
    @notification_setting ||= user&.notification_settings_for(source)
358 359
  end

360
  # rubocop: disable CodeReuse/ServiceClass
H
http://jneen.net/ 已提交
361
  def notifiable?(type, opts = {})
362 363 364 365 366
    # always notify when there isn't a user yet
    return true if user.blank?

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

369 370 371 372 373 374 375 376 377
  # 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 已提交
378 379 380 381 382 383
  private

  def send_invite
    # override in subclass
  end

R
Rémy Coutable 已提交
384
  def send_request
R
Rémy Coutable 已提交
385
    notification_service.new_access_request(self)
D
Douwe Maan 已提交
386 387 388 389 390 391 392
  end

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

  def post_update_hook
393
    # override in sub class
D
Douwe Maan 已提交
394 395 396 397 398 399
  end

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

400 401 402 403 404 405
  # 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.
406
  # rubocop: disable CodeReuse/ServiceClass
407
  def refresh_member_authorized_projects
408 409 410
    # 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
411 412 413 414
    return if destroyed_by_association.present?

    UserProjectAccessChangedService.new(user_id).execute
  end
415
  # rubocop: enable CodeReuse/ServiceClass
416

D
Douwe Maan 已提交
417 418 419 420
  def after_accept_invite
    post_create_hook
  end

D
Douwe Maan 已提交
421 422 423 424
  def after_decline_invite
    # override in subclass
  end

R
Rémy Coutable 已提交
425
  def after_accept_request
D
Douwe Maan 已提交
426 427 428
    post_create_hook
  end

429
  # rubocop: disable CodeReuse/ServiceClass
D
Douwe Maan 已提交
430 431 432
  def system_hook_service
    SystemHooksService.new
  end
433
  # rubocop: enable CodeReuse/ServiceClass
D
Douwe Maan 已提交
434

435
  # rubocop: disable CodeReuse/ServiceClass
D
Douwe Maan 已提交
436 437 438
  def notification_service
    NotificationService.new
  end
439
  # rubocop: enable CodeReuse/ServiceClass
440

441 442
  def notifiable_options
    {}
443
  end
444 445 446 447 448 449 450 451

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

      errors.add(:access_level, s_("should be higher than %{access} inherited membership from group %{group_name}") % error_parameters)
    end
  end
D
Dmitriy Zaporozhets 已提交
452
end