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

Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
3
class Member < ActiveRecord::Base
4
  include AfterCommitQueue
5
  include Sortable
6
  include Importable
7
  include Expirable
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
8
  include Gitlab::Access
9
  include Presentable
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
10

11
  attr_accessor :raw_invite_token
12
  attr_accessor :skip_notification
13

14
  belongs_to :created_by, class_name: "User"
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
15
  belongs_to :user
16
  belongs_to :source, polymorphic: true # rubocop:disable Cop/PolymorphicAssociations
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
17

Douwe Maan's avatar
Douwe Maan committed
18 19
  delegate :name, :username, :email, to: :user, prefix: true

Douwe Maan's avatar
Douwe Maan committed
20
  validates :user, presence: true, unless: :invite?
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
21
  validates :source, presence: true
22
  validates :user_id, uniqueness: { scope: [:source_type, :source_id],
Douwe Maan's avatar
Douwe Maan committed
23 24
                                    message: "already exists in source",
                                    allow_nil: true }
25
  validates :access_level, inclusion: { in: Gitlab::Access.all_values }, presence: true
26 27 28 29 30 31 32 33 34 35 36
  validates :invite_email,
    presence: {
      if: :invite?
    },
    email: {
      allow_nil: true
    },
    uniqueness: {
      scope: [:source_type, :source_id],
      allow_nil: true
    }
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
37

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

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

65 66 67
  scope :invite, -> { where.not(invite_token: nil) }
  scope :non_invite, -> { where(invite_token: nil) }
  scope :request, -> { where.not(requested_at: nil) }
68
  scope :non_request, -> { where(requested_at: nil) }
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) }
75 76
  scope :maintainers, -> { active.where(access_level: MAINTAINER) }
  scope :masters, -> { maintainers } # @deprecated
77
  scope :owners,  -> { active.where(access_level: OWNER) }
78 79
  scope :owners_and_maintainers,  -> { active.where(access_level: [OWNER, MAINTAINER]) }
  scope :owners_and_masters,  -> { owners_and_maintainers } # @deprecated
80

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

Douwe Maan's avatar
Douwe Maan committed
86
  before_validation :generate_invite_token, on: :create, if: -> (member) { member.invite_email.present? }
87

88
  after_create :send_invite, if: :invite?, unless: :importing?
James Lopez's avatar
James Lopez committed
89 90 91 92
  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?]
93
  after_destroy :destroy_notification_setting
94
  after_destroy :post_destroy_hook, unless: :pending?
95
  after_commit :refresh_member_authorized_projects
Douwe Maan's avatar
Douwe Maan committed
96

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

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

104 105 106 107 108 109 110 111 112 113 114
    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

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

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

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

      joins(member_users)
    end

Stan Hu's avatar
Stan Hu committed
139 140 141 142
    def access_for_user_ids(user_ids)
      where(user_id: user_ids).has_access.pluck(:user_id, :access_level).to_h
    end

143 144 145 146 147
    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

Lin Jen-Shin's avatar
Lin Jen-Shin committed
148
    def add_user(source, user, access_level, existing_members: nil, current_user: nil, expires_at: nil, ldap: false)
149 150
      # `user` can be either a User object, User ID or an email to be invited
      member = retrieve_member(source, user, existing_members)
151
      access_level = retrieve_access_level(access_level)
152

153 154 155 156 157
      return member unless can_update_member?(current_user, member)

      member.attributes = {
        created_by: member.created_by || current_user,
        access_level: access_level,
Rémy Coutable's avatar
Rémy Coutable committed
158 159 160 161 162
        expires_at: expires_at
      }

      ## EE-only START
      member.attributes = {
Rémy Coutable's avatar
Rémy Coutable committed
163 164
        skip_notification: ldap,
        ldap: ldap
165
      }
Rémy Coutable's avatar
Rémy Coutable committed
166
      ## EE-only END
167 168

      if member.request?
Rémy Coutable's avatar
Rémy Coutable committed
169 170 171
        ::Members::ApproveAccessRequestService.new(
          current_user,
          access_level: access_level
Rémy Coutable's avatar
Rémy Coutable committed
172 173 174 175 176
        ).execute(
          member,
          skip_authorization: ldap,
          skip_log_audit_event: ldap
        )
