member.rb 9.3 KB
Newer Older
D
Dmitriy Zaporozhets 已提交
1
class Member < ActiveRecord::Base
2
  include Sortable
3
  include Importable
4
  include Expirable
D
Dmitriy Zaporozhets 已提交
5 6
  include Gitlab::Access

7 8
  attr_accessor :raw_invite_token

9
  belongs_to :created_by, class_name: "User"
D
Dmitriy Zaporozhets 已提交
10 11 12
  belongs_to :user
  belongs_to :source, polymorphic: true

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

D
Douwe Maan 已提交
15
  validates :user, presence: true, unless: :invite?
D
Dmitriy Zaporozhets 已提交
16
  validates :source, presence: true
17
  validates :user_id, uniqueness: { scope: [:source_type, :source_id],
D
Douwe Maan 已提交
18 19
                                    message: "already exists in source",
                                    allow_nil: true }
20
  validates :access_level, inclusion: { in: Gitlab::Access.all_values }, presence: true
D
Douwe Maan 已提交
21 22 23 24
  validates :invite_email,
    presence: {
      if: :invite?
    },
25
    email: {
D
Douwe Maan 已提交
26 27 28 29 30 31
      allow_nil: true
    },
    uniqueness: {
      scope: [:source_type, :source_id],
      allow_nil: true
    }
D
Dmitriy Zaporozhets 已提交
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)

D
Douwe Maan 已提交
44 45 46
    includes(:user).references(:users)
      .where(is_external_invite.or(user_is_active))
      .where(requested_at: nil)
47 48
  end

R
Rémy Coutable 已提交
49
  scope :invite, -> { where.not(invite_token: nil) }
R
Rémy Coutable 已提交
50
  scope :non_invite, -> { where(invite_token: nil) }
R
Rémy Coutable 已提交
51
  scope :request, -> { where.not(requested_at: nil) }
52
  scope :non_request, -> { where(requested_at: nil) }
53 54 55 56 57 58 59 60 61

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

63 64 65 66
  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')) }
67

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

70
  after_create :send_invite, if: :invite?, unless: :importing?
J
James Lopez 已提交
71 72 73 74
  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?]
75
  after_destroy :post_destroy_hook, unless: :pending?
76
  after_commit :refresh_member_authorized_projects
D
Douwe Maan 已提交
77

78 79
  default_value_for :notification_level, NotificationSetting.levels[:global]

80
  class << self
81 82 83 84 85 86
    def search(query)
      joins(:user).merge(User.search(query))
    end

    def sort(method)
      case method.to_s
87 88
      when 'access_level_asc' then reorder(access_level: :asc)
      when 'access_level_desc' then reorder(access_level: :desc)
89 90 91 92 93 94 95 96 97
      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

98 99 100 101
    def left_join_users
      users = User.arel_table
      members = Member.arel_table

102 103 104
      member_users = members.join(users, Arel::Nodes::OuterJoin).
                             on(members[:user_id].eq(users[:id])).
                             join_sources
105 106 107 108

      joins(member_users)
    end

S
Stan Hu 已提交
109
    def access_for_user_ids(user_ids)
A
Adam Niedzielski 已提交
110
      where(user_id: user_ids).has_access.pluck(:user_id, :access_level).to_h
S
Stan Hu 已提交
111 112
    end

113 114 115 116 117
    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

118 119 120
    def add_user(source, user, access_level, current_user: nil, expires_at: nil)
      user = retrieve_user(user)
      access_level = retrieve_access_level(access_level)
121

122
      # `user` can be either a User object or an email to be invited
123 124 125
      member =
        if user.is_a?(User)
          source.members.find_by(user_id: user.id) ||
126 127
            source.requesters.find_by(user_id: user.id) ||
            source.members.build(user_id: user.id)
128 129 130 131 132 133 134 135 136 137 138 139 140
        else
          source.members.build(invite_email: user)
        end

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

151 152
      member
    end
153

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

157 158 159 160 161
      # Collect all user ids into separate array
      # so we can use single sql query to get user objects
      user_ids = users.select { |user| user =~ /\A\d+\Z/ }
      users = users - user_ids + User.where(id: user_ids)

162 163 164 165 166 167 168 169 170 171 172 173 174
      self.transaction do
        users.map do |user|
          add_user(
            source,
            user,
            access_level,
            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
    # 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

    def retrieve_access_level(access_level)
      access_levels.fetch(access_level) { access_level.to_i }
    end

193
    def can_update_member?(current_user, member)
D
Douwe Maan 已提交
194
      # There is no current user for bulk actions, in which case anything is allowed
195
      !current_user || current_user.can?(:"update_#{member.type.underscore}", member)
196
    end
D
Douwe Maan 已提交
197 198
  end

R
Rémy Coutable 已提交
199 200 201 202
  def real_source_type
    source_type
  end

203 204 205 206
  def access_field
    access_level
  end

D
Douwe Maan 已提交
207 208 209 210
  def invite?
    self.invite_token.present?
  end

211
  def request?
R
Rémy Coutable 已提交
212
    requested_at.present?
213 214
  end

R
Rémy Coutable 已提交
215 216
  def pending?
    invite? || request?
D
Douwe Maan 已提交
217 218
  end

R
Rémy Coutable 已提交
219
  def accept_request
220 221
    return false unless request?

R
Rémy Coutable 已提交
222
    updated = self.update(requested_at: nil)
R
Rémy Coutable 已提交
223
    after_accept_request if updated
224

R
Rémy Coutable 已提交
225
    updated
226 227
  end

D
Douwe Maan 已提交
228
  def accept_invite!(new_user)
D
Douwe Maan 已提交
229
    return false unless invite?
230

D
Douwe Maan 已提交
231 232 233 234 235 236 237 238 239 240 241 242
    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 已提交
243 244 245 246 247 248 249 250 251 252
  def decline_invite!
    return false unless invite?

    destroyed = self.destroy

    after_decline_invite if destroyed

    destroyed
  end

D
Douwe Maan 已提交
253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270
  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

271
  def create_notification_setting
272
    user.notification_settings.find_or_create_for(source)
273 274
  end

275
  def notification_setting
276
    @notification_setting ||= user.notification_settings_for(source)
277 278
  end

D
Douwe Maan 已提交
279 280 281 282 283 284
  private

  def send_invite
    # override in subclass
  end

R
Rémy Coutable 已提交
285
  def send_request
R
Rémy Coutable 已提交
286
    notification_service.new_access_request(self)
D
Douwe Maan 已提交
287 288 289 290 291 292 293
  end

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

  def post_update_hook
294
    # override in sub class
D
Douwe Maan 已提交
295 296 297 298 299 300
  end

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

301 302 303 304 305 306
  # 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.
307
  def refresh_member_authorized_projects
308 309 310
    # 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
311 312 313 314 315
    return if destroyed_by_association.present?

    UserProjectAccessChangedService.new(user_id).execute
  end

D
Douwe Maan 已提交
316 317 318 319
  def after_accept_invite
    post_create_hook
  end

D
Douwe Maan 已提交
320 321 322 323
  def after_decline_invite
    # override in subclass
  end

R
Rémy Coutable 已提交
324
  def after_accept_request
D
Douwe Maan 已提交
325 326 327 328 329 330 331 332 333 334
    post_create_hook
  end

  def system_hook_service
    SystemHooksService.new
  end

  def notification_service
    NotificationService.new
  end
D
Dmitriy Zaporozhets 已提交
335
end