gitlab-org--gitlab-foss/app/models/member.rb

320 lines
8.9 KiB
Ruby
Raw Normal View History

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
2015-04-10 09:09:37 -04:00
validates :user, presence: true, unless: :invite?
validates :source, presence: true
validates :user_id, uniqueness: { scope: [:source_type, :source_id],
2015-04-10 09:09:37 -04:00
message: "already exists in source",
allow_nil: true }
validates :access_level, inclusion: { in: Gitlab::Access.all_values }, presence: true
2015-11-17 09:49:37 -05:00
validates :invite_email,
presence: {
if: :invite?
},
email: {
2015-11-17 09:49:37 -05:00
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]) }
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')) }
2015-04-10 09:09:37 -04:00
before_validation :generate_invite_token, on: :create, if: -> (member) { member.invite_email.present? }
after_create :send_invite, if: :invite?, unless: :importing?
2016-06-16 08:07:49 -04:00
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_create :refresh_member_authorized_projects, if: :importing?
2016-06-16 08:07:49 -04:00
after_update :post_update_hook, unless: [:pending?, :importing?]
after_destroy :post_destroy_hook, unless: :pending?
2015-04-10 09:09:37 -04:00
delegate :name, :username, :email, to: :user, prefix: true
2015-04-10 09:09:37 -04:00
default_value_for :notification_level, NotificationSetting.levels[:global]
class << self
def search(query)
joins(:user).merge(User.search(query))
end
def sort(method)
case method.to_s
when 'access_level_asc' then reorder(access_level: :asc)
when 'access_level_desc' then reorder(access_level: :desc)
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
def left_join_users
users = User.arel_table
members = Member.arel_table
member_users = members.join(users, Arel::Nodes::OuterJoin).
on(members[:user_id].eq(users[:id])).
join_sources
joins(member_users)
end
2016-07-25 09:21:55 -04:00
def access_for_user_ids(user_ids)
2016-08-04 01:59:14 -04:00
where(user_id: user_ids).has_access.pluck(:user_id, :access_level).to_h
2016-07-25 09:21:55 -04:00
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
def add_user(source, user, access_level, current_user: nil, expires_at: nil)
user = retrieve_user(user)
access_level = retrieve_access_level(access_level)
# `user` can be either a User object or an email to be invited
member =
if user.is_a?(User)
source.members.find_by(user_id: user.id) ||
source.requesters.find_by(user_id: user.id) ||
source.members.build(user_id: user.id)
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?
::Members::ApproveAccessRequestService.new(
source,
current_user,
id: member.id,
access_level: access_level
).execute
else
member.save
end
UserProjectAccessChangedService.new(user.id).execute if user.is_a?(User)
member
end
def access_levels
Gitlab::Access.sym_options
end
private
# 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
def can_update_member?(current_user, member)
2015-11-17 09:49:37 -05:00
# There is no current user for bulk actions, in which case anything is allowed
!current_user || current_user.can?(:"update_#{member.type.underscore}", member)
end
def add_users_to_source(source, users, access_level, current_user: nil, expires_at: nil)
users.each do |user|
add_user(
source,
user,
access_level,
current_user: current_user,
expires_at: expires_at
)
end
end
2015-04-10 09:22:31 -04:00
end
def real_source_type
source_type
end
2015-04-10 09:09:37 -04:00
def invite?
self.invite_token.present?
end
def request?
requested_at.present?
end
def pending?
invite? || request?
2015-04-10 09:09:37 -04:00
end
def accept_request
return false unless request?
updated = self.update(requested_at: nil)
after_accept_request if updated
updated
end
2015-04-10 09:09:37 -04:00
def accept_invite!(new_user)
2015-04-10 09:22:31 -04:00
return false unless invite?
2015-04-10 09:09:37 -04:00
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
2015-04-10 10:37:02 -04:00
def decline_invite!
return false unless invite?
destroyed = self.destroy
after_decline_invite if destroyed
destroyed
end
2015-04-10 09:09:37 -04:00
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
2015-04-10 09:09:37 -04:00
private
def send_invite
# override in subclass
end
def send_request
notification_service.new_access_request(self)
2015-04-10 09:09:37 -04:00
end
def post_create_hook
UserProjectAccessChangedService.new(user.id).execute
2015-04-10 09:09:37 -04:00
system_hook_service.execute_hooks_for(self, :create)
end
def post_update_hook
UserProjectAccessChangedService.new(user.id).execute if access_level_changed?
2015-04-10 09:09:37 -04:00
end
def post_destroy_hook
refresh_member_authorized_projects
2015-04-10 09:09:37 -04:00
system_hook_service.execute_hooks_for(self, :destroy)
end
def refresh_member_authorized_projects
# If user/source is being destroyed, project access are gonna be destroyed eventually
# because of DB foreign keys, so we shouldn't bother with refreshing after each
# member is destroyed through association
return if destroyed_by_association.present?
UserProjectAccessChangedService.new(user_id).execute
end
2015-04-10 09:09:37 -04:00
def after_accept_invite
post_create_hook
end
2015-04-10 10:37:02 -04:00
def after_decline_invite
# override in subclass
end
def after_accept_request
2015-04-10 09:09:37 -04:00
post_create_hook
end
def system_hook_service
SystemHooksService.new
end
def notification_service
NotificationService.new
end
end