Read about our upcoming Code of Conduct on this issue

This instance will be upgraded to Heptapod 0.28.0 on 2022-01-18 at 14:00 UTC+1 (a few minutes of down time)

Commit 17c22156 authored by David Alexander's avatar David Alexander Committed by Rémy Coutable
Browse files

Initial implementation of user access request to projects

parent 94943c091c89
class Projects::ProjectMembersController < Projects::ApplicationController
# Authorize
before_action :authorize_admin_project_member!, except: [:leave, :index]
before_action :authorize_admin_project_member!, except: [:index, :leave, :request_access]
def index
@project_members = @project.project_members
@project_members = @project_members.non_invite unless can?(current_user, :admin_project, @project)
@project_members = @project_members.non_pending unless can?(current_user, :admin_project, @project)
if params[:search].present?
users = @project.users.search(params[:search]).to_a
......@@ -93,6 +93,33 @@ def leave
end
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!
redirect_to redirect_path, notice: 'Your request for access has been queued for review.'
end
def approval
@project_member = @project.project_members.find(params[:id])
return render_403 unless can?(current_user, :update_project_member, @project_member)
@project_member.requested = nil
@project_member.save!
respond_to do |format|
format.html do
redirect_to namespace_project_project_members_path(@project.namespace, @project)
end
format.js { render nothing: true }
end
end
def apply_import
source_project = Project.find(params[:source_project_id])
......
module ProjectsHelper
def remove_from_project_team_message(project, member)
if member.user
"You are going to remove #{member.user.name} from #{project.name} project team. Are you sure?"
else
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?"
end
def link_to_project(project)
link_to [project.namespace.becomes(Namespace), project], title: h(project.name) do
title = content_tag(:span, project.name, class: 'project-name')
......
......@@ -11,6 +11,48 @@ def project_access_granted_email(project_member_id)
subject: subject("Access to project was granted"))
end
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 = 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
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
@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 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
@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'))
end
def project_member_invited_email(project_member_id, token)
@project_member = ProjectMember.find project_member_id
@project = @project_member.project
......
......@@ -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))
rules.push(*project_team_rules(project.team, user)) unless project.team.pending?(user)
if project.owner == user ||
(project.group && project.group.has_owner?(user)) ||
......
......@@ -27,7 +27,12 @@ class Member < ActiveRecord::Base
}
scope :invite, -> { where(user_id: nil) }
scope :non_invite, -> { where("user_id IS NOT NULL") }
scope :non_invite, -> { where('user_id IS NOT NULL') }
scope :request, -> { where(requested: true) }
scope :non_request, -> { where(requested: nil) }
scope :pending, -> { where("user_id IS NULL OR requested") }
scope :non_pending, -> { self.non_invite.non_request }
scope :guests, -> { where(access_level: GUEST) }
scope :reporters, -> { where(access_level: REPORTER) }
scope :developers, -> { where(access_level: DEVELOPER) }
......@@ -35,11 +40,16 @@ class Member < ActiveRecord::Base
scope :owners, -> { where(access_level: OWNER) }
before_validation :generate_invite_token, on: :create, if: -> (member) { member.invite_email.present? }
after_create :send_invite, if: :invite?
after_create :create_notification_setting, unless: :invite?
after_create :post_create_hook, unless: :invite?
after_update :post_update_hook, unless: :invite?
after_destroy :post_destroy_hook, unless: :invite?
after_create :send_request_access, if: :request?
after_create :create_notification_setting, unless: :pending?
after_create :post_create_hook, unless: :pending?
after_update :post_update_hook, unless: :pending?
after_destroy :post_destroy_hook, unless: :pending?
delegate :name, :username, :email, to: :user, prefix: true
......@@ -96,10 +106,38 @@ def project_creator?(member, access_level)
end
end
def pending?
request? || invite?
end
def request?
self.requested
end
def invite?
self.invite_token.present?
end
def accept_request_access!
return false unless request?
self.request = false
saved = self.save
after_accept_request_access if saved
saved
end
def decline_request_access!
return false unless request?
destroyed = self.destroy
after_decline_request_access if destroyed
destroyed
end
def accept_invite!(new_user)
return false unless invite?
......@@ -153,6 +191,10 @@ def notification_setting
private
def send_request_access
# override in subclass
end
def send_invite
# override in subclass
end
......@@ -169,6 +211,14 @@ def post_destroy_hook
system_hook_service.execute_hooks_for(self, :destroy)
end
def after_accept_request_access
post_create_hook
end
def after_decline_request_access
# override in subclass
end
def after_accept_invite
post_create_hook
end
......
......@@ -107,6 +107,12 @@ def delete_member_todos
user.todos.where(project_id: source_id).destroy_all if user
end
def send_request_access
notification_service.request_access_project_member(self)
super
end
def send_invite
notification_service.invite_project_member(self, @raw_invite_token)
......@@ -136,6 +142,18 @@ def post_destroy_hook
super
end
def after_accept_request_access
notification_service.accept_project_request_access(self)
super
end
def after_decline_request_access
notification_service.decline_project_request_access(self)
super
end
def after_accept_invite
notification_service.accept_project_invite(self)
......
......@@ -115,6 +115,12 @@ def import(source_project, current_user = nil)
false
end
def pending?(user)
project.project_members.each do |member|
return member.pending? if member.user_id == user.id
end
end
def guest?(user)
max_member_access(user.id) == Gitlab::Access::GUEST
end
......
......@@ -173,6 +173,18 @@ def new_note(note)
end
end
def request_access_project_member(project_member)
mailer.project_member_requested_access(project_member.id).deliver_later
end
def accept_project_request_access(project_member)
mailer.project_request_access_accepted_email(project_member.id).deliver_later
end
def decline_project_request_access(project_member)
mailer.project_request_access_declined_email(project_member.id).deliver_later
end
def invite_project_member(project_member, token)
mailer.project_member_invited_email(project_member.id, token).deliver_later
end
......
......@@ -8,6 +8,19 @@
= icon('caret-down')
%ul.dropdown-menu.dropdown-menu-align-right
= render 'layouts/nav/project_settings'
- if access
%li
= link_to leave_namespace_project_project_members_path(@project.namespace, @project),
data: { confirm: leave_project_message(@project) }, method: :delete, title: 'Leave project' do
Leave Project
- else
= link_to request_access_namespace_project_project_members_path(@project.namespace, @project),
class: 'btn btn-gray', style: 'margin-left: 10px', method: :post, title: 'Request access' do
Request Access
%li.divider
- if can_edit
%li
......@@ -18,6 +31,11 @@
= link_to leave_namespace_project_project_members_path(@project.namespace, @project),
data: { confirm: leave_project_message(@project) }, method: :delete, title: 'Leave project' do
Leave Project
- else
%li
= link_to request_access_namespace_project_project_members_path(@project.namespace, @project),
class: 'btn btn-gray', style: 'margin-left: 10px', method: :post, title: 'Request access' do
Request Access
%div{ class: nav_control_class }
%ul.nav-links.scrolling-tabs
......
%p
Your request to join project
#{link_to @project.name_with_namespace, namespace_project_url(@project.namespace, @project)}
has been granted with #{@project_member.human_access} access.
Your request to join project <%= @project.name_with_namespace %> has been granted with <%= @project_member.human_access %> access.
<%= namespace_project_url(@project.namespace, @project) %>
%p
Your request to join project
#{link_to @project.name_with_namespace, namespace_project_url(@project.namespace, @project)}
has been denied.
Your request to join project <%= @project.name_with_namespace %> has been denied.
<%= namespace_project_url(@project.namespace, @project) %>
.panel.panel-default
.panel-heading
%strong #{@project.name}
candidates
%small
(#{members.count})
.controls
= form_tag namespace_project_project_members_path(@project.namespace, @project), method: :get, class: 'form-inline member-search-form' do
.form-group
= search_field_tag :search, params[:search], { placeholder: 'Find existing member by name', class: 'form-control', spellcheck: false }
= button_tag class: 'btn', title: 'Search' do
= icon("search")
%ul.content-list
- members.each do |project_member|
= render 'project_member', member: project_member
:javascript
$('form.member-search-form').on('submit', function (event) {
event.preventDefault();
Turbolinks.visit(this.action + '?' + $(this).serialize());
});
......@@ -13,6 +13,9 @@
- if user.blocked?
%label.label.label-danger
%strong Blocked
- if member.request?
%span.label.label-info
Pending Approval
- else
= image_tag avatar_icon(member.invite_email, 24), class: "avatar s24", alt: ''
%strong
......@@ -27,7 +30,6 @@
- if can?(current_user, :admin_project_member, @project)
= link_to resend_invite_namespace_project_project_member_path(@project.namespace, @project, member), method: :post, class: "btn-xs btn", title: 'Resend invite' do
Resend invite
- if can?(current_user, :admin_project_member, @project)
.pull-right
%strong= member.human_access
......@@ -35,10 +37,19 @@
= button_tag class: "btn-xs btn-grouped inline btn js-toggle-button",
title: 'Edit access level', type: 'button' do
= icon('pencil')
- if member.request?
&nbsp;
= link_to approval_namespace_project_project_member_path(@project.namespace, @project, member),
class: "btn-xs btn btn-success",
title: 'Grant access', type: 'button' do
%i.fa.fa-check.fa-inverse
- if can?(current_user, :destroy_project_member, member)
&nbsp;
- if current_user == user
- if member.request?
= link_to namespace_project_project_member_path(@project.namespace, @project, member), data: { confirm: remove_from_project_team_message(@project, member) }, method: :delete, class: "btn-xs btn btn-remove", title: 'Deny access' do
%i.fa.fa-times.fa-inverse
- elsif current_user == user
= link_to leave_namespace_project_project_members_path(@project.namespace, @project), data: { confirm: leave_project_message(@project) }, method: :delete, class: "btn-xs btn btn-remove", title: 'Leave project' do
= icon("sign-out")
Leave
......
......@@ -12,8 +12,9 @@
%p.light
Users with access to this project are listed below.
= render "new_project_member"
= render "pending", members: @project_members.request
= render "team", members: @project_members
= render "team", members: @project_members.non_request
- if @group
= render "group_members", members: @group_members
......
......@@ -768,6 +768,7 @@
resources :project_members, except: [:new, :edit], constraints: { id: /[a-zA-Z.\/0-9_\-#%+]+/ } do
collection do
delete :leave
post :request_access
# Used for import team
# from another project
......@@ -777,6 +778,7 @@
member do
post :resend_invite
post :approval
end
end
......
class AddMembershipRequest < ActiveRecord::Migration
def change
add_column :members, :requested, :boolean
end
end
......@@ -536,6 +536,7 @@
t.string "invite_email"
t.string "invite_token"
t.datetime "invite_accepted_at"
t.boolean "requested"
end
add_index "members", ["access_level"], name: "index_members_on_access_level", using: :btree
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment