debian-mirror-gitlab/app/models/member.rb

586 lines
17 KiB
Ruby
Raw Normal View History

2018-11-18 11:00:15 +05:30
# frozen_string_literal: true
2019-07-07 11:18:12 +05:30
class Member < ApplicationRecord
2020-05-24 23:13:21 +05:30
include EachBatch
2018-03-17 18:26:18 +05:30
include AfterCommitQueue
2015-04-26 12:48:37 +05:30
include Sortable
2016-06-22 15:30:34 +05:30
include Importable
2020-10-04 03:57:07 +05:30
include CreatedAtFilterable
2016-09-13 17:45:13 +05:30
include Expirable
2015-04-26 12:48:37 +05:30
include Gitlab::Access
2018-03-17 18:26:18 +05:30
include Presentable
2019-02-15 15:39:39 +05:30
include Gitlab::Utils::StrongMemoize
2019-10-31 01:37:42 +05:30
include FromUnion
2020-04-22 19:07:51 +05:30
include UpdateHighestRole
2015-04-26 12:48:37 +05:30
2021-03-08 18:12:59 +05:30
AVATAR_SIZE = 40
2021-09-04 01:27:46 +05:30
ACCESS_REQUEST_APPROVERS_TO_BE_NOTIFIED_LIMIT = 10
2021-03-08 18:12:59 +05:30
2015-04-26 12:48:37 +05:30
attr_accessor :raw_invite_token
belongs_to :created_by, class_name: "User"
belongs_to :user
2017-09-10 17:25:29 +05:30
belongs_to :source, polymorphic: true # rubocop:disable Cop/PolymorphicAssociations
2015-04-26 12:48:37 +05:30
2017-08-17 22:00:37 +05:30
delegate :name, :username, :email, to: :user, prefix: true
2020-10-04 03:57:07 +05:30
validates :expires_at, allow_blank: true, future_date: true
2015-04-26 12:48:37 +05:30
validates :user, presence: true, unless: :invite?
validates :source, presence: true
2015-11-26 14:37:03 +05:30
validates :user_id, uniqueness: { scope: [:source_type, :source_id],
2015-04-26 12:48:37 +05:30
message: "already exists in source",
allow_nil: true }
2019-02-15 15:39:39 +05:30
validate :higher_access_level_than_group, unless: :importing?
2015-11-26 14:37:03 +05:30
validates :invite_email,
presence: {
if: :invite?
},
2019-07-07 11:18:12 +05:30
devise_email: {
2015-11-26 14:37:03 +05:30
allow_nil: true
},
uniqueness: {
scope: [:source_type, :source_id],
allow_nil: true
}
2020-07-28 23:09:34 +05:30
validates :user_id,
uniqueness: {
message: _('project bots cannot be added to other groups / projects')
},
if: :project_bot?
2015-04-26 12:48:37 +05:30
2021-02-11 23:33:58 +05:30
scope :in_hierarchy, ->(source) do
groups = source.root_ancestor.self_and_descendants
group_members = Member.default_scoped.where(source: groups)
projects = source.root_ancestor.all_projects
project_members = Member.default_scoped.where(source: projects)
Member.default_scoped.from_union([
group_members,
project_members
]).merge(self)
end
2016-09-29 09:46:39 +05:30
# 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)
2017-09-10 17:25:29 +05:30
user_ok = Arel::Nodes::Grouping.new(is_external_invite).or(user_is_active)
left_join_users
.where(user_ok)
2021-04-17 20:07:23 +05:30
.non_request
.non_minimal_access
.reorder(nil)
end
scope :blocked, -> do
is_external_invite = arel_table[:user_id].eq(nil).and(arel_table[:invite_token].not_eq(nil))
user_is_blocked = User.arel_table[:state].eq(:blocked)
left_join_users
2021-06-08 01:23:25 +05:30
.where(user_is_blocked)
.where.not(is_external_invite)
2021-04-17 20:07:23 +05:30
.non_request
2020-11-24 15:15:51 +05:30
.non_minimal_access
2017-09-10 17:25:29 +05:30
.reorder(nil)
end
2021-06-08 01:23:25 +05:30
scope :connected_to_user, -> { where.not(user_id: nil) }
# This scope is exclusively used to get the members
# that can possibly have project_authorization records
# to projects/groups.
scope :authorizable, -> do
connected_to_user
.non_request
.non_minimal_access
end
2017-09-10 17:25:29 +05:30
# Like active, but without invites. For when a User is required.
2018-04-04 21:44:52 +05:30
scope :active_without_invites_and_requests, -> do
2017-09-10 17:25:29 +05:30
left_join_users
.where(users: { state: 'active' })
2021-09-04 01:27:46 +05:30
.without_invites_and_requests
.reorder(nil)
end
scope :without_invites_and_requests, -> do
non_request
2020-11-24 15:15:51 +05:30
.non_invite
.non_minimal_access
2016-09-29 09:46:39 +05:30
end
scope :invite, -> { where.not(invite_token: nil) }
scope :non_invite, -> { where(invite_token: nil) }
2021-02-11 23:33:58 +05:30
scope :request, -> { where.not(requested_at: nil) }
2017-08-17 22:00:37 +05:30
scope :non_request, -> { where(requested_at: nil) }
2016-09-29 09:46:39 +05:30
2021-01-03 14:25:43 +05:30
scope :not_accepted_invitations, -> { invite.where(invite_accepted_at: nil) }
scope :not_accepted_invitations_by_user, -> (user) { not_accepted_invitations.where(created_by: user) }
scope :not_expired, -> (today = Date.current) { where(arel_table[:expires_at].gt(today).or(arel_table[:expires_at].eq(nil))) }
2021-02-11 23:33:58 +05:30
scope :created_today, -> do
now = Date.current
where(created_at: now.beginning_of_day..now.end_of_day)
end
2021-01-03 14:25:43 +05:30
scope :last_ten_days_excluding_today, -> (today = Date.current) { where(created_at: (today - 10).beginning_of_day..(today - 1).end_of_day) }
2020-09-03 11:15:55 +05:30
2016-09-29 09:46:39 +05:30
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) }
2018-11-18 11:00:15 +05:30
scope :maintainers, -> { active.where(access_level: MAINTAINER) }
2020-03-13 15:44:24 +05:30
scope :non_guests, -> { where('members.access_level > ?', GUEST) }
2020-11-24 15:15:51 +05:30
scope :non_minimal_access, -> { where('members.access_level > ?', MINIMAL_ACCESS) }
2020-04-22 19:07:51 +05:30
scope :owners, -> { active.where(access_level: OWNER) }
2019-03-02 22:35:43 +05:30
scope :owners_and_maintainers, -> { active.where(access_level: [OWNER, MAINTAINER]) }
2019-01-03 12:48:30 +05:30
scope :with_user, -> (user) { where(user: user) }
2021-01-29 00:20:46 +05:30
scope :with_user_by_email, -> (email) { left_join_users.where(users: { email: email } ) }
2020-10-24 23:57:45 +05:30
scope :preload_user_and_notification_settings, -> { preload(user: :notification_settings) }
2015-04-26 12:48:37 +05:30
2019-09-04 21:01:54 +05:30
scope :with_source_id, ->(source_id) { where(source_id: source_id) }
2020-03-13 15:44:24 +05:30
scope :including_source, -> { includes(:source) }
2019-09-04 21:01:54 +05:30
2021-04-29 21:17:54 +05:30
scope :distinct_on_user_with_max_access_level, -> do
distinct_members = select('DISTINCT ON (user_id, invite_email) *')
.order('user_id, invite_email, access_level DESC, expires_at DESC, created_at ASC')
2021-06-08 01:23:25 +05:30
from(distinct_members, :members)
2021-04-29 21:17:54 +05:30
end
2017-08-17 22:00:37 +05:30
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')) }
2019-02-15 15:39:39 +05:30
scope :on_project_and_ancestors, ->(project) { where(source: [project] + project.ancestors) }
2019-02-02 18:00:53 +05:30
2015-04-26 12:48:37 +05:30
before_validation :generate_invite_token, on: :create, if: -> (member) { member.invite_email.present? }
2016-06-22 15:30:34 +05:30
after_create :send_invite, if: :invite?, unless: :importing?
after_create :send_request, if: :request?, unless: :importing?
after_create :create_notification_setting, unless: [:pending?, :importing?]
2021-09-04 01:27:46 +05:30
after_create :post_create_hook, unless: [:pending?, :importing?], if: :hook_prerequisites_met?
after_update :post_update_hook, unless: [:pending?, :importing?], if: :hook_prerequisites_met?
2018-03-27 19:54:05 +05:30
after_destroy :destroy_notification_setting
2021-09-04 01:27:46 +05:30
after_destroy :post_destroy_hook, unless: :pending?, if: :hook_prerequisites_met?
2017-08-17 22:00:37 +05:30
after_commit :refresh_member_authorized_projects
2015-04-26 12:48:37 +05:30
2016-06-02 11:05:42 +05:30
default_value_for :notification_level, NotificationSetting.levels[:global]
2015-04-26 12:48:37 +05:30
class << self
2017-08-17 22:00:37 +05:30
def search(query)
joins(:user).merge(User.search(query))
end
2019-10-12 21:52:04 +05:30
def search_invite_email(query)
invite.where(['invite_email ILIKE ?', "%#{query}%"])
end
2018-11-08 19:23:39 +05:30
def filter_by_2fa(value)
case value
when 'enabled'
2018-11-20 20:47:30 +05:30
left_join_users.merge(User.with_two_factor)
2018-11-08 19:23:39 +05:30
when 'disabled'
left_join_users.merge(User.without_two_factor)
else
all
end
end
2018-05-09 12:01:36 +05:30
def sort_by_attribute(method)
2017-08-17 22:00:37 +05:30
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
2017-09-10 17:25:29 +05:30
member_users = members.join(users, Arel::Nodes::OuterJoin)
.on(members[:user_id].eq(users[:id]))
.join_sources
2017-08-17 22:00:37 +05:30
joins(member_users)
end
2016-09-13 17:45:13 +05:30
def access_for_user_ids(user_ids)
where(user_id: user_ids).has_access.pluck(:user_id, :access_level).to_h
end
2020-11-24 15:15:51 +05:30
def find_by_invite_token(raw_invite_token)
invite_token = Devise.token_generator.digest(self, :invite_token, raw_invite_token)
2015-04-26 12:48:37 +05:30
find_by(invite_token: invite_token)
end
2018-03-27 19:54:05 +05:30
def add_user(source, user, access_level, existing_members: nil, current_user: nil, expires_at: nil, ldap: false)
2018-12-05 23:21:45 +05:30
# rubocop: disable CodeReuse/ServiceClass
2018-03-17 18:26:18 +05:30
# `user` can be either a User object, User ID or an email to be invited
member = retrieve_member(source, user, existing_members)
2016-11-03 12:29:30 +05:30
access_level = retrieve_access_level(access_level)
2015-11-26 14:37:03 +05:30
2016-11-03 12:29:30 +05:30
return member unless can_update_member?(current_user, member)
2019-02-15 15:39:39 +05:30
set_member_attributes(
member,
access_level,
current_user: current_user,
expires_at: expires_at,
ldap: ldap
)
2016-11-03 12:29:30 +05:30
if member.request?
::Members::ApproveAccessRequestService.new(
current_user,
access_level: access_level
2018-03-27 19:54:05 +05:30
).execute(
member,
skip_authorization: ldap,
skip_log_audit_event: ldap
)
2015-04-26 12:48:37 +05:30
else
2016-11-03 12:29:30 +05:30
member.save
2015-04-26 12:48:37 +05:30
end
2016-11-03 12:29:30 +05:30
member
2018-12-05 23:21:45 +05:30
# rubocop: enable CodeReuse/ServiceClass
2016-11-03 12:29:30 +05:30
end
2015-11-26 14:37:03 +05:30
2019-02-15 15:39:39 +05:30
# Populates the attributes of a member.
#
# This logic resides in a separate method so that EE can extend this logic,
# without having to patch the `add_user` method directly.
def set_member_attributes(member, access_level, current_user: nil, expires_at: nil, ldap: false)
member.attributes = {
created_by: member.created_by || current_user,
access_level: access_level,
expires_at: expires_at
}
end
2017-08-17 22:00:37 +05:30
def add_users(source, users, access_level, current_user: nil, expires_at: nil)
return [] unless users.present?
2018-03-17 18:26:18 +05:30
emails, users, existing_members = parse_users_list(source, users)
2017-08-17 22:00:37 +05:30
self.transaction do
2018-03-17 18:26:18 +05:30
(emails + users).map! do |user|
2017-08-17 22:00:37 +05:30
add_user(
source,
user,
access_level,
2018-03-17 18:26:18 +05:30
existing_members: existing_members,
2017-08-17 22:00:37 +05:30
current_user: current_user,
expires_at: expires_at
)
end
end
end
2016-11-03 12:29:30 +05:30
def access_levels
Gitlab::Access.sym_options
2015-11-26 14:37:03 +05:30
end
2021-04-29 21:17:54 +05:30
def valid_email?(email)
Devise.email_regexp.match?(email)
end
2015-11-26 14:37:03 +05:30
private
2015-04-26 12:48:37 +05:30
2018-03-17 18:26:18 +05:30
def parse_users_list(source, list)
2021-04-29 21:17:54 +05:30
emails = []
user_ids = []
users = []
2018-03-17 18:26:18 +05:30
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))
2021-04-29 21:17:54 +05:30
# the below will automatically discard invalid user_ids
2018-03-17 18:26:18 +05:30
existing_members = source.members_and_requesters.where(user_id: user_ids).index_by(&:user_id)
end
[emails, users, existing_members]
end
2016-11-03 12:29:30 +05:30
# 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)
2020-04-22 19:07:51 +05:30
return User.find_by(id: user) if user.is_a?(Integer)
2021-09-04 01:27:46 +05:30
User.find_by_any_email(user) || user
2016-11-03 12:29:30 +05:30
end
2018-03-17 18:26:18 +05:30
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
2016-11-03 12:29:30 +05:30
def retrieve_access_level(access_level)
access_levels.fetch(access_level) { access_level.to_i }
end
2015-11-26 14:37:03 +05:30
def can_update_member?(current_user, member)
# There is no current user for bulk actions, in which case anything is allowed
2016-11-03 12:29:30 +05:30
!current_user || current_user.can?(:"update_#{member.type.underscore}", member)
2015-04-26 12:48:37 +05:30
end
end
def real_source_type
source_type
end
2017-09-10 17:25:29 +05:30
def access_field
access_level
end
2015-04-26 12:48:37 +05:30
def invite?
self.invite_token.present?
end
def request?
requested_at.present?
end
def pending?
invite? || request?
end
2021-09-04 01:27:46 +05:30
def hook_prerequisites_met?
# It is essential that an associated user record exists
# so that we can successfully fire any member related hooks/notifications.
user.present?
end
def accept_request
return false unless request?
updated = self.update(requested_at: nil)
after_accept_request if updated
updated
end
2015-04-26 12:48:37 +05:30
def accept_invite!(new_user)
return false unless invite?
2015-11-26 14:37:03 +05:30
2015-04-26 12:48:37 +05:30
self.invite_token = nil
2020-06-23 00:09:42 +05:30
self.invite_accepted_at = Time.current.utc
2015-04-26 12:48:37 +05:30
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
2021-01-03 14:25:43 +05:30
def send_invitation_reminder(reminder_index)
return unless invite?
generate_invite_token! unless @raw_invite_token
run_after_commit_or_now { notification_service.invite_member_reminder(self, @raw_invite_token, reminder_index) }
end
2016-06-02 11:05:42 +05:30
def create_notification_setting
user.notification_settings.find_or_create_for(source)
end
2018-03-27 19:54:05 +05:30
def destroy_notification_setting
notification_setting&.destroy
end
2016-06-02 11:05:42 +05:30
def notification_setting
2018-03-27 19:54:05 +05:30
@notification_setting ||= user&.notification_settings_for(source)
2016-06-02 11:05:42 +05:30
end
2018-12-05 23:21:45 +05:30
# rubocop: disable CodeReuse/ServiceClass
2018-03-17 18:26:18 +05:30
def notifiable?(type, opts = {})
# always notify when there isn't a user yet
return true if user.blank?
2020-04-08 14:13:33 +05:30
NotificationRecipients::BuildService.notifiable?(user, type, notifiable_options.merge(opts))
2018-03-17 18:26:18 +05:30
end
2018-12-05 23:21:45 +05:30
# rubocop: enable CodeReuse/ServiceClass
2018-03-17 18:26:18 +05:30
2019-02-15 15:39:39 +05:30
# Find the user's group member with a highest access level
def highest_group_member
strong_memoize(:highest_group_member) do
next unless user_id && source&.ancestors&.any?
GroupMember.where(source: source.ancestors, user_id: user_id).order(:access_level).last
end
end
2020-11-24 15:15:51 +05:30
def invite_to_unknown_user?
invite? && user_id.nil?
end
2021-01-29 00:20:46 +05:30
def created_by_name
created_by&.name
end
2015-04-26 12:48:37 +05:30
private
def send_invite
# override in subclass
end
def send_request
2016-08-24 12:49:21 +05:30
notification_service.new_access_request(self)
end
2015-04-26 12:48:37 +05:30
def post_create_hook
system_hook_service.execute_hooks_for(self, :create)
end
def post_update_hook
2019-12-04 20:38:33 +05:30
system_hook_service.execute_hooks_for(self, :update)
2015-04-26 12:48:37 +05:30
end
def post_destroy_hook
system_hook_service.execute_hooks_for(self, :destroy)
end
2017-08-17 22:00:37 +05:30
# 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.
2018-12-05 23:21:45 +05:30
# rubocop: disable CodeReuse/ServiceClass
2017-08-17 22:00:37 +05:30
def refresh_member_authorized_projects
# 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
return if destroyed_by_association.present?
UserProjectAccessChangedService.new(user_id).execute
end
2018-12-05 23:21:45 +05:30
# rubocop: enable CodeReuse/ServiceClass
2017-08-17 22:00:37 +05:30
2015-04-26 12:48:37 +05:30
def after_accept_invite
post_create_hook
end
def after_decline_invite
# override in subclass
end
def after_accept_request
post_create_hook
end
2018-12-05 23:21:45 +05:30
# rubocop: disable CodeReuse/ServiceClass
2015-04-26 12:48:37 +05:30
def system_hook_service
SystemHooksService.new
end
2018-12-05 23:21:45 +05:30
# rubocop: enable CodeReuse/ServiceClass
2015-04-26 12:48:37 +05:30
2018-12-05 23:21:45 +05:30
# rubocop: disable CodeReuse/ServiceClass
2015-04-26 12:48:37 +05:30
def notification_service
NotificationService.new
end
2018-12-05 23:21:45 +05:30
# rubocop: enable CodeReuse/ServiceClass
2018-03-17 18:26:18 +05:30
def notifiable_options
{}
end
2019-02-15 15:39:39 +05:30
def higher_access_level_than_group
2019-07-07 11:18:12 +05:30
if highest_group_member && highest_group_member.access_level > access_level
2019-02-15 15:39:39 +05:30
error_parameters = { access: highest_group_member.human_access, group_name: highest_group_member.group.name }
2019-07-07 11:18:12 +05:30
errors.add(:access_level, s_("should be greater than or equal to %{access} inherited membership from group %{group_name}") % error_parameters)
2019-02-15 15:39:39 +05:30
end
end
2020-04-22 19:07:51 +05:30
def update_highest_role?
return unless user_id.present?
previous_changes[:access_level].present?
end
def update_highest_role_attribute
user_id
end
2020-07-28 23:09:34 +05:30
def project_bot?
user&.project_bot?
end
2015-04-26 12:48:37 +05:30
end
2019-12-04 20:38:33 +05:30
2021-06-08 01:23:25 +05:30
Member.prepend_mod_with('Member')