member.rb 9.31 KB
Newer Older
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
1
class Member < ActiveRecord::Base
2
  include Sortable
3
  include Importable
4
  include Expirable
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
5 6
  include Gitlab::Access

7 8
  attr_accessor :raw_invite_token

9
  belongs_to :created_by, class_name: "User"
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
10 11 12
  belongs_to :user
  belongs_to :source, polymorphic: true

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

Douwe Maan's avatar
Douwe Maan committed
15
  validates :user, presence: true, unless: :invite?
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
16
  validates :source, presence: true
17
  validates :user_id, uniqueness: { scope: [:source_type, :source_id],
Douwe Maan's avatar
Douwe Maan committed
18 19
                                    message: "already exists in source",
                                    allow_nil: true }
20
  validates :access_level, inclusion: { in: Gitlab::Access.all_values }, presence: true
Douwe Maan's avatar
Douwe Maan committed
21 22 23 24
  validates :invite_email,
    presence: {
      if: :invite?
    },
25
    email: {
Douwe Maan's avatar
Douwe Maan committed
26 27 28 29 30 31
      allow_nil: true
    },
    uniqueness: {
      scope: [:source_type, :source_id],
      allow_nil: true
    }
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
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
    includes(:user).references(:users)
      .where(is_external_invite.or(user_is_active))
      .where(requested_at: nil)
47 48
  end

49
  scope :invite, -> { where.not(invite_token: nil) }
50
  scope :non_invite, -> { where(invite_token: nil) }
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

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

70
  after_create :send_invite, if: :invite?, unless: :importing?
James Lopez's avatar
James Lopez committed
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
Douwe Maan's avatar
Douwe Maan committed
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

Stan Hu's avatar
Stan Hu committed
109
    def access_for_user_ids(user_ids)
Adam Niedzielski's avatar
Adam Niedzielski committed
110
      where(user_id: user_ids).has_access.pluck(:user_id, :access_level).to_h
Stan Hu's avatar
Stan Hu committed
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)
Douwe Maan's avatar
Douwe Maan committed
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
Douwe Maan's avatar
Douwe Maan committed
197 198
  end

199 200 201 202
  def real_source_type
    source_type
  end

Douwe Maan's avatar
Douwe Maan committed
203 204 205 206
  def invite?
    self.invite_token.present?
  end

207
  def request?
208
    requested_at.present?
209 210
  end

211 212
  def pending?
    invite? || request?
Douwe Maan's avatar
Douwe Maan committed
213 214
  end

215
  def accept_request
216 217
    return false unless request?

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

221
    updated
222 223
  end

Douwe Maan's avatar
Douwe Maan committed
224
  def accept_invite!(new_user)
Douwe Maan's avatar
Douwe Maan committed
225
    return false unless invite?
226

Douwe Maan's avatar
Douwe Maan committed
227 228 229 230 231 232 233 234 235 236 237 238
    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
239 240 241 242 243 244 245 246 247 248
  def decline_invite!
    return false unless invite?

    destroyed = self.destroy

    after_decline_invite if destroyed

    destroyed
  end

Douwe Maan's avatar
Douwe Maan committed
249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266
  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

267
  def create_notification_setting
268
    user.notification_settings.find_or_create_for(source)
269 270
  end

271
  def notification_setting
272
    @notification_setting ||= user.notification_settings_for(source)
273 274
  end

Douwe Maan's avatar
Douwe Maan committed
275 276 277 278 279 280
  private

  def send_invite
    # override in subclass
  end

281
  def send_request
282
    notification_service.new_access_request(self)
Douwe Maan's avatar
Douwe Maan committed
283 284 285 286 287 288 289
  end

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

  def post_update_hook
290
    # override in sub class
Douwe Maan's avatar
Douwe Maan committed
291 292 293 294 295 296
  end

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

297 298 299 300 301 302
  # 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.
303
  def refresh_member_authorized_projects
304 305 306
    # 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
307 308 309 310 311
    return if destroyed_by_association.present?

    UserProjectAccessChangedService.new(user_id).execute
  end

Douwe Maan's avatar
Douwe Maan committed
312 313 314 315
  def after_accept_invite
    post_create_hook
  end

Douwe Maan's avatar
Douwe Maan committed
316 317 318 319
  def after_decline_invite
    # override in subclass
  end

320
  def after_accept_request
Douwe Maan's avatar
Douwe Maan committed
321 322 323 324 325 326 327 328 329 330
    post_create_hook
  end

  def system_hook_service
    SystemHooksService.new
  end

  def notification_service
    NotificationService.new
  end
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
331
end