Commit 35d4344e authored by Yorick Peterse's avatar Yorick Peterse

Merge branch 'fix/security-group-user-removal' into 'master'

[master] Resolve "Removing a user from a private group doesn't remove them from group's project, if their project's role was changed"

See merge request gitlab/gitlabhq!2629
parents ba727d97 0034119e
...@@ -35,7 +35,9 @@ module MembershipActions ...@@ -35,7 +35,9 @@ module MembershipActions
respond_to do |format| respond_to do |format|
format.html do format.html do
message = "User was successfully removed from #{source_type}." source = source_type == 'group' ? 'group and any subresources' : source_type
message = "User was successfully removed from #{source}."
redirect_to members_page_url, notice: message redirect_to members_page_url, notice: message
end end
......
...@@ -18,12 +18,13 @@ module MembersHelper ...@@ -18,12 +18,13 @@ module MembersHelper
"remove #{member.user.name} from" "remove #{member.user.name} from"
end end
"#{text} #{action} the #{member.source.human_name} #{member.real_source_type.humanize(capitalize: false)}?" "#{text} #{action} the #{member.source.human_name} #{source_text(member)}?"
end end
def remove_member_title(member) def remove_member_title(member)
action = member.request? ? 'Deny access request' : 'Remove user' action = member.request? ? 'Deny access request' : 'Remove user'
"#{action} from #{member.real_source_type.humanize(capitalize: false)}"
"#{action} from #{source_text(member)}"
end end
def leave_confirmation_message(member_source) def leave_confirmation_message(member_source)
...@@ -35,4 +36,14 @@ module MembersHelper ...@@ -35,4 +36,14 @@ module MembersHelper
options = params.slice(:search, :sort).merge(options).permit! options = params.slice(:search, :sort).merge(options).permit!
"#{request.path}?#{options.to_param}" "#{request.path}?#{options.to_param}"
end end
private
def source_text(member)
type = member.real_source_type.humanize(capitalize: false)
return type if member.request? || member.invite? || type != 'group'
'group and any subresources'
end
end end
...@@ -78,6 +78,7 @@ class Member < ActiveRecord::Base ...@@ -78,6 +78,7 @@ class Member < ActiveRecord::Base
scope :owners, -> { active.where(access_level: OWNER) } scope :owners, -> { active.where(access_level: OWNER) }
scope :owners_and_maintainers, -> { active.where(access_level: [OWNER, MAINTAINER]) } scope :owners_and_maintainers, -> { active.where(access_level: [OWNER, MAINTAINER]) }
scope :owners_and_masters, -> { owners_and_maintainers } # @deprecated scope :owners_and_masters, -> { owners_and_maintainers } # @deprecated
scope :with_user, -> (user) { where(user: user) }
scope :order_name_asc, -> { left_join_users.reorder(Gitlab::Database.nulls_last_order('users.name', 'ASC')) } 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_name_desc, -> { left_join_users.reorder(Gitlab::Database.nulls_last_order('users.name', 'DESC')) }
......
...@@ -12,6 +12,8 @@ class GroupMember < Member ...@@ -12,6 +12,8 @@ class GroupMember < Member
validates :source_type, format: { with: /\ANamespace\z/ } validates :source_type, format: { with: /\ANamespace\z/ }
default_scope { where(source_type: SOURCE_TYPE) } default_scope { where(source_type: SOURCE_TYPE) }
scope :in_groups, ->(groups) { where(source_id: groups.select(:id)) }
after_create :update_two_factor_requirement, unless: :invite? after_create :update_two_factor_requirement, unless: :invite?
after_destroy :update_two_factor_requirement, unless: :invite? after_destroy :update_two_factor_requirement, unless: :invite?
......
...@@ -12,6 +12,10 @@ class ProjectMember < Member ...@@ -12,6 +12,10 @@ class ProjectMember < Member
default_scope { where(source_type: SOURCE_TYPE) } default_scope { where(source_type: SOURCE_TYPE) }
scope :in_project, ->(project) { where(source_id: project.id) } scope :in_project, ->(project) { where(source_id: project.id) }
scope :in_namespaces, ->(groups) do
joins('INNER JOIN projects ON projects.id = members.source_id')
.where('projects.namespace_id in (?)', groups.select(:id))
end
class << self class << self
# Add users to projects with passed access option # Add users to projects with passed access option
......
...@@ -2,9 +2,11 @@ ...@@ -2,9 +2,11 @@
module Members module Members
class DestroyService < Members::BaseService class DestroyService < Members::BaseService
def execute(member, skip_authorization: false) def execute(member, skip_authorization: false, skip_subresources: false)
raise Gitlab::Access::AccessDeniedError unless skip_authorization || can_destroy_member?(member) raise Gitlab::Access::AccessDeniedError unless skip_authorization || can_destroy_member?(member)
@skip_auth = skip_authorization
return member if member.is_a?(GroupMember) && member.source.last_owner?(member.user) return member if member.is_a?(GroupMember) && member.source.last_owner?(member.user)
member.destroy member.destroy
...@@ -15,6 +17,7 @@ module Members ...@@ -15,6 +17,7 @@ module Members
notification_service.decline_access_request(member) notification_service.decline_access_request(member)
end end
delete_subresources(member) unless skip_subresources
enqueue_delete_todos(member) enqueue_delete_todos(member)
after_execute(member: member) after_execute(member: member)
...@@ -24,6 +27,29 @@ module Members ...@@ -24,6 +27,29 @@ module Members
private private
def delete_subresources(member)
return unless member.is_a?(GroupMember) && member.user && member.group
delete_project_members(member)
delete_subgroup_members(member) if Group.supports_nested_objects?
end
def delete_project_members(member)
groups = member.group.self_and_descendants
ProjectMember.in_namespaces(groups).with_user(member.user).each do |project_member|
self.class.new(current_user).execute(project_member, skip_authorization: @skip_auth)
end
end
def delete_subgroup_members(member)
groups = member.group.descendants
GroupMember.in_groups(groups).with_user(member.user).each do |group_member|
self.class.new(current_user).execute(group_member, skip_authorization: @skip_auth, skip_subresources: true)
end
end
def can_destroy_member?(member) def can_destroy_member?(member)
can?(current_user, destroy_member_permission(member), member) can?(current_user, destroy_member_permission(member), member)
end end
......
---
title: Add subresources removal to member destroy service
merge_request:
author:
type: security
...@@ -126,7 +126,7 @@ describe Groups::GroupMembersController do ...@@ -126,7 +126,7 @@ describe Groups::GroupMembersController do
it '[HTML] removes user from members' do it '[HTML] removes user from members' do
delete :destroy, params: { group_id: group, id: member } delete :destroy, params: { group_id: group, id: member }
expect(response).to set_flash.to 'User was successfully removed from group.' expect(response).to set_flash.to 'User was successfully removed from group and any subresources.'
expect(response).to redirect_to(group_group_members_path(group)) expect(response).to redirect_to(group_group_members_path(group))
expect(group.members).not_to include member expect(group.members).not_to include member
end end
......
...@@ -16,7 +16,7 @@ describe MembersHelper do ...@@ -16,7 +16,7 @@ describe MembersHelper do
it { expect(remove_member_message(project_member_invite)).to eq "Are you sure you want to revoke the invitation for #{project_member_invite.invite_email} to join the #{project.full_name} project?" } it { expect(remove_member_message(project_member_invite)).to eq "Are you sure you want to revoke the invitation for #{project_member_invite.invite_email} to join the #{project.full_name} project?" }
it { expect(remove_member_message(project_member_request)).to eq "Are you sure you want to deny #{requester.name}'s request to join the #{project.full_name} project?" } it { expect(remove_member_message(project_member_request)).to eq "Are you sure you want to deny #{requester.name}'s request to join the #{project.full_name} project?" }
it { expect(remove_member_message(project_member_request, user: requester)).to eq "Are you sure you want to withdraw your access request for the #{project.full_name} project?" } it { expect(remove_member_message(project_member_request, user: requester)).to eq "Are you sure you want to withdraw your access request for the #{project.full_name} project?" }
it { expect(remove_member_message(group_member)).to eq "Are you sure you want to remove #{group_member.user.name} from the #{group.name} group?" } it { expect(remove_member_message(group_member)).to eq "Are you sure you want to remove #{group_member.user.name} from the #{group.name} group and any subresources?" }
it { expect(remove_member_message(group_member_invite)).to eq "Are you sure you want to revoke the invitation for #{group_member_invite.invite_email} to join the #{group.name} group?" } it { expect(remove_member_message(group_member_invite)).to eq "Are you sure you want to revoke the invitation for #{group_member_invite.invite_email} to join the #{group.name} group?" }
it { expect(remove_member_message(group_member_request)).to eq "Are you sure you want to deny #{requester.name}'s request to join the #{group.name} group?" } it { expect(remove_member_message(group_member_request)).to eq "Are you sure you want to deny #{requester.name}'s request to join the #{group.name} group?" }
it { expect(remove_member_message(group_member_request, user: requester)).to eq "Are you sure you want to withdraw your access request for the #{group.name} group?" } it { expect(remove_member_message(group_member_request, user: requester)).to eq "Are you sure you want to withdraw your access request for the #{group.name} group?" }
...@@ -33,7 +33,7 @@ describe MembersHelper do ...@@ -33,7 +33,7 @@ describe MembersHelper do
it { expect(remove_member_title(project_member)).to eq 'Remove user from project' } it { expect(remove_member_title(project_member)).to eq 'Remove user from project' }
it { expect(remove_member_title(project_member_request)).to eq 'Deny access request from project' } it { expect(remove_member_title(project_member_request)).to eq 'Deny access request from project' }
it { expect(remove_member_title(group_member)).to eq 'Remove user from group' } it { expect(remove_member_title(group_member)).to eq 'Remove user from group and any subresources' }
it { expect(remove_member_title(group_member_request)).to eq 'Deny access request from group' } it { expect(remove_member_title(group_member_request)).to eq 'Deny access request from group' }
end end
......
...@@ -69,14 +69,14 @@ describe Members::DestroyService do ...@@ -69,14 +69,14 @@ describe Members::DestroyService do
it 'calls Member#after_decline_request' do it 'calls Member#after_decline_request' do
expect_any_instance_of(NotificationService).to receive(:decline_access_request).with(member) expect_any_instance_of(NotificationService).to receive(:decline_access_request).with(member)
described_class.new(current_user).execute(member) described_class.new(current_user).execute(member, opts)
end end
context 'when current user is the member' do context 'when current user is the member' do
it 'does not call Member#after_decline_request' do it 'does not call Member#after_decline_request' do
expect_any_instance_of(NotificationService).not_to receive(:decline_access_request).with(member) expect_any_instance_of(NotificationService).not_to receive(:decline_access_request).with(member)
described_class.new(member_user).execute(member) described_class.new(member_user).execute(member, opts)
end end
end end
end end
...@@ -159,7 +159,7 @@ describe Members::DestroyService do ...@@ -159,7 +159,7 @@ describe Members::DestroyService do
end end
it_behaves_like 'a service destroying a member' do it_behaves_like 'a service destroying a member' do
let(:opts) { { skip_authorization: true } } let(:opts) { { skip_authorization: true, skip_subresources: true } }
let(:member) { group_project.requesters.find_by(user_id: member_user.id) } let(:member) { group_project.requesters.find_by(user_id: member_user.id) }
end end
...@@ -168,12 +168,14 @@ describe Members::DestroyService do ...@@ -168,12 +168,14 @@ describe Members::DestroyService do
end end
it_behaves_like 'a service destroying a member' do it_behaves_like 'a service destroying a member' do
let(:opts) { { skip_authorization: true } } let(:opts) { { skip_authorization: true, skip_subresources: true } }
let(:member) { group.requesters.find_by(user_id: member_user.id) } let(:member) { group.requesters.find_by(user_id: member_user.id) }
end end
end end
context 'when current user can destroy the given access requester' do context 'when current user can destroy the given access requester' do
let(:opts) { { skip_subresources: true } }
before do before do
group_project.add_maintainer(current_user) group_project.add_maintainer(current_user)
group.add_owner(current_user) group.add_owner(current_user)
...@@ -229,4 +231,54 @@ describe Members::DestroyService do ...@@ -229,4 +231,54 @@ describe Members::DestroyService do
end end
end end
end end
context 'subresources' do
let(:user) { create(:user) }
let(:member_user) { create(:user) }
let(:opts) { {} }
let(:group) { create(:group, :public) }
let(:subgroup) { create(:group, parent: group) }
let(:subsubgroup) { create(:group, parent: subgroup) }
let(:subsubproject) { create(:project, group: subsubgroup) }
let(:group_project) { create(:project, :public, group: group) }
let(:control_project) { create(:project, group: subsubgroup) }
before do
create(:group_member, :developer, group: subsubgroup, user: member_user)
subsubproject.add_developer(member_user)
control_project.add_maintainer(user)
group.add_owner(user)
group_member = create(:group_member, :developer, group: group, user: member_user)
described_class.new(user).execute(group_member, opts)
end
it 'removes the project membership' do
expect(group_project.members.map(&:user)).not_to include(member_user)
end
it 'removes the group membership' do
expect(group.members.map(&:user)).not_to include(member_user)
end
it 'removes the subgroup membership', :postgresql do
expect(subgroup.members.map(&:user)).not_to include(member_user)
end
it 'removes the subsubgroup membership', :postgresql do
expect(subsubgroup.members.map(&:user)).not_to include(member_user)
end
it 'removes the subsubproject membership', :postgresql do
expect(subsubproject.members.map(&:user)).not_to include(member_user)
end
it 'does not remove the user from the control project' do
expect(control_project.members.map(&:user)).to include(user)
end
end
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