9521edb4f4
An unapproved access request should not give access rights, and blocked users should not be considered members of anything. One visible outcome of this behaviour is that owners and masters of a group or project may be blocked, yet still receive notification emails for access requests. This commit prevents this from happening.
246 lines
6.4 KiB
Ruby
246 lines
6.4 KiB
Ruby
class Member < ActiveRecord::Base
|
|
include Sortable
|
|
include Importable
|
|
include Expirable
|
|
include Gitlab::Access
|
|
|
|
attr_accessor :raw_invite_token
|
|
|
|
belongs_to :created_by, class_name: "User"
|
|
belongs_to :user
|
|
belongs_to :source, polymorphic: true
|
|
|
|
validates :user, presence: true, unless: :invite?
|
|
validates :source, presence: true
|
|
validates :user_id, uniqueness: { scope: [:source_type, :source_id],
|
|
message: "already exists in source",
|
|
allow_nil: true }
|
|
validates :access_level, inclusion: { in: Gitlab::Access.all_values }, presence: true
|
|
validates :invite_email,
|
|
presence: {
|
|
if: :invite?
|
|
},
|
|
email: {
|
|
allow_nil: true
|
|
},
|
|
uniqueness: {
|
|
scope: [:source_type, :source_id],
|
|
allow_nil: true
|
|
}
|
|
|
|
# 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)
|
|
|
|
includes(:user).references(:users)
|
|
.where(is_external_invite.or(user_is_active))
|
|
.where(requested_at: nil)
|
|
end
|
|
|
|
scope :invite, -> { where.not(invite_token: nil) }
|
|
scope :non_invite, -> { where(invite_token: nil) }
|
|
scope :request, -> { where.not(requested_at: nil) }
|
|
|
|
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]) }
|
|
|
|
before_validation :generate_invite_token, on: :create, if: -> (member) { member.invite_email.present? }
|
|
|
|
after_create :send_invite, if: :invite?, unless: :importing?
|
|
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?]
|
|
after_destroy :post_destroy_hook, unless: :pending?
|
|
|
|
delegate :name, :username, :email, to: :user, prefix: true
|
|
|
|
default_value_for :notification_level, NotificationSetting.levels[:global]
|
|
|
|
class << self
|
|
def access_for_user_ids(user_ids)
|
|
where(user_id: user_ids).has_access.pluck(:user_id, :access_level).to_h
|
|
end
|
|
|
|
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
|
|
|
|
# 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 user_for_id(user_id)
|
|
return user_id if user_id.is_a?(User)
|
|
|
|
user = User.find_by(id: user_id)
|
|
user ||= User.find_by(email: user_id)
|
|
user ||= user_id
|
|
user
|
|
end
|
|
|
|
def add_user(members, user_id, access_level, current_user: nil, expires_at: nil)
|
|
user = user_for_id(user_id)
|
|
|
|
# `user` can be either a User object or an email to be invited
|
|
if user.is_a?(User)
|
|
member = members.find_or_initialize_by(user_id: user.id)
|
|
else
|
|
member = members.build
|
|
member.invite_email = user
|
|
end
|
|
|
|
if can_update_member?(current_user, member) || project_creator?(member, access_level)
|
|
member.created_by ||= current_user
|
|
member.access_level = access_level
|
|
member.expires_at = expires_at
|
|
|
|
member.save
|
|
end
|
|
end
|
|
|
|
private
|
|
|
|
def can_update_member?(current_user, member)
|
|
# There is no current user for bulk actions, in which case anything is allowed
|
|
!current_user ||
|
|
current_user.can?(:update_group_member, member) ||
|
|
current_user.can?(:update_project_member, member)
|
|
end
|
|
|
|
def project_creator?(member, access_level)
|
|
member.new_record? && member.owner? &&
|
|
access_level.to_i == ProjectMember::MASTER
|
|
end
|
|
end
|
|
|
|
def real_source_type
|
|
source_type
|
|
end
|
|
|
|
def invite?
|
|
self.invite_token.present?
|
|
end
|
|
|
|
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
|
|
|
|
def accept_invite!(new_user)
|
|
return false unless invite?
|
|
|
|
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
|
|
|
|
def decline_invite!
|
|
return false unless invite?
|
|
|
|
destroyed = self.destroy
|
|
|
|
after_decline_invite if destroyed
|
|
|
|
destroyed
|
|
end
|
|
|
|
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
|
|
|
|
def create_notification_setting
|
|
user.notification_settings.find_or_create_for(source)
|
|
end
|
|
|
|
def notification_setting
|
|
@notification_setting ||= user.notification_settings_for(source)
|
|
end
|
|
|
|
private
|
|
|
|
def send_invite
|
|
# override in subclass
|
|
end
|
|
|
|
def send_request
|
|
notification_service.new_access_request(self)
|
|
end
|
|
|
|
def post_create_hook
|
|
system_hook_service.execute_hooks_for(self, :create)
|
|
end
|
|
|
|
def post_update_hook
|
|
# override in subclass
|
|
end
|
|
|
|
def post_destroy_hook
|
|
system_hook_service.execute_hooks_for(self, :destroy)
|
|
end
|
|
|
|
def after_accept_invite
|
|
post_create_hook
|
|
end
|
|
|
|
def after_decline_invite
|
|
# override in subclass
|
|
end
|
|
|
|
def after_accept_request
|
|
post_create_hook
|
|
end
|
|
|
|
def system_hook_service
|
|
SystemHooksService.new
|
|
end
|
|
|
|
def notification_service
|
|
NotificationService.new
|
|
end
|
|
end
|