BigW Consortium Gitlab

member.rb 10.6 KB
Newer Older
1
class Member < ActiveRecord::Base
2
  include Sortable
3
  include Importable
4
  include Expirable
5 6
  include Gitlab::Access

7 8
  attr_accessor :raw_invite_token

9
  belongs_to :created_by, class_name: "User"
10
  belongs_to :user
11
  belongs_to :source, polymorphic: true # rubocop:disable Cop/PolymorphicAssociations
12

Douwe Maan committed
13 14
  delegate :name, :username, :email, to: :user, prefix: true

15
  validates :user, presence: true, unless: :invite?
16
  validates :source, presence: true
17
  validates :user_id, uniqueness: { scope: [:source_type, :source_id],
18 19
                                    message: "already exists in source",
                                    allow_nil: true }
20
  validates :access_level, inclusion: { in: Gitlab::Access.all_values }, presence: true
Douwe Maan committed
21 22 23 24
  validates :invite_email,
    presence: {
      if: :invite?
    },
25
    email: {
Douwe Maan committed
26 27 28 29 30 31
      allow_nil: true
    },
    uniqueness: {
      scope: [:source_type, :source_id],
      allow_nil: true
    }
32

33 34 35 36 37 38 39 40 41 42 43
  # 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)

44 45 46 47 48 49 50 51 52 53 54 55
    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.
  scope :active_without_invites, -> do
    left_join_users
      .where(users: { state: 'active' })
56
      .where(requested_at: nil)
57
      .reorder(nil)
58 59
  end

60
  scope :invite, -> { where.not(invite_token: nil) }
61
  scope :non_invite, -> { where(invite_token: nil) }
62
  scope :request, -> { where.not(requested_at: nil) }
63
  scope :non_request, -> { where(requested_at: nil) }
64 65 66 67 68 69 70 71 72

  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]) }
73

74 75 76 77
  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')) }
78

79
  before_validation :generate_invite_token, on: :create, if: -> (member) { member.invite_email.present? }
80

81
  after_create :send_invite, if: :invite?, unless: :importing?
James Lopez committed
82 83 84 85
  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?]
86
  after_destroy :post_destroy_hook, unless: :pending?
87
  after_commit :refresh_member_authorized_projects
88

89 90
  default_value_for :notification_level, NotificationSetting.levels[:global]

91
  class << self
92 93 94 95 96 97
    def search(query)
      joins(:user).merge(User.search(query))
    end

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

109 110 111 112
    def left_join_users
      users = User.arel_table
      members = Member.arel_table

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

      joins(member_users)
    end

Stan Hu committed
120
    def access_for_user_ids(user_ids)
Adam Niedzielski committed
121
      where(user_id: user_ids).has_access.pluck(:user_id, :access_level).to_h
Stan Hu committed
122 123
    end

124 125 126 127 128
    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

129 130 131
    def add_user(source, user, access_level, existing_members: nil, current_user: nil, expires_at: nil)
      # `user` can be either a User object, User ID or an email to be invited
      member = retrieve_member(source, user, existing_members)
132
      access_level = retrieve_access_level(access_level)
133

134 135 136 137 138 139 140 141 142
      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?
143 144 145 146 147 148
        ::Members::ApproveAccessRequestService.new(
          source,
          current_user,
          id: member.id,
          access_level: access_level
        ).execute
149
      else
150
        member.save
151
      end
152

153 154
      member
    end
155

156 157 158
    def add_users(source, users, access_level, current_user: nil, expires_at: nil)
      return [] unless users.present?

159
      emails, users, existing_members = parse_users_list(source, users)
160

161
      self.transaction do
162
        (emails + users).map! do |user|
163 164 165 166
          add_user(
            source,
            user,
            access_level,
167
            existing_members: existing_members,
168 169 170 171 172 173 174
            current_user: current_user,
            expires_at: expires_at
          )
        end
      end
    end

175 176
    def access_levels
      Gitlab::Access.sym_options
177
    end
178 179 180

    private

181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205
    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

206 207 208 209 210 211 212 213
    # 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

214 215 216 217 218 219 220 221 222 223 224 225 226 227
    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

228 229 230 231
    def retrieve_access_level(access_level)
      access_levels.fetch(access_level) { access_level.to_i }
    end

232
    def can_update_member?(current_user, member)
Douwe Maan committed
233
      # There is no current user for bulk actions, in which case anything is allowed
234
      !current_user || current_user.can?(:"update_#{member.type.underscore}", member)
235
    end
Douwe Maan committed
236 237
  end

238 239 240 241
  def real_source_type
    source_type
  end

242 243 244 245
  def access_field
    access_level
  end

246 247 248 249
  def invite?
    self.invite_token.present?
  end

250
  def request?
251
    requested_at.present?
252 253
  end

254 255
  def pending?
    invite? || request?
256 257
  end

258
  def accept_request
259 260
    return false unless request?

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

264
    updated
265 266
  end

267
  def accept_invite!(new_user)
Douwe Maan committed
268
    return false unless invite?
269

270 271 272 273 274 275 276 277 278 279 280 281
    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 committed
282 283 284 285 286 287 288 289 290 291
  def decline_invite!
    return false unless invite?

    destroyed = self.destroy

    after_decline_invite if destroyed

    destroyed
  end

292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309
  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

310
  def create_notification_setting
311
    user.notification_settings.find_or_create_for(source)
312 313
  end

314
  def notification_setting
315
    @notification_setting ||= user.notification_settings_for(source)
316 317
  end

http://jneen.net/ committed
318
  def notifiable?(type, opts = {})
319 320 321 322 323 324
    # always notify when there isn't a user yet
    return true if user.blank?

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

325 326 327 328 329 330
  private

  def send_invite
    # override in subclass
  end

331
  def send_request
332
    notification_service.new_access_request(self)
333 334 335 336 337 338 339
  end

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

  def post_update_hook
340
    # override in sub class
341 342 343 344 345 346
  end

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

347 348 349 350 351 352
  # 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.
353
  def refresh_member_authorized_projects
354 355 356
    # 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
357 358 359 360 361
    return if destroyed_by_association.present?

    UserProjectAccessChangedService.new(user_id).execute
  end

362 363 364 365
  def after_accept_invite
    post_create_hook
  end

Douwe Maan committed
366 367 368 369
  def after_decline_invite
    # override in subclass
  end

370
  def after_accept_request
371 372 373 374 375 376 377 378 379 380
    post_create_hook
  end

  def system_hook_service
    SystemHooksService.new
  end

  def notification_service
    NotificationService.new
  end
381

382 383
  def notifiable_options
    {}
384
  end
385
end