member.rb 10.8 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 99 100
    def search(query)
      joins(:user).merge(User.search(query))
    end

    def sort(method)
      case method.to_s
101 102
      when 'access_level_asc' then reorder(access_level: :asc)
      when 'access_level_desc' then reorder(access_level: :desc)
103 104 105 106 107 108 109 110 111
      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

112 113 114 115
    def left_join_users
      users = User.arel_table
      members = Member.arel_table

116 117 118
      member_users = members.join(users, Arel::Nodes::OuterJoin)
                             .on(members[:user_id].eq(users[:id]))
                             .join_sources
119 120 121 122

      joins(member_users)
    end

S
Stan Hu 已提交
123
    def access_for_user_ids(user_ids)
A
Adam Niedzielski 已提交
124
      where(user_id: user_ids).has_access.pluck(:user_id, :access_level).to_h
S
Stan Hu 已提交
125 126
    end

127 128 129 130 131
    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 已提交
132
    def add_user(source, user, access_level, existing_members: nil, current_user: nil, expires_at: nil, ldap: false)
133 134
      # `user` can be either a User object, User ID or an email to be invited
      member = retrieve_member(source, user, existing_members)
135
      access_level = retrieve_access_level(access_level)
136

137 138 139 140 141 142 143 144 145
      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?
146 147 148
        ::Members::ApproveAccessRequestService.new(
          current_user,
          access_level: access_level
R
Rémy Coutable 已提交
149 150 151 152 153
        ).execute(
          member,
          skip_authorization: ldap,
          skip_log_audit_event: ldap
        )
154
      else
155
        member.save
156
      end
157

158 159
      member
    end
160

161 162 163
    def add_users(source, users, access_level, current_user: nil, expires_at: nil)
      return [] unless users.present?

164
      emails, users, existing_members = parse_users_list(source, users)
165

166
      self.transaction do
167
        (emails + users).map! do |user|
168 169 170 171
          add_user(
            source,
            user,
            access_level,
172
            existing_members: existing_members,
173 174 175 176 177 178 179
            current_user: current_user,
            expires_at: expires_at
          )
        end
      end
    end

180 181
    def access_levels
      Gitlab::Access.sym_options
182
    end
183 184 185

    private

186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210
    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

211 212 213 214 215 216 217 218
    # 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

219 220 221 222 223 224 225 226 227 228 229 230 231 232
    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

233 234 235 236
    def retrieve_access_level(access_level)
      access_levels.fetch(access_level) { access_level.to_i }
    end

237
    def can_update_member?(current_user, member)
D
Douwe Maan 已提交
238
      # There is no current user for bulk actions, in which case anything is allowed
239
      !current_user || current_user.can?(:"update_#{member.type.underscore}", member)
240
    end
D
Douwe Maan 已提交
241 242
  end

R
Rémy Coutable 已提交
243 244 245 246
  def real_source_type
    source_type
  end

247 248 249 250
  def access_field
    access_level
  end

D
Douwe Maan 已提交
251 252 253 254
  def invite?
    self.invite_token.present?
  end

255
  def request?
R
Rémy Coutable 已提交
256
    requested_at.present?
257 258
  end

R
Rémy Coutable 已提交
259 260
  def pending?
    invite? || request?
D
Douwe Maan 已提交
261 262
  end

R
Rémy Coutable 已提交
263
  def accept_request
264 265
    return false unless request?

R
Rémy Coutable 已提交
266
    updated = self.update(requested_at: nil)
R
Rémy Coutable 已提交
267
    after_accept_request if updated
268

R
Rémy Coutable 已提交
269
    updated
270 271
  end

D
Douwe Maan 已提交
272
  def accept_invite!(new_user)
D
Douwe Maan 已提交
273
    return false unless invite?
274

D
Douwe Maan 已提交
275 276 277 278 279 280 281 282 283 284 285 286
    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 已提交
287 288 289 290 291 292 293 294 295 296
  def decline_invite!
    return false unless invite?

    destroyed = self.destroy

    after_decline_invite if destroyed

    destroyed
  end

D
Douwe Maan 已提交
297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314
  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

315
  def create_notification_setting
316
    user.notification_settings.find_or_create_for(source)
317 318
  end

319 320 321 322
  def destroy_notification_setting
    notification_setting&.destroy
  end

323
  def notification_setting
324
    @notification_setting ||= user&.notification_settings_for(source)
325 326
  end

H
http://jneen.net/ 已提交
327
  def notifiable?(type, opts = {})
328 329 330 331 332 333
    # 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 已提交
334 335 336 337 338 339
  private

  def send_invite
    # override in subclass
  end

R
Rémy Coutable 已提交
340
  def send_request
R
Rémy Coutable 已提交
341
    notification_service.new_access_request(self)
D
Douwe Maan 已提交
342 343 344 345 346 347 348
  end

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

  def post_update_hook
349
    # override in sub class
D
Douwe Maan 已提交
350 351 352 353 354 355
  end

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

356 357 358 359 360 361
  # 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.
362
  def refresh_member_authorized_projects
363 364 365
    # 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
366 367 368 369 370
    return if destroyed_by_association.present?

    UserProjectAccessChangedService.new(user_id).execute
  end

D
Douwe Maan 已提交
371 372 373 374
  def after_accept_invite
    post_create_hook
  end

D
Douwe Maan 已提交
375 376 377 378
  def after_decline_invite
    # override in subclass
  end

R
Rémy Coutable 已提交
379
  def after_accept_request
D
Douwe Maan 已提交
380 381 382 383 384 385 386 387 388 389
    post_create_hook
  end

  def system_hook_service
    SystemHooksService.new
  end

  def notification_service
    NotificationService.new
  end
390

391 392
  def notifiable_options
    {}
393
  end
D
Dmitriy Zaporozhets 已提交
394
end