Commit c11006ac authored by Douwe Maan's avatar Douwe Maan

Merge branch '18755-fix-destroy-project-causes-post_decline_request-to-be-executed' into 'master'

Resolve "Destroying a project causes post_decline_request to be executed"

## What does this MR do?

Ensure we don't send "access request declined" to access requesters when a project is deleted.

## Are there points in the code the reviewer needs to double check?

I've created a service to decouple the notification sending from the AR model.

## Why was this MR needed?

Because there was an issue.

## What are the relevant issue numbers?

Fixes #18755, #18750.

## Does this MR meet the acceptance criteria?

- [x] No CHANGELOG needed.
- [x] Tests
  - [x] Added for this feature/bug
  - [x] All builds are passing
- [x] Conform by the [style guides](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/CONTRIBUTING.md#style-guides)
- [x] Branch has no merge conflicts with `master` (if you do - rebase it please)
- [x] [Squashed related commits together](https://git-scm.com/book/en/Git-Tools-Rewriting-History#Squashing-Commits)

See merge request !4744
parents 0909535e 909a0ff3
......@@ -36,6 +36,10 @@ class ApplicationController < ActionController::Base
render_404
end
rescue_from Gitlab::Access::AccessDeniedError do |exception|
render_403
end
def redirect_back_or_default(default: root_path, options: {})
redirect_to request.referer.present? ? :back : default, options
end
......
......@@ -21,29 +21,18 @@ module MembershipActions
def leave
@member = membershipable.members.find_by(user_id: current_user)
return render_403 unless @member
Members::DestroyService.new(@member, current_user).execute
source_type = @member.real_source_type.humanize(capitalize: false)
if can?(current_user, action_member_permission(:destroy, @member), @member)
notice =
if @member.request?
"Your access request to the #{source_type} has been withdrawn."
else
"You left the \"#{@member.source.human_name}\" #{source_type}."
end
@member.destroy
redirect_path = @member.request? ? @member.source : [:dashboard, @member.real_source_type.tableize]
redirect_to [:dashboard, @member.real_source_type.tableize], notice: notice
else
if cannot_leave?
alert = "You can not leave the \"#{@member.source.human_name}\" #{source_type}."
alert << " Transfer or delete the #{source_type}."
redirect_to polymorphic_url(membershipable), alert: alert
else
render_403
end
end
redirect_to redirect_path, notice: notice
end
protected
......@@ -51,8 +40,4 @@ module MembershipActions
def membershipable
raise NotImplementedError
end
def cannot_leave?
raise NotImplementedError
end
end
......@@ -36,9 +36,7 @@ class Groups::GroupMembersController < Groups::ApplicationController
def destroy
@group_member = @group.group_members.find(params[:id])
return render_403 unless can?(current_user, :destroy_group_member, @group_member)
@group_member.destroy
Members::DestroyService.new(@group_member, current_user).execute
respond_to do |format|
format.html { redirect_to group_group_members_path(@group), notice: 'User was successfully removed from group.' }
......@@ -68,8 +66,4 @@ class Groups::GroupMembersController < Groups::ApplicationController
# MembershipActions concern
alias_method :membershipable, :group
def cannot_leave?
@group.last_owner?(current_user)
end
end
......@@ -50,9 +50,7 @@ class Projects::ProjectMembersController < Projects::ApplicationController
def destroy
@project_member = @project.project_members.find(params[:id])
return render_403 unless can?(current_user, :destroy_project_member, @project_member)
@project_member.destroy
Members::DestroyService.new(@project_member, current_user).execute
respond_to do |format|
format.html do
......@@ -98,8 +96,4 @@ class Projects::ProjectMembersController < Projects::ApplicationController
# MembershipActions concern
alias_method :membershipable, :project
def cannot_leave?
current_user == @project.owner
end
end
......@@ -48,7 +48,6 @@ class Member < ActiveRecord::Base
after_create :post_create_hook, unless: [:pending?, :importing?]
after_update :post_update_hook, unless: [:pending?, :importing?]
after_destroy :post_destroy_hook, unless: :pending?
after_destroy :post_decline_request, if: :request?
delegate :name, :username, :email, to: :user, prefix: true
......@@ -188,7 +187,7 @@ class Member < ActiveRecord::Base
end
def send_request
# override in subclass
notification_service.new_access_request(self)
end
def post_create_hook
......@@ -215,10 +214,6 @@ class Member < ActiveRecord::Base
post_create_hook
end
def post_decline_request
# override in subclass
end
def system_hook_service
SystemHooksService.new
end
......
......@@ -33,12 +33,6 @@ class GroupMember < Member
super
end
def send_request
notification_service.new_group_access_request(self)
super
end
def post_create_hook
notification_service.new_group_member(self)
......@@ -64,10 +58,4 @@ class GroupMember < Member
super
end
def post_decline_request
notification_service.decline_group_access_request(self)
super
end
end
......@@ -111,12 +111,6 @@ class ProjectMember < Member
super
end
def send_request
notification_service.new_project_access_request(self)
super
end
def post_create_hook
unless owner?
event_service.join_project(self.project, self.user)
......@@ -152,12 +146,6 @@ class ProjectMember < Member
super
end
def post_decline_request
notification_service.decline_project_access_request(self)
super
end
def event_service
EventCreateService.new
end
......
module Members
class DestroyService < BaseService
attr_accessor :member, :current_user
def initialize(member, user)
@member, @current_user = member, user
end
def execute
unless member && can?(current_user, "destroy_#{member.type.underscore}".to_sym, member)
raise Gitlab::Access::AccessDeniedError
end
member.destroy
if member.request? && member.user != current_user
notification_service.decline_access_request(member)
end
end
end
end
......@@ -181,15 +181,16 @@ class NotificationService
end
end
# Project access request
def new_project_access_request(project_member)
mailer.member_access_requested_email(project_member.real_source_type, project_member.id).deliver_later
# Members
def new_access_request(member)
mailer.member_access_requested_email(member.real_source_type, member.id).deliver_later
end
def decline_project_access_request(project_member)
mailer.member_access_denied_email(project_member.real_source_type, project_member.project.id, project_member.user.id).deliver_later
def decline_access_request(member)
mailer.member_access_denied_email(member.real_source_type, member.source_id, member.user_id).deliver_later
end
# Project invite
def invite_project_member(project_member, token)
mailer.member_invited_email(project_member.real_source_type, project_member.id, token).deliver_later
end
......@@ -216,15 +217,7 @@ class NotificationService
mailer.member_access_granted_email(project_member.real_source_type, project_member.id).deliver_later
end
# Group access request
def new_group_access_request(group_member)
mailer.member_access_requested_email(group_member.real_source_type, group_member.id).deliver_later
end
def decline_group_access_request(group_member)
mailer.member_access_denied_email(group_member.real_source_type, group_member.group.id, group_member.user.id).deliver_later
end
# Group invite
def invite_group_member(group_member, token)
mailer.member_invited_email(group_member.real_source_type, group_member.id, token).deliver_later
end
......
- if current_user
- if access = @group.users.find_by(id: current_user.id)
- can_edit = can?(current_user, :admin_group, @group)
- member = @group.members.non_request.find_by(user_id: current_user.id)
- can_leave = member && can?(current_user, :destroy_group_member, member)
.controls
.dropdown.group-settings-dropdown
%a.dropdown-new.btn.btn-default#group-settings-button{href: '#', 'data-toggle' => 'dropdown'}
= icon('cog')
= icon('caret-down')
%ul.dropdown-menu.dropdown-menu-align-right
- if can?(current_user, :admin_group, @group)
= nav_link(path: 'groups#projects') do
= link_to projects_group_path(@group), title: 'Projects' do
Projects
= link_to 'Projects', projects_group_path(@group), title: 'Projects'
%li.divider
- if can_edit
%li
= link_to edit_group_path(@group) do
Edit Group
= link_to 'Edit Group', edit_group_path(@group)
- if can_leave
%li
= link_to polymorphic_path([:leave, @group, :members]),
data: { confirm: leave_confirmation_message(@group) }, method: :delete, title: 'Leave group' do
Leave Group
......@@ -5,19 +5,20 @@
= icon('cog')
= icon('caret-down')
%ul.dropdown-menu.dropdown-menu-align-right
- is_project_member = @project.users.exists?(current_user.id)
- access = @project.team.max_member_access(current_user.id)
- can_edit = can?(current_user, :admin_project, @project)
-# We don't use @project.team.find_member because it searches for group members too...
- member = @project.members.non_request.find_by(user_id: current_user.id)
- can_leave = member && can?(current_user, :destroy_project_member, member)
= render 'layouts/nav/project_settings', access: access, can_edit: can_edit
= render 'layouts/nav/project_settings', can_edit: can_edit
- if can_edit || is_project_member
- if can_edit || can_leave
%li.divider
- if can_edit
%li
= link_to edit_project_path(@project) do
Edit Project
- if is_project_member
- if can_leave
%li
= link_to polymorphic_path([:leave, @project, :members]),
data: { confirm: leave_confirmation_message(@project) }, method: :delete, title: 'Leave project' do
......
......@@ -3,7 +3,7 @@
= link_to namespace_project_project_members_path(@project.namespace, @project), title: 'Members', class: 'team-tab tab' do
%span
Members
- if access && can_edit
- if can_edit
- if @project.allowed_to_share_with_group?
= nav_link(controller: :group_links) do
= link_to namespace_project_group_links_path(@project.namespace, @project), title: "Groups" do
......
......@@ -5,53 +5,9 @@ Feature: Dashboard Group
And "John Doe" is owner of group "Owned"
And "John Doe" is guest of group "Guest"
# Leave groups
@javascript
Scenario: Owner should be able to leave from group if he is not the last owner
Given "Mary Jane" is owner of group "Owned"
When I visit dashboard groups page
Then I should see group "Owned" in group list
Then I should see group "Guest" in group list
When I click on the "Leave" button for group "Owned"
And I visit dashboard groups page
Then I should not see group "Owned" in group list
Then I should see group "Guest" in group list
@javascript
Scenario: Owner should not be able to leave from group if he is the last owner
Given "Mary Jane" is guest of group "Owned"
When I visit dashboard groups page
Then I should see group "Owned" in group list
Then I should see group "Guest" in group list
When I click on the "Leave" button for group "Owned"
Then I should see the "Can not leave message"
@javascript
Scenario: Guest should be able to leave from group
Given "Mary Jane" is guest of group "Guest"
When I visit dashboard groups page
Then I should see group "Owned" in group list
Then I should see group "Guest" in group list
When I click on the "Leave" button for group "Guest"
When I visit dashboard groups page
Then I should see group "Owned" in group list
Then I should not see group "Guest" in group list
@javascript
Scenario: Guest should be able to leave from group even if he is the only user in the group
When I visit dashboard groups page
Then I should see group "Owned" in group list
Then I should see group "Guest" in group list
When I click on the "Leave" button for group "Guest"
When I visit dashboard groups page
Then I should see group "Owned" in group list
Then I should not see group "Guest" in group list
Scenario: Create a group from dasboard
And I visit dashboard groups page
And I click new group link
And submit form with new group "Samurai" info
Then I should be redirected to group "Samurai" page
And I should see newly created group "Samurai"
......@@ -4,44 +4,6 @@ class Spinach::Features::DashboardGroup < Spinach::FeatureSteps
include SharedPaths
include SharedUser
# Leave
step 'I click on the "Leave" button for group "Owned"' do
find(:css, 'li', text: "Owner").find(:css, 'i.fa.fa-sign-out').click
# poltergeist always confirms popups.
end
step 'I click on the "Leave" button for group "Guest"' do
find(:css, 'li', text: "Guest").find(:css, 'i.fa.fa-sign-out').click
# poltergeist always confirms popups.
end
step 'I should not see the "Leave" button for group "Owned"' do
expect(find(:css, 'li', text: "Owner")).not_to have_selector(:css, 'i.fa.fa-sign-out')
# poltergeist always confirms popups.
end
step 'I should not see the "Leave" button for groupr "Guest"' do
expect(find(:css, 'li', text: "Guest")).not_to have_selector(:css, 'i.fa.fa-sign-out')
# poltergeist always confirms popups.
end
step 'I should see group "Owned" in group list' do
expect(page).to have_content("Owned")
end
step 'I should not see group "Owned" in group list' do
expect(page).not_to have_content("Owned")
end
step 'I should see group "Guest" in group list' do
expect(page).to have_content("Guest")
end
step 'I should not see group "Guest" in group list' do
expect(page).not_to have_content("Guest")
end
step 'I click new group link' do
click_link "New Group"
end
......@@ -60,8 +22,4 @@ class Spinach::Features::DashboardGroup < Spinach::FeatureSteps
expect(page).to have_content "Samurai"
expect(page).to have_content "Tokugawa Shogunate"
end
step 'I should see the "Can not leave message"' do
expect(page).to have_content "You can not leave the \"Owned\" group."
end
end
......@@ -5,6 +5,8 @@
#
module Gitlab
module Access
class AccessDeniedError < StandardError; end
GUEST = 10
REPORTER = 20
DEVELOPER = 30
......
......@@ -118,9 +118,7 @@ describe Groups::GroupMembersController do
it 'cannot removes himself from the group' do
delete :leave, group_id: group
expect(response).to redirect_to(group_path(group))
expect(response).to set_flash[:alert].to "You can not leave the \"#{group.name}\" group. Transfer or delete the group."
expect(group.users).to include user
expect(response.status).to eq(403)
end
end
......@@ -134,7 +132,7 @@ describe Groups::GroupMembersController do
delete :leave, group_id: group
expect(response).to set_flash.to 'Your access request to the group has been withdrawn.'
expect(response).to redirect_to(dashboard_groups_path)
expect(response).to redirect_to(group_path(group))
expect(group.members.request).to be_empty
expect(group.users).not_to include user
end
......
......@@ -171,11 +171,7 @@ describe Projects::ProjectMembersController do
delete :leave, namespace_id: project.namespace,
project_id: project
expect(response).to redirect_to(
namespace_project_path(project.namespace, project)
)
expect(response).to set_flash[:alert].to "You can not leave the \"#{project.human_name}\" project. Transfer or delete the project."
expect(project.users).to include user
expect(response.status).to eq(403)
end
end
......@@ -190,7 +186,7 @@ describe Projects::ProjectMembersController do
project_id: project
expect(response).to set_flash.to 'Your access request to the project has been withdrawn.'
expect(response).to redirect_to(dashboard_projects_path)
expect(response).to redirect_to(namespace_project_path(project.namespace, project))
expect(project.members.request).to be_empty
expect(project.users).not_to include user
end
......
require 'spec_helper'
feature 'Groups > Members > Last owner cannot leave group', feature: true do
let(:owner) { create(:user) }
let(:group) { create(:group) }
background do
group.add_owner(owner)
login_as(owner)
visit group_path(group)
end
scenario 'user does not see a "Leave Group" link' do
expect(page).not_to have_content 'Leave Group'
end
end
require 'spec_helper'
feature 'Groups > Members > Member leaves group', feature: true do
let(:user) { create(:user) }
let(:owner) { create(:user) }
let(:group) { create(:group, :public) }
background do
group.add_owner(owner)
group.add_developer(user)
login_as(user)
visit group_path(group)
end
scenario 'user leaves group' do
click_link 'Leave Group'
expect(current_path).to eq(dashboard_groups_path)
expect(group.users.exists?(user.id)).to be_falsey
end
end
......@@ -21,6 +21,7 @@ feature 'Groups > Members > User requests access', feature: true do
expect(page).to have_content 'Your request for access has been queued for review.'
expect(page).to have_content 'Withdraw Access Request'
expect(page).not_to have_content 'Leave Group'
end
scenario 'user is not listed in the group members page' do
......
require 'spec_helper'
feature 'Projects > Members > Member leaves project', feature: true do
let(:user) { create(:user) }
let(:project) { create(:project) }
background do
project.team << [user, :developer]
login_as(user)
visit namespace_project_path(project.namespace, project)
end
scenario 'user leaves project' do
click_link 'Leave Project'
expect(current_path).to eq(dashboard_projects_path)
expect(project.users.exists?(user.id)).to be_falsey
end
end
require 'spec_helper'
feature 'Projects > Members > Owner cannot leave project', feature: true do
let(:owner) { create(:user) }
let(:project) { create(:project) }
background do
project.team << [owner, :owner]
login_as(owner)
visit namespace_project_path(project.namespace, project)
end
scenario 'user does not see a "Leave Project" link' do
expect(page).not_to have_content 'Leave Project'
end
end
......@@ -21,6 +21,7 @@ feature 'Projects > Members > User requests access', feature: true do
expect(page).to have_content 'Your request for access has been queued for review.'
expect(page).to have_content 'Withdraw Access Request'
expect(page).not_to have_content 'Leave Project'
end
scenario 'user is not listed in the project members page' do
......
......@@ -70,22 +70,6 @@ feature 'Project', feature: true do
end
end
describe 'leave project link' do
let(:user) { create(:user) }
let(:project) { create(:project, namespace: user.namespace) }
before do
login_with(user)
project.team.add_user(user, Gitlab::Access::MASTER)
visit namespace_project_path(project.namespace, project)
end
it 'click project-settings and find leave project' do
find('#project-settings-button').click
expect(page).to have_link('Leave Project')
end
end
describe 'project title' do
include WaitForAjax
......
......@@ -134,18 +134,6 @@ describe Member, models: true do
it { is_expected.to respond_to(:user_email) }
end
describe 'Callbacks' do
describe 'after_destroy :post_decline_request, if: :request?' do
let(:member) { create(:project_member, requested_at: Time.now.utc) }
it 'calls #post_decline_request' do
expect(member).to receive(:post_decline_request)
member.destroy
end
end
end
describe ".add_user" do
let!(:user) { create(:user) }
let(:project) { create(:project) }
......
......@@ -61,16 +61,6 @@ describe GroupMember, models: true do
end
end
describe '#post_decline_request' do
it 'calls NotificationService.decline_group_access_request' do
member = create(:group_member, user: build_stubbed(:user), requested_at: Time.now)
expect_any_instance_of(NotificationService).to receive(:decline_group_access_request)
member.__send__(:post_decline_request)
end
end
describe '#real_source_type' do
subject { create(:group_member).real_source_type }
......
......@@ -152,15 +152,5 @@ describe ProjectMember, models: true do
member.__send__(:after_accept_request)
end
end
describe '#post_decline_request' do
it 'calls NotificationService.decline_project_access_request' do
member = create(:project_member, user: build_stubbed(:user), requested_at: Time.now)
expect_any_instance_of(NotificationService).to receive(:decline_project_access_request)
member.__send__(:post_decline_request)
end
end
end
end
require 'spec_helper'
describe Members::DestroyService, services: true do
let(:user) { create(:user) }
let(:project) { create(:project) }
let!(:member) { create(:project_member, source: project) }
context 'when member is nil' do
before do
project.team << [user, :developer]
end
it 'does not destroy the member' do
expect { destroy_member(nil, user) }.to raise_error(Gitlab::Access::AccessDeniedError)
end
end
context 'when current user cannot destroy the given member' do
before do
project.team << [user, :developer]
end
it 'does not destroy the member' do
expect { destroy_member(member, user) }.to raise_error(Gitlab::Access::AccessDeniedError)
end
end
context 'when current user can destroy the given member' do
before do
project.team << [user, :master]
end
it 'destroys the member' do
destroy_member(member, user)
expect(member).to be_destroyed
end
context 'when the given member is a requester' do
before do
member.update_column(:requested_at, Time.now)
end
it 'calls Member#after_decline_request' do
expect_any_instance_of(NotificationService).to receive(:decline_access_request).with(member)
destroy_member(member, user)
end
context 'when current user is the member' do
it 'does not call Member#after_decline_request' do
expect_any_instance_of(NotificationService).not_to receive(:decline_access_request).with(member)
destroy_member(member, member.user)
end
end
context 'when current user is the member and ' do
it 'does not call Member#after_decline_request' do
expect_any_instance_of(NotificationService).not_to receive(:decline_access_request).with(member)
destroy_member(member, member.user)
end
end
end
end
def destroy_member(member, user)
Members::DestroyService.new(member, user).execute
end
end
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