This instance will be upgraded to Heptapod 0.26.0rc1 on 2021-10-26 at 14:30 UTC+2 (a few minutes of down time)

Commit d26f8123 authored by Rémy Coutable's avatar Rémy Coutable
Browse files

Add request access for groups


Signed-off-by: default avatarRémy Coutable <remy@rymai.me>
parent 58f08aa9e328
......@@ -286,10 +286,6 @@
color: #555;
}
.project_member_row form {
margin: 0;
}
.transfer-project .select2-container {
min-width: 200px;
}
......
class Groups::GroupMembersController < Groups::ApplicationController
# Authorize
before_action :authorize_admin_group_member!, except: [:index, :leave]
before_action :authorize_admin_group_member!, except: [:index, :leave, :request_access]
def index
@project = @group.projects.find(params[:project_id]) if params[:project_id]
@members = @group.group_members
@members = @members.non_invite unless can?(current_user, :admin_group, @group)
@members = @members.non_pending unless can?(current_user, :admin_group, @group)
if params[:search].present?
users = @group.users.search(params[:search]).to_a
......@@ -36,7 +36,7 @@ def destroy
return render_403 unless can?(current_user, :destroy_group_member, @group_member)
@group_member.destroy
@group_member.request? ? @group_member.decline_request : @group_member.destroy
respond_to do |format|
format.html { redirect_to group_group_members_path(@group), notice: 'User was successfully removed from group.' }
......@@ -59,12 +59,20 @@ def resend_invite
end
def leave
@group_member = @group.group_members.find_by(user_id: current_user)
@group_member =
@group.group_members.find_by(user_id: current_user.id) ||
@group.group_members.find_by(created_by_id: current_user.id)
if can?(current_user, :destroy_group_member, @group_member)
notice =
if @group_member.request?
'You withdrawn your access request to the group.'
else
"You left #{@group.name} group."
end
@group_member.destroy
redirect_to(dashboard_groups_path, notice: "You left #{group.name} group.")
redirect_to dashboard_groups_path, notice: notice
else
if @group.last_owner?(current_user)
redirect_to(dashboard_groups_path, alert: "You can not leave #{group.name} group because you're the last owner. Transfer or delete the group.")
......@@ -74,6 +82,22 @@ def leave
end
end
def request_access
@group.request_access(current_user)
redirect_to group_path(@group), notice: 'Your request for access has been queued for review.'
end
def approve
@group_member = @group.group_members.request.find(params[:id])
return render_403 unless can?(current_user, :update_group_member, @group_member)
@group_member.accept_request
redirect_to group_group_members_path(@group)
end
protected
def member_params
......
......@@ -14,9 +14,10 @@ def index
@project_members = @project_members.order('access_level DESC')
@group = @project.group
if @group
@group_members = @group.group_members
@group_members = @group_members.non_invite unless can?(current_user, :admin_group, @group)
@group_members = @group_members.non_pending unless can?(current_user, :admin_group, @group)
if params[:search].present?
users = @group.users.search(params[:search]).to_a
......@@ -49,7 +50,7 @@ def destroy
return render_403 unless can?(current_user, :destroy_project_member, @project_member)
@project_member.destroy
@project_member.request? ? @project_member.decline_request : @project_member.destroy
respond_to do |format|
format.html do
......@@ -74,15 +75,20 @@ def resend_invite
end
def leave
@project_member = @project.project_members.find_by(user_id: current_user)
@project_member =
@project.project_members.find_by(user_id: current_user.id) ||
@project.project_members.find_by(created_by_id: current_user.id)
if can?(current_user, :destroy_project_member, @project_member)
notice =
if @project_member.request?
'You withdrawn your access request to the project.'
else
'You left the project.'
end
@project_member.destroy
respond_to do |format|
format.html { redirect_to dashboard_projects_path, notice: "You left the project." }
format.js { head :ok }
end
redirect_to dashboard_projects_path, notice: notice
else
if current_user == @project.owner
message = 'You can not leave your own project. Transfer or delete the project.'
......@@ -94,30 +100,20 @@ def leave
end
def request_access
redirect_path = namespace_project_path(@project.namespace, @project)
# current_user
# @project
@project_member = ProjectMember.new(source: @project, access_level: ProjectMember::DEVELOPER, user_id: current_user.id, created_by_id: current_user.id, requested: true)
@project_member.save!
@project.request_access(current_user)
redirect_to redirect_path, notice: 'Your request for access has been queued for review.'
redirect_to namespace_project_path(@project.namespace, @project),
notice: 'Your request for access has been queued for review.'
end
def approval
@project_member = @project.project_members.find(params[:id])
def approve
@project_member = @project.project_members.request.find(params[:id])
return render_403 unless can?(current_user, :update_project_member, @project_member)
@project_member.requested = nil
@project_member.save!
@project_member.accept_request
respond_to do |format|
format.html do
redirect_to namespace_project_project_members_path(@project.namespace, @project)
end
format.js { render nothing: true }
end
redirect_to namespace_project_project_members_path(@project.namespace, @project)
end
def apply_import
......
module GroupsHelper
def remove_user_from_group_message(group, member)
if member.user
"Are you sure you want to remove \"#{member.user.name}\" from \"#{group.name}\"?"
else
"Are you sure you want to revoke the invitation for \"#{member.invite_email}\" to join \"#{group.name}\"?"
end
end
def leave_group_message(group)
"Are you sure you want to leave \"#{group}\" group?"
end
def should_user_see_group_roles?(user, group)
if user
user.is_admin? || group.members.exists?(user_id: user.id)
else
false
end
end
def can_change_group_visibility_level?(group)
can?(current_user, :change_visibility_level, group)
end
......
module MembersHelper
def member_class(member)
"#{member.source.class.to_s}Member".constantize
end
def members_association(entity)
"#{entity.class.to_s.underscore}_members".to_sym
end
def action_member_permission(action, member)
"#{action}_#{member.source.class.to_s.underscore}_member".to_sym
end
def can_see_entity_roles?(user, entity)
return false unless user
user.is_admin? || entity.send(members_association(entity)).exists?(user_id: user.id)
end
def member_path(member)
case member.source
when Project
namespace_project_project_member_path(member.source.namespace, member.source, member)
when Group
group_group_member_path(member.source, member)
else
raise ArgumentError.new('Unknown object class')
end
end
def resend_invite_member_path(member)
case member.source
when Project
resend_invite_namespace_project_project_member_path(member.source.namespace, member.source, member)
when Group
resend_invite_group_group_member_path(member.source, member)
else
raise ArgumentError.new('Unknown object class')
end
end
def request_access_path(entity)
case entity
when Project
request_access_namespace_project_project_members_path(entity.namespace, entity)
when Group
request_access_group_group_members_path(entity)
else
raise ArgumentError.new('Unknown object class')
end
end
def approve_request_member_path(member)
case member.source
when Project
approve_namespace_project_project_member_path(member.source.namespace, member.source, member)
when Group
approve_group_group_member_path(member.source, member)
else
raise ArgumentError.new('Unknown object class')
end
end
def leave_path(entity)
case entity
when Project
leave_namespace_project_project_members_path(entity.namespace, entity)
when Group
leave_group_group_members_path(entity)
else
raise ArgumentError.new('Unknown object class')
end
end
def withdraw_request_message(entity)
"Are you sure you want to withdraw your access request for the \"#{entity_name(entity)}\" #{entity_type(entity)}?"
end
def remove_member_message(member)
entity = member.source
entity_type = entity_type(entity)
entity_name = entity_name(entity)
if member.request?
"You are going to deny #{member.created_by.name}'s request to join the #{entity_name} #{entity_type}. Are you sure?"
elsif member.invite?
"You are going to revoke the invitation for #{member.invite_email} to join the #{entity_name} #{entity_type}. Are you sure?"
else
"You are going to remove #{member.user.name} from the #{entity_name} #{entity_type}. Are you sure?"
end
end
def remove_member_title(member)
member.request? ? 'Deny access request' : 'Remove user'
end
def leave_confirmation_message(entity)
"Are you sure you want to leave \"#{entity_name(entity)}\" #{entity_type(entity)}?"
end
private
def entity_type(entity)
entity.class.to_s.underscore
end
def entity_name(entity)
case entity
when Project
entity.name_with_namespace
when Group
entity.name
else
raise ArgumentError.new('Unknown object class')
end
end
end
module ProjectsHelper
def remove_from_project_team_message(project, member)
if !member.user
"You are going to revoke the invitation for #{member.invite_email} to join #{project.name} project team. Are you sure?"
elsif member.request?
"You are going to deny #{member.user.name}'s request to join #{project.name} project team. Are you sure?"
else
"You are going to remove #{member.user.name} from #{project.name} project team. Are you sure?"
end
end
def approve_for_project_team_message(project, member)
"You are going to approve #{member.user.name}'s request for #{member.human_access} access to the #{project.name} project team. Are you sure?"
def max_access_level(project, user)
Gitlab::Access.options_with_owner.key(project.team.max_member_access(user.id))
end
def link_to_project(project)
......@@ -121,14 +111,6 @@ def can_change_visibility_level?(project, current_user)
end
end
def user_max_access_in_project(user_id, project)
level = project.team.max_member_access(user_id)
if level
Gitlab::Access.options_with_owner.key(level)
end
end
def license_short_name(project)
return 'LICENSE' if project.repository.license_key.nil?
......@@ -292,10 +274,6 @@ def project_status_css_class(status)
end
end
def leave_project_message(project)
"Are you sure you want to leave \"#{project.name}\" project?"
end
def new_readme_path
ref = @repository.root_ref if @repository
ref ||= 'master'
......
module Emails
module Groups
def group_access_requested_email(group_member_id)
setup_group_member_mail(group_member_id)
@requester = @group_member.created_by
group_admins = User.where(id: @group.group_members.admins.pluck(:user_id)).pluck(:notification_email)
mail(to: group_admins,
subject: subject("Request to join #{@group.name} group"))
end
def group_access_granted_email(group_member_id)
@group_member = GroupMember.find(group_member_id)
@group = @group_member.group
setup_group_member_mail(group_member_id)
@target_url = group_url(@group)
@current_user = @group_member.user
mail(to: @group_member.user.notification_email,
subject: subject("Access to group was granted"))
mail(to: @current_user.notification_email,
subject: subject("Access to #{@group.name} group was granted"))
end
def group_access_denied_email(group_id, user_id)
@group = Group.find(group_id)
@current_user = User.find(user_id)
@target_url = group_url(@group)
mail(to: @current_user.notification_email,
subject: subject("Access to #{@group.name} group was denied"))
end
def group_member_invited_email(group_member_id, token)
@group_member = GroupMember.find group_member_id
@group = @group_member.group
@token = token
setup_group_member_mail(group_member_id)
@target_url = group_url(@group)
@token = token
@current_user = @group_member.user
mail(to: @group_member.invite_email,
......@@ -24,15 +40,12 @@ def group_member_invited_email(group_member_id, token)
end
def group_invite_accepted_email(group_member_id)
@group_member = GroupMember.find group_member_id
setup_group_member_mail(group_member_id)
return if @group_member.created_by.nil?
@group = @group_member.group
@target_url = group_url(@group)
@current_user = @group_member.created_by
mail(to: @group_member.created_by.notification_email,
mail(to: @current_user.notification_email,
subject: subject("Invitation accepted"))
end
......@@ -43,10 +56,18 @@ def group_invite_declined_email(group_id, invite_email, access_level, created_by
@current_user = @created_by = User.find(created_by_id)
@access_level = access_level
@invite_email = invite_email
@target_url = group_url(@group)
mail(to: @created_by.notification_email,
subject: subject("Invitation declined"))
end
private
def setup_group_member_mail(group_member_id)
@group_member = GroupMember.find(group_member_id)
@group = @group_member.group
@target_url = group_url(@group)
end
end
end
module Emails
module Projects
def project_access_granted_email(project_member_id)
@project_member = ProjectMember.find project_member_id
@project = @project_member.project
@target_url = namespace_project_url(@project.namespace, @project)
@current_user = @project_member.user
def project_access_requested_email(project_member_id)
setup_project_member_mail(project_member_id)
mail(to: @project_member.user.notification_email,
subject: subject("Access to project was granted"))
end
@requester = @project_member.created_by
def project_member_requested_access(project_member_id)
@project_member = ProjectMember.find project_member_id
@project = @project_member.project
@target_url = namespace_project_url(@project.namespace, @project)
project_admins = User.where(id: @project.project_members.admins.pluck(:user_id)).pluck(:notification_email)
project_admins = ProjectMember.in_project(@project)
.where(access_level: [Gitlab::Access::OWNER, Gitlab::Access::MASTER])
.pluck(:notification_email)
project_admins.each do |address|
mail(to: address,
subject: subject("Request to join project: #{@project.name_with_namespace}"))
end
mail(to: project_admins,
subject: subject("Request to join #{@project.name_with_namespace} project"))
end
def project_request_access_accepted_email(project_member_id)
@project_member = ProjectMember.find project_member_id
return if @project_member.created_by.nil?
@project = @project_member.project
def project_access_granted_email(project_member_id)
setup_project_member_mail(project_member_id)
@target_url = namespace_project_url(@project.namespace, @project)
@current_user = @project_member.created_by
@current_user = @project_member.user
mail(to: @project_member.created_by.notification_email,
subject: subject('Request for access granted'))
mail(to: @current_user.notification_email,
subject: subject("Access to #{@project.name_with_namespace} project was granted"))
end
def project_request_access_declined_email(project_member_id)
@project_member = ProjectMember.find project_member_id
return if @project_member.created_by.nil?
@project = @project_member.project
def project_access_denied_email(project_id, user_id)
@project = Project.find(project_id)
@current_user = User.find(user_id)
@target_url = namespace_project_url(@project.namespace, @project)
@current_user = @project_member.created_by
mail(to: @project_member.created_by.notification_email,
subject: subject('Request for access declined'))
mail(to: @current_user.notification_email,
subject: subject("Access to #{@project.name_with_namespace} project was denied"))
end
def project_member_invited_email(project_member_id, token)
@project_member = ProjectMember.find project_member_id
@project = @project_member.project
@token = token
setup_project_member_mail(project_member_id)
@target_url = namespace_project_url(@project.namespace, @project)
@token = token
@current_user = @project_member.user
mail(to: @project_member.invite_email,
......@@ -66,12 +40,9 @@ def project_member_invited_email(project_member_id, token)
end
def project_invite_accepted_email(project_member_id)
@project_member = ProjectMember.find project_member_id
setup_project_member_mail(project_member_id)
return if @project_member.created_by.nil?
@project = @project_member.project
@target_url = namespace_project_url(@project.namespace, @project)
@current_user = @project_member.created_by
mail(to: @project_member.created_by.notification_email,
......@@ -117,5 +88,13 @@ def repository_push_email(project_id, opts = {})
reply_to: @message.reply_to,
subject: @message.subject)
end
private
def setup_project_member_mail(project_member_id)
@project_member = ProjectMember.find(project_member_id)
@project = @project_member.project
@target_url = namespace_project_url(@project.namespace, @project)
end
end
end
......@@ -153,7 +153,7 @@ def project_abilities(user, project)
RequestStore.store[key] ||= begin
# Push abilities on the users team role
rules.push(*project_team_rules(project.team, user)) unless project.team.pending?(user)
rules.push(*project_team_rules(project.team, user))
if project.owner == user ||
(project.group && project.group.has_owner?(user)) ||
......@@ -187,6 +187,8 @@ def project_team_rules(team, user)
project_report_rules
elsif team.guest?(user)
project_guest_rules
else
[]
end
end
......@@ -458,6 +460,8 @@ def group_member_abilities(user, subject)
rules << :destroy_group_member
elsif user == target_user
rules << :destroy_group_member
elsif subject.request? && user == subject.created_by
rules << :destroy_group_member
end
end
......@@ -477,6 +481,8 @@ def project_member_abilities(user, subject)
rules << :destroy_project_member
elsif user == target_user
rules << :destroy_project_member
elsif subject.request? && user == subject.created_by
rules << :destroy_project_member
end
end
......
# == AccessRequestable concern
#
# Contains functionality related to objects that can receive request for access.
#
# Used by Project, and Group.
#
module AccessRequestable
extend ActiveSupport::Concern
def request_access(user)
members.create(
access_level: Gitlab::Access::DEVELOPER,
created_by: user,
requested_at: Time.now.utc)
end
def access_requested?(user)
members.where(created_by_id: user.id).where.not(requested_at: nil).any?
end
private