177
      else
178
        member.save
179 180
      end

181 182
      member
    end
Douwe Maan's avatar
Douwe Maan committed
183

184 185 186
    def add_users(source, users, access_level, current_user: nil, expires_at: nil)
      return [] unless users.present?

187
      emails, users, existing_members = parse_users_list(source, users)
188

189
      self.transaction do
190
        (emails + users).map! do |user|
191 192 193 194
          add_user(
            source,
            user,
            access_level,
195
            existing_members: existing_members,
196 197 198 199 200 201 202
            current_user: current_user,
            expires_at: expires_at
          )
        end
      end
    end

203 204
    def access_levels
      Gitlab::Access.sym_options
205 206 207
    end

    private
208

209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233
    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

234 235 236 237 238 239 240 241
    # 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

242 243 244 245 246 247 248 249 250 251 252 253 254 255
    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

256 257 258 259
    def retrieve_access_level(access_level)
      access_levels.fetch(access_level) { access_level.to_i }
    end

260 261
    def can_update_member?(current_user, member)
      # There is no current user for bulk actions, in which case anything is allowed
262
      !current_user || current_user.can?(:"update_#{member.type.underscore}", member)
263
    end
Douwe Maan's avatar
Douwe Maan committed
264 265
  end

266 267 268 269
  def real_source_type
    source_type
  end

270 271 272 273
  def access_field
    access_level
  end

Douwe Maan's avatar
Douwe Maan committed
274 275 276 277
  def invite?
    self.invite_token.present?
  end

278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294
  def request?
    requested_at.present?
  end

  def pending?
    invite? || request?
  end

  def accept_request
    return false unless request?

    updated = self.update(requested_at: nil)
    after_accept_request if updated

    updated
  end

Douwe Maan's avatar
Douwe Maan committed
295
  def accept_invite!(new_user)
Douwe Maan's avatar
Douwe Maan committed
296
    return false unless invite?
297

Douwe Maan's avatar
Douwe Maan committed
298 299 300 301 302 303 304 305 306 307 308 309
    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

Douwe Maan's avatar
Douwe Maan committed
310 311 312 313 314 315 316 317 318 319
  def decline_invite!
    return false unless invite?

    destroyed = self.destroy

    after_decline_invite if destroyed

    destroyed
  end

Douwe Maan's avatar
Douwe Maan committed
320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337
  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

338
  def create_notification_setting
339
    user.notification_settings.find_or_create_for(source)
340 341
  end

342 343 344 345
  def destroy_notification_setting
    notification_setting&.destroy
  end

346
  def notification_setting
347
    @notification_setting ||= user&.notification_settings_for(source)
348 349
  end

http://jneen.net/'s avatar
http://jneen.net/ committed
350
  def notifiable?(type, opts = {})
351 352 353 354 355 356
    # always notify when there isn't a user yet
    return true if user.blank?

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

Douwe Maan's avatar
Douwe Maan committed
357 358 359 360 361 362
  private

  def send_invite
    # override in subclass
  end

363
  def send_request
364
    notification_service.new_access_request(self)
365 366
  end

Douwe Maan's avatar
Douwe Maan committed
367 368 369 370 371
  def post_create_hook
    system_hook_service.execute_hooks_for(self, :create)
  end

  def post_update_hook
372
    # override in sub class
Douwe Maan's avatar
Douwe Maan committed
373 374 375 376 377 378
  end

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

379 380 381 382 383 384
  # 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.
385
  def refresh_member_authorized_projects
386 387 388
    # 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
389 390 391 392 393
    return if destroyed_by_association.present?

    UserProjectAccessChangedService.new(user_id).execute
  end

Douwe Maan's avatar
Douwe Maan committed
394 395 396 397
  def after_accept_invite
    post_create_hook
  end

Douwe Maan's avatar
Douwe Maan committed
398 399 400 401
  def after_decline_invite
    # override in subclass
  end

402 403 404 405
  def after_accept_request
    post_create_hook
  end

Douwe Maan's avatar
Douwe Maan committed
406 407 408 409 410 411 412
  def system_hook_service
    SystemHooksService.new
  end

  def notification_service
    NotificationService.new
  end
413

414 415
  def notifiable_options
    {}
416
  end
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
417
end