Skip to content
Snippets Groups Projects
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 0c0ef7df
No related branches found
No related tags found
No related merge requests found
Showing
with 248 additions and 14 deletions
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
Loading
Loading
@@ -93,6 +93,33 @@ class Projects::ProjectMembersController < Projects::ApplicationController
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])
 
Loading
Loading
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')
Loading
Loading
Loading
Loading
@@ -11,6 +11,48 @@ module Emails
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
Loading
Loading
Loading
Loading
@@ -153,7 +153,7 @@ class Ability
 
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)) ||
Loading
Loading
Loading
Loading
@@ -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) }
Loading
Loading
@@ -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
 
Loading
Loading
@@ -96,10 +106,38 @@ class Member < ActiveRecord::Base
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?
 
Loading
Loading
@@ -153,6 +191,10 @@ class Member < ActiveRecord::Base
 
private
 
def send_request_access
# override in subclass
end
def send_invite
# override in subclass
end
Loading
Loading
@@ -169,6 +211,14 @@ class Member < ActiveRecord::Base
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
Loading
Loading
Loading
Loading
@@ -107,6 +107,12 @@ class ProjectMember < Member
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)
 
Loading
Loading
@@ -136,6 +142,18 @@ class ProjectMember < Member
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)
 
Loading
Loading
Loading
Loading
@@ -115,6 +115,12 @@ class ProjectTeam
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
Loading
Loading
Loading
Loading
@@ -173,6 +173,18 @@ class NotificationService
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
Loading
Loading
Loading
Loading
@@ -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
Loading
Loading
@@ -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
Loading
Loading
%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());
});
Loading
Loading
@@ -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
Loading
Loading
@@ -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
Loading
Loading
@@ -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
Loading
Loading
Loading
Loading
@@ -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
Loading
Loading
Loading
Loading
@@ -768,6 +768,7 @@ Rails.application.routes.draw do
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
Loading
Loading
@@ -777,6 +778,7 @@ Rails.application.routes.draw do
 
member do
post :resend_invite
post :approval
end
end
 
Loading
Loading
class AddMembershipRequest < ActiveRecord::Migration
def change
add_column :members, :requested, :boolean
end
end
Loading
Loading
@@ -536,6 +536,7 @@ ActiveRecord::Schema.define(version: 20160610301627) do
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
Loading
Loading
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment