Commit 22d6e69e authored by Douwe Maan's avatar Douwe Maan

Merge branch 'improve-share-locking-feature-for-subgroups' into 'master'

Improve "Share with group lock" feature for subgroups

Closes #30550

See merge request !13944
parents 6a4ebc4a 95f4dd4f
......@@ -3,6 +3,10 @@ module GroupsHelper
can?(current_user, :change_visibility_level, group)
end
def can_change_share_with_group_lock?(group)
can?(current_user, :change_share_with_group_lock, group)
end
def group_icon(group)
if group.is_a?(String)
group = Group.find_by_full_path(group)
......@@ -65,6 +69,20 @@ module GroupsHelper
{ group_name: group.name }
end
def share_with_group_lock_help_text(group)
return default_help unless group.parent&.share_with_group_lock?
if group.share_with_group_lock?
if can?(current_user, :change_share_with_group_lock, group.parent)
ancestor_locked_but_you_can_override(group)
else
ancestor_locked_so_ask_the_owner(group)
end
else
ancestor_locked_and_has_been_overridden(group)
end
end
private
def group_title_link(group, hidable: false, show_avatar: false)
......@@ -80,4 +98,45 @@ module GroupsHelper
output.html_safe
end
end
def ancestor_group(group)
ancestor = oldest_consecutively_locked_ancestor(group)
if can?(current_user, :read_group, ancestor)
link_to ancestor.name, group_path(ancestor)
else
ancestor.name
end
end
def remove_the_share_with_group_lock_from_ancestor(group)
ancestor = oldest_consecutively_locked_ancestor(group)
text = s_("GroupSettings|remove the share with group lock from %{ancestor_group_name}") % { ancestor_group_name: ancestor.name }
if can?(current_user, :admin_group, ancestor)
link_to text, edit_group_path(ancestor)
else
text
end
end
def oldest_consecutively_locked_ancestor(group)
group.ancestors.find do |group|
!group.has_parent? || !group.parent.share_with_group_lock?
end
end
def default_help
s_("GroupSettings|This setting will be applied to all subgroups unless overridden by a group owner.")
end
def ancestor_locked_but_you_can_override(group)
s_("GroupSettings|This setting is applied on %{ancestor_group}. You can override the setting or %{remove_ancestor_share_with_group_lock}.").html_safe % { ancestor_group: ancestor_group(group), remove_ancestor_share_with_group_lock: remove_the_share_with_group_lock_from_ancestor(group) }
end
def ancestor_locked_so_ask_the_owner(group)
s_("GroupSettings|This setting is applied on %{ancestor_group}. To share projects in this group with another group, ask the owner to override the setting or %{remove_ancestor_share_with_group_lock}.").html_safe % { ancestor_group: ancestor_group(group), remove_ancestor_share_with_group_lock: remove_the_share_with_group_lock_from_ancestor(group) }
end
def ancestor_locked_and_has_been_overridden(group)
s_("GroupSettings|This setting is applied on %{ancestor_group} and has been overridden on this subgroup.").html_safe % { ancestor_group: ancestor_group(group) }
end
end
......@@ -44,6 +44,10 @@ class Namespace < ActiveRecord::Base
after_commit :refresh_access_of_projects_invited_groups, on: :update, if: -> { previous_changes.key?('share_with_group_lock') }
before_create :sync_share_with_group_lock_with_parent
before_update :sync_share_with_group_lock_with_parent, if: :parent_changed?
after_update :force_share_with_group_lock_on_descendants, if: -> { share_with_group_lock_changed? && share_with_group_lock? }
# Legacy Storage specific hooks
after_update :move_dir, if: :path_changed?
......@@ -219,4 +223,14 @@ class Namespace < ActiveRecord::Base
errors.add(:parent_id, "has too deep level of nesting")
end
end
def sync_share_with_group_lock_with_parent
if parent&.share_with_group_lock?
self.share_with_group_lock = true
end
end
def force_share_with_group_lock_on_descendants
descendants.update_all(share_with_group_lock: true)
end
end
......@@ -15,6 +15,11 @@ class GroupPolicy < BasePolicy
condition(:nested_groups_supported, scope: :global) { Group.supports_nested_groups? }
condition(:has_parent, scope: :subject) { @subject.has_parent? }
condition(:share_with_group_locked, scope: :subject) { @subject.share_with_group_lock? }
condition(:parent_share_with_group_locked, scope: :subject) { @subject.parent&.share_with_group_lock? }
condition(:can_change_parent_share_with_group_lock) { can?(:change_share_with_group_lock, @subject.parent) }
condition(:has_projects) do
GroupProjectsFinder.new(group: @subject, current_user: @user).execute.any?
end
......@@ -54,6 +59,8 @@ class GroupPolicy < BasePolicy
rule { ~can?(:view_globally) }.prevent :request_access
rule { has_access }.prevent :request_access
rule { owner & (~share_with_group_locked | ~has_parent | ~parent_share_with_group_locked | can_change_parent_share_with_group_lock) }.enable :change_share_with_group_lock
def access_level
return GroupMember::NO_ACCESS if @user.nil?
......
module UpdateVisibilityLevel
def valid_visibility_level_change?(target, new_visibility)
# check that user is allowed to set specified visibility_level
if new_visibility && new_visibility.to_i != target.visibility_level
unless can?(current_user, :change_visibility_level, target) &&
Gitlab::VisibilityLevel.allowed_for?(current_user, new_visibility)
deny_visibility_level(target, new_visibility)
return false
end
end
true
end
end
module Groups
class UpdateService < Groups::BaseService
include UpdateVisibilityLevel
def execute
reject_parent_id!
# check that user is allowed to set specified visibility_level
new_visibility = params[:visibility_level]
if new_visibility && new_visibility.to_i != group.visibility_level
unless can?(current_user, :change_visibility_level, group) &&
Gitlab::VisibilityLevel.allowed_for?(current_user, new_visibility)
return false unless valid_visibility_level_change?(group, params[:visibility_level])
deny_visibility_level(group, new_visibility)
return group
end
end
return false unless valid_share_with_group_lock_change?
group.assign_attributes(params)
......@@ -30,5 +25,19 @@ module Groups
def reject_parent_id!
params.except!(:parent_id)
end
def valid_share_with_group_lock_change?
return true unless changing_share_with_group_lock?
return true if can?(current_user, :change_share_with_group_lock, group)
group.errors.add(:share_with_group_lock, s_('GroupSettings|cannot be disabled when the parent group "Share with group lock" is enabled, except by the owner of the parent group'))
false
end
def changing_share_with_group_lock?
return false if params[:share_with_group_lock].nil?
params[:share_with_group_lock] != group.share_with_group_lock
end
end
end
module Projects
class UpdateService < BaseService
include UpdateVisibilityLevel
def execute
unless visibility_level_allowed?
unless valid_visibility_level_change?(project, params[:visibility_level])
return error('New visibility level not allowed!')
end
......@@ -28,22 +30,6 @@ module Projects
private
def visibility_level_allowed?
# check that user is allowed to set specified visibility_level
new_visibility = params[:visibility_level]
if new_visibility && new_visibility.to_i != project.visibility_level
unless can?(current_user, :change_visibility_level, project) &&
Gitlab::VisibilityLevel.allowed_for?(current_user, new_visibility)
deny_visibility_level(project, new_visibility)
return false
end
end
true
end
def renaming_project_with_container_registry_tags?
new_path = params[:path]
......
......@@ -28,17 +28,20 @@
.col-sm-offset-2.col-sm-10
= render 'shared/allow_request_access', form: f
= render 'group_admin_settings', f: f
.form-group
%hr
= f.label :share_with_group_lock, class: 'control-label' do
Share with group lock
%label.control-label
= s_("GroupSettings|Share with group lock")
.col-sm-10
.checkbox
= f.check_box :share_with_group_lock
%span.descr Prevent sharing a project with another group within this group
= f.label :share_with_group_lock do
= f.check_box :share_with_group_lock, disabled: !can_change_share_with_group_lock?(@group)
%strong
- group_link = link_to @group.name, group_path(@group)
= s_("GroupSettings|Prevent sharing a project within %{group} with other groups").html_safe % { group: group_link }
%br
%span.descr= share_with_group_lock_help_text(@group)
= render 'group_admin_settings', f: f
.form-actions
= f.submit 'Save group', class: "btn btn-save"
......
---
title: '"Share with group lock" now applies to subgroups, but owner can override setting
on subgroups'
merge_request: 13944
author:
type: changed
......@@ -168,8 +168,7 @@ GitLab administrators can use the admin interface to move any project to any nam
You can [share your projects with a group](../project/members/share_project_with_groups.md)
and give your group members access to the project all at once.
Alternatively, with [GitLab Enterprise Edition Starter](https://about.gitlab.com/gitlab-ee/),
you can [lock the sharing with group feature](#share-with-group-lock-ees-eep).
Alternatively, you can [lock the sharing with group feature](#share-with-group-lock).
## Manage group memberships via LDAP
......@@ -191,10 +190,28 @@ access further configurations for your group.
#### Enforce 2FA to group members
Add a secury layer to your group by
Add a security layer to your group by
[enforcing two-factor authentication (2FA)](../../security/two_factor_authentication.md#enforcing-2fa-for-all-users-in-a-group)
to all group members.
#### Share with group lock
Prevent projects in a group from [sharing
a project with another group](../project/members/share_project_with_groups.md).
This allows for tighter control over project access.
For example, consider you have two distinct teams (Group A and Group B)
working together in a project.
To inherit the group membership, you share the project between the
two groups A and B. **Share with group lock** prevents any project within
the group from being shared with another group. By doing so, you
guarantee only the right group members have access to that projects.
To enable this feature, navigate to the group settings page. Select
**Share with group lock** and **Save the group**.
![Checkbox for share with group lock](img/share_with_group_lock.png)
#### Member Lock (EES/EEP)
Available in [GitLab Enterprise Edition Starter](https://about.gitlab.com/gitlab-ee/),
......@@ -203,15 +220,6 @@ level of members in group.
Learn more about [Member Lock](https://docs.gitlab.com/ee/user/group/index.html#member-lock-ees-eep).
#### Share with group lock (EES/EEP)
In [GitLab Enterprise Edition Starter](https://about.gitlab.com/gitlab-ee/)
it is possible to prevent projects in a group from [sharing
a project with another group](../project/members/share_project_with_groups.md).
This allows for tighter control over project access.
Learn more about [Share with group lock](https://docs.gitlab.com/ee/user/group/index.html#share-with-group-lock-ees-eep).
### Advanced settings
- **Projects**: view all projects within that group, add members to each project,
......
......@@ -34,11 +34,10 @@ After sharing 'Project Acme' with 'Engineering', the project will be listed on t
In the example above, the maximum access level of 'Developer' for members from 'Engineering' means that users with higher access levels in 'Engineering' ('Master' or 'Owner') will only have 'Developer' access to 'Project Acme'.
## Share project with group lock (EES/EEP)
## Share project with group lock
In [GitLab Enterprise Edition Starter](https://about.gitlab.com/gitlab-ee/)
it is possible to prevent projects in a group from [sharing
It is possible to prevent projects in a group from [sharing
a project with another group](../members/share_project_with_groups.md).
This allows for tighter control over project access.
Learn more about [Share with group lock](https://docs.gitlab.com/ee/user/group/index.html#share-with-group-lock-ees-eep).
Learn more about [Share with group lock](../../group/index.html#share-with-group-lock).
require 'spec_helper'
feature 'Group share with group lock' do
given(:root_owner) { create(:user) }
given(:root_group) { create(:group) }
background do
root_group.add_owner(root_owner)
sign_in(root_owner)
end
context 'with a subgroup', :nested_groups do
given!(:subgroup) { create(:group, parent: root_group) }
context 'when enabling the parent group share with group lock' do
scenario 'the subgroup share with group lock becomes enabled' do
visit edit_group_path(root_group)
check 'group_share_with_group_lock'
click_on 'Save group'
expect(subgroup.reload.share_with_group_lock?).to be_truthy
end
end
context 'when disabling the parent group share with group lock (which was already enabled)' do
background do
visit edit_group_path(root_group)
check 'group_share_with_group_lock'
click_on 'Save group'
end
context 'and the subgroup share with group lock is enabled' do
scenario 'the subgroup share with group lock does not change' do
visit edit_group_path(root_group)
uncheck 'group_share_with_group_lock'
click_on 'Save group'
expect(subgroup.reload.share_with_group_lock?).to be_truthy
end
end
context 'but the subgroup share with group lock is disabled' do
background do
visit edit_group_path(subgroup)
uncheck 'group_share_with_group_lock'
click_on 'Save group'
end
scenario 'the subgroup share with group lock does not change' do
visit edit_group_path(root_group)
uncheck 'group_share_with_group_lock'
click_on 'Save group'
expect(subgroup.reload.share_with_group_lock?).to be_falsey
end
end
end
end
end
require 'spec_helper'
feature 'Project group links', :js do
include Select2Helper
let(:master) { create(:user) }
let(:project) { create(:project) }
let!(:group) { create(:group) }
background do
project.add_master(master)
sign_in(master)
end
context 'setting an expiration date for a group link' do
before do
visit project_settings_members_path(project)
click_on 'share-with-group-tab'
select2 group.id, from: '#link_group_id'
fill_in 'expires_at_groups', with: (Time.current + 4.5.days).strftime('%Y-%m-%d')
page.find('body').click
find('.btn-create').trigger('click')
end
it 'shows the expiration time with a warning class' do
page.within('.project-members-groups') do
expect(page).to have_content('Expires in 4 days')
expect(page).to have_selector('.text-warning')
end
end
end
context 'nested group project' do
let!(:nested_group) { create(:group, parent: group) }
let!(:another_group) { create(:group) }
let!(:project) { create(:project, namespace: nested_group) }
background do
group.add_master(master)
another_group.add_master(master)
end
it 'does not show ancestors', :nested_groups do
visit project_settings_members_path(project)
click_on 'share-with-group-tab'
click_link 'Search for a group'
page.within '.select2-drop' do
expect(page).to have_content(another_group.name)
expect(page).not_to have_content(group.name)
end
end
end
describe 'the groups dropdown' do
before do
group_two = create(:group)
group.add_owner(master)
group_two.add_owner(master)
visit project_settings_members_path(project)
execute_script 'GroupsSelect.PER_PAGE = 1;'
open_select2 '#link_group_id'
end
it 'should infinitely scroll' do
expect(find('.select2-drop .select2-results')).to have_selector('.select2-result', count: 1)
scroll_select2_to_bottom('.select2-drop .select2-results:visible')
expect(find('.select2-drop .select2-results')).to have_selector('.select2-result', count: 2)
end
end
end
require 'spec_helper'
feature 'Projects > Members > Anonymous user sees members', js: true do
feature 'Projects > Members > Groups with access list', js: true do
let(:user) { create(:user) }
let(:group) { create(:group, :public) }
let(:project) { create(:project, :public) }
......@@ -13,7 +13,7 @@ feature 'Projects > Members > Anonymous user sees members', js: true do
visit project_settings_members_path(project)
end
it 'updates group access level' do
scenario 'updates group access level' do
click_button @group_link.human_access
page.within '.dropdown-menu' do
......@@ -27,7 +27,7 @@ feature 'Projects > Members > Anonymous user sees members', js: true do
expect(first('.group_member')).to have_content('Guest')
end
it 'updates expiry date' do
scenario 'updates expiry date' do
tomorrow = Date.today + 3
fill_in "member_expires_at_#{group.id}", with: tomorrow.strftime("%F")
......@@ -38,7 +38,7 @@ feature 'Projects > Members > Anonymous user sees members', js: true do
end
end
it 'deletes group link' do
scenario 'deletes group link' do
page.within(first('.group_member')) do
find('.btn-remove').click
end
......@@ -47,8 +47,8 @@ feature 'Projects > Members > Anonymous user sees members', js: true do
expect(page).not_to have_selector('.group_member')
end
context 'search' do
it 'finds no results' do
context 'search in existing members (yes, this filters the groups list as well)' do
scenario 'finds no results' do
page.within '.member-search-form' do
fill_in 'search', with: 'testing 123'
find('.member-search-btn').click
......@@ -57,7 +57,7 @@ feature 'Projects > Members > Anonymous user sees members', js: true do
expect(page).not_to have_selector('.group_member')
end
it 'finds results' do
scenario 'finds results' do
page.within '.member-search-form' do
fill_in 'search', with: group.name
find('.member-search-btn').click
......
require 'spec_helper'
feature 'Project > Members > Share with Group', :js do
include Select2Helper
include ActionView::Helpers::DateHelper
let(:master) { create(:user) }
describe 'Share with group lock' do
shared_examples 'the project can be shared with groups' do
scenario 'the "Share with group" tab exists' do
visit project_settings_members_path(project)
expect(page).to have_selector('#share-with-group-tab')
end
end
shared_examples 'the project cannot be shared with groups' do
scenario 'the "Share with group" tab does not exist' do
visit project_settings_members_path(project)
expect(page).to have_selector('#add-member-tab')
expect(page).not_to have_selector('#share-with-group-tab')
end
end
context 'for a project in a root group' do
let!(:group_to_share_with) { create(:group) }
let(:project) { create(:project, namespace: create(:group)) }
background do
project.add_master(master)
sign_in(master)
end
context 'when the group has "Share with group lock" disabled' do
it_behaves_like 'the project can be shared with groups'
scenario 'the project can be shared with another group' do
visit project_settings_members_path(project)
click_on 'share-with-group-tab'
select2 group_to_share_with.id, from: '#link_group_id'
page.find('body').click
find('.btn-create').trigger('click')
page.within('.project-members-groups') do
expect(page).to have_content(group_to_share_with.name)
end
end
end
context 'when the group has "Share with group lock" enabled' do
before do
project.namespace.update_column(:share_with_group_lock, true)
end
it_behaves_like 'the project cannot be shared with groups'
end
end
context 'for a project in a subgroup', :nested_groups do
let!(:group_to_share_with) { create(:group) }
let(:root_group) { create(:group) }
let(:subgroup) { create(:group, parent: root_group) }
let(:project) { create(:project, namespace: subgroup) }
background do
project.add_master(master)
sign_in(master)
end
context 'when the root_group has "Share with group lock" disabled' do
context 'when the subgroup has "Share with group lock" disabled' do
it_behaves_like 'the project can be shared with groups'
end
context 'when the subgroup has "Share with group lock" enabled' do
before do
subgroup.update_column(:share_with_group_lock, true)
end
it_behaves_like 'the project cannot be shared with groups'
end
end
context 'when the root_group has "Share with group lock" enabled' do
before do
root_group.update_column(:share_with_group_lock, true)
end
context 'when the subgroup has "Share with group lock" disabled (parent overridden)' do
it_behaves_like 'the project can be shared with groups'
end
context 'when the subgroup has "Share with group lock" enabled' do
before do
subgroup.update_column(:share_with_group_lock, true)
end
it_behaves_like 'the project cannot be shared with groups'
end
end
end
end
describe 'setting an expiration date for a group link' do
let(:project) { create(:project) }
let!(:group) { create(:group) }
around do |example|
Timecop.freeze { example.run }
end
before do
project.add_master(master)
sign_in(master)
visit project_settings_members_path(project)
click_on 'share-with-group-tab'
select2 group.id, from: '#link_group_id'
fill_in 'expires_at_groups', with: (Time.now + 4.5.days).strftime('%Y-%m-%d')
page.find('body').click
find('.btn-create').trigger('click')
end
scenario 'the group link shows the expiration time with a warning class' do
page.within('.project-members-groups') do
# Using distance_of_time_in_words_to_now because it is not the same as
# subtraction, and this way avoids time zone issues as well
expires_in_text = distance_of_time_in_words_to_now(project.project_group_links.first.expires_at)
expect(page).to have_content(expires_in_text)
expect(page).to have_selector('.text-warning')
end
end
end
describe 'the groups dropdown' do
context 'with multiple groups to choose from' do
let(:project) { create(:project) }
background do
project.add_master(master)
sign_in(master)
create(:group).add_owner(master)
create(:group).add_owner(master)
visit project_settings_members_path(project)
execute_script 'GroupsSelect.PER_PAGE = 1;'
open_select2 '#link_group_id'
end
it 'should infinitely scroll' do
expect(find('.select2-drop .select2-results')).to have_selector('.select2-result', count: 1)
scroll_select2_to_bottom('.select2-drop .select2-results:visible')
expect(find('.select2-drop .select2-results')).to have_selector('.select2-result', count: 2)
end
end
context 'for a project in a nested group' do
let(:group) { create(:group) }
let!(:nested_group) { create(:group, parent: group) }
let!(:group_to_share_with) { create(:group) }
let!(:project) { create(:project, namespace: nested_group) }
background do
project.add_master(master)
sign_in(master)
group.add_master(master)
group_to_share_with.add_master(master)
end
scenario 'the groups dropdown does not show ancestors', :nested_groups do
visit project_settings_members_path(project)
click_on 'share-with-group-tab'
click_link 'Search for a group'
page.within '.select2-drop' do
expect(page).to have_content(group_to_share_with.name)
expect(page).not_to have_content(group.name)
end
end
end
end
end
......@@ -95,4 +95,97 @@ describe GroupsHelper do
.to match(/<li style="text-indent: 16px;"><a.*>#{deep_nested_group.name}.*<\/li>.*<a.*>#{very_deep_nested_group.name}<\/a>/m)
end
end
# rubocop:disable Layout/SpaceBeforeComma
describe '#share_with_group_lock_help_text', :nested_groups do
let!(:root_group) { create(:group) }
let!(:subgroup) { create(:group, parent: root_group) }
let!(:sub_subgroup) { create(:group, parent: subgroup) }
let(:root_owner) { create(:user) }
let(:sub_owner) { create(:user) }
let(:sub_sub_owner) { create(:user) }
let(:possible_help_texts) do
{
default_help: "This setting will be applied to all subgroups unless overridden by a group owner",
ancestor_locked_but_you_can_override: /This setting is applied on <a .+>.+<\/a>\. You can override the setting or .+/,
ancestor_locked_so_ask_the_owner: /This setting is applied on .+\. To share projects in this group with another group, ask the owner to override the setting or remove the share with group lock from .+/,
ancestor_locked_and_has_been_overridden: /This setting is applied on .+ and has been overridden on this subgroup/
}
end
let(:possible_linked_ancestors) do
{
root_group: root_group,
subgroup: subgroup
}
end
let(:users) do
{
root_owner: root_owner,
sub_owner: sub_owner,
sub_sub_owner: sub_sub_owner
}
end
subject { helper.share_with_group_lock_help_text(sub_subgroup) }
where(:root_share_with_group_locked, :subgroup_share_with_group_locked, :sub_subgroup_share_with_group_locked, :current_user, :help_text, :linked_ancestor) do
[
[false , false , false , :root_owner , :default_help , nil],
[false , false , false , :sub_owner , :default_help , nil],
[false , false , false , :sub_sub_owner , :default_help , nil],
[false , false , true , :root_owner , :default_help , nil],
[false , false , true , :sub_owner , :default_help , nil],
[false , false , true , :sub_sub_owner , :default_help , nil],
[false , true , false , :root_owner , :ancestor_locked_and_has_been_overridden , :subgroup],
[false , true , false , :sub_owner , :ancestor_locked_and_has_been_overridden , :subgroup],
[false , true , false , :sub_sub_owner , :ancestor_locked_and_has_been_overridden , :subgroup],
[false , true , true , :root_owner , :ancestor_locked_but_you_can_override , :subgroup],
[false , true , true , :sub_owner , :ancestor_locked_but_you_can_override , :subgroup],
[false , true , true , :sub_sub_owner , :ancestor_locked_so_ask_the_owner , :subgroup],
[true , false , false , :root_owner , :default_help , nil],
[true , false , false , :sub_owner , :default_help , nil],
[true , false , false , :sub_sub_owner , :default_help , nil],
[true , false , true , :root_owner , :default_help , nil],
[true , false , true , :sub_owner , :default_help , nil],
[true , false , true , :sub_sub_owner , :default_help , nil],
[true , true , false , :root_owner , :ancestor_locked_and_has_been_overridden , :root_group],
[true , true , false , :sub_owner , :ancestor_locked_and_has_been_overridden , :root_group],
[true , true , false , :sub_sub_owner , :ancestor_locked_and_has_been_overridden , :root_group],
[true , true , true , :root_owner , :ancestor_locked_but_you_can_override , :root_group],
[true , true , true , :sub_owner , :ancestor_locked_so_ask_the_owner , :root_group],
[true , true , true , :sub_sub_owner , :ancestor_locked_so_ask_the_owner , :root_group]
]
end
with_them do
before do
root_group.add_owner(root_owner)
subgroup.add_owner(sub_owner)
sub_subgroup.add_owner(sub_sub_owner)
root_group.update_column(:share_with_group_lock, true) if root_share_with_group_locked
subgroup.update_column(:share_with_group_lock, true) if subgroup_share_with_group_locked
sub_subgroup.update_column(:share_with_group_lock, true) if sub_subgroup_share_with_group_locked
allow(helper).to receive(:current_user).and_return(users[current_user])
allow(helper).to receive(:can?)
.with(users[current_user], :change_share_with_group_lock, subgroup)
.and_return(Ability.allowed?(users[current_user], :change_share_with_group_lock, subgroup))
ancestor = possible_linked_ancestors[linked_ancestor]
if ancestor
allow(helper).to receive(:can?)
.with(users[current_user], :read_group, ancestor)
.and_return(Ability.allowed?(users[current_user], :read_group, ancestor))
allow(helper).to receive(:can?)
.with(users[current_user], :admin_group, ancestor)
.and_return(Ability.allowed?(users[current_user], :admin_group, ancestor))
end
end
it 'has the correct help text with correct ancestor links' do
expect(subject).to match(possible_help_texts[help_text])
expect(subject).to match(possible_linked_ancestors[linked_ancestor].name) unless help_text == :default_help
end
end
end
end
......@@ -406,4 +406,116 @@ describe Namespace do
it { expect(group.all_projects.to_a).to match_array([project2, project1]) }
end
describe '#share_with_group_lock with subgroups', :nested_groups do
context 'when creating a subgroup' do
let(:subgroup) { create(:group, parent: root_group )}
context 'under a parent with "Share with group lock" enabled' do
let(:root_group) { create(:group, share_with_group_lock: true) }
it 'enables "Share with group lock" on the subgroup' do
expect(subgroup.share_with_group_lock).to be_truthy
end
end
context 'under a parent with "Share with group lock" disabled' do
let(:root_group) { create(:group) }
it 'does not enable "Share with group lock" on the subgroup' do
expect(subgroup.share_with_group_lock).to be_falsey
end
end
end
context 'when enabling the parent group "Share with group lock"' do
let(:root_group) { create(:group) }
let!(:subgroup) { create(:group, parent: root_group )}
it 'the subgroup "Share with group lock" becomes enabled' do
root_group.update!(share_with_group_lock: true)
expect(subgroup.reload.share_with_group_lock).to be_truthy
end
end
context 'when disabling the parent group "Share with group lock" (which was already enabled)' do
let(:root_group) { create(:group, share_with_group_lock: true) }
context 'and the subgroup "Share with group lock" is enabled' do
let(:subgroup) { create(:group, parent: root_group, share_with_group_lock: true )}
it 'the subgroup "Share with group lock" does not change' do
root_group.update!(share_with_group_lock: false)
expect(subgroup.reload.share_with_group_lock).to be_truthy
end
end
context 'but the subgroup "Share with group lock" is disabled' do
let(:subgroup) { create(:group, parent: root_group )}
it 'the subgroup "Share with group lock" does not change' do
root_group.update!(share_with_group_lock: false)
expect(subgroup.reload.share_with_group_lock?).to be_falsey
end
end
end
# Note: Group transfers are not yet implemented
context 'when a group is transferred into a root group' do
context 'when the root group "Share with group lock" is enabled' do
let(:root_group) { create(:group, share_with_group_lock: true) }
context 'when the subgroup "Share with group lock" is enabled' do
let(:subgroup) { create(:group, share_with_group_lock: true )}
it 'the subgroup "Share with group lock" does not change' do
subgroup.parent = root_group
subgroup.save!
expect(subgroup.share_with_group_lock).to be_truthy
end
end
context 'when the subgroup "Share with group lock" is disabled' do
let(:subgroup) { create(:group)}
it 'the subgroup "Share with group lock" becomes enabled' do
subgroup.parent = root_group
subgroup.save!
expect(subgroup.share_with_group_lock).to be_truthy
end
end
end
context 'when the root group "Share with group lock" is disabled' do
let(:root_group) { create(:group) }
context 'when the subgroup "Share with group lock" is enabled' do
let(:subgroup) { create(:group, share_with_group_lock: true )}
it 'the subgroup "Share with group lock" does not change' do
subgroup.parent = root_group
subgroup.save!
expect(subgroup.share_with_group_lock).to be_truthy
end
end
context 'when the subgroup "Share with group lock" is disabled' do
let(:subgroup) { create(:group)}
it 'the subgroup "Share with group lock" does not change' do
subgroup.parent = root_group
subgroup.save!
expect(subgroup.share_with_group_lock).to be_falsey
end
end
end
end
end
end
......@@ -242,4 +242,94 @@ describe GroupPolicy do
end
end
end
describe 'change_share_with_group_lock' do
context 'when the current_user owns the group' do
let(:current_user) { owner }
context 'when the group share_with_group_lock is enabled' do
let(:group) { create(:group, share_with_group_lock: true, parent: parent) }
context 'when the parent group share_with_group_lock is enabled' do
context 'when the group has a grandparent' do
let(:parent) { create(:group, share_with_group_lock: true, parent: grandparent) }
context 'when the grandparent share_with_group_lock is enabled' do
let(:grandparent) { create(:group, share_with_group_lock: true) }
context 'when the current_user owns the parent' do
before do
parent.add_owner(current_user)
end
context 'when the current_user owns the grandparent' do
before do
grandparent.add_owner(current_user)
end
it { expect_allowed(:change_share_with_group_lock) }
end
context 'when the current_user does not own the grandparent' do
it { expect_disallowed(:change_share_with_group_lock) }
end
end
context 'when the current_user does not own the parent' do
it { expect_disallowed(:change_share_with_group_lock) }
end
end
context 'when the grandparent share_with_group_lock is disabled' do
let(:grandparent) { create(:group) }
context 'when the current_user owns the parent' do
before do
parent.add_owner(current_user)
end
it { expect_allowed(:change_share_with_group_lock) }
end
context 'when the current_user does not own the parent' do
it { expect_disallowed(:change_share_with_group_lock) }
end
end
end
context 'when the group does not have a grandparent' do
let(:parent) { create(:group, share_with_group_lock: true) }
context 'when the current_user owns the parent' do
before do
parent.add_owner(current_user)
end
it { expect_allowed(:change_share_with_group_lock) }
end
context 'when the current_user does not own the parent' do
it { expect_disallowed(:change_share_with_group_lock) }
end
end
end
context 'when the parent group share_with_group_lock is disabled' do
let(:parent) { create(:group) }
it { expect_allowed(:change_share_with_group_lock) }
end
end
context 'when the group share_with_group_lock is disabled' do
it { expect_allowed(:change_share_with_group_lock) }
end
end
context 'when the current_user does not own the group' do
let(:current_user) { create(:user) }
it { expect_disallowed(:change_share_with_group_lock) }
end
end
end
......@@ -100,4 +100,38 @@ describe Groups::UpdateService do
end
end
end
context 'for a subgroup', :nested_groups do
let(:subgroup) { create(:group, :private, parent: private_group) }
context 'when the parent group share_with_group_lock is enabled' do
before do
private_group.update_column(:share_with_group_lock, true)
end
context 'for the parent group owner' do
it 'allows disabling share_with_group_lock' do
private_group.add_owner(user)
result = described_class.new(subgroup, user, share_with_group_lock: false).execute
expect(result).to be_truthy
expect(subgroup.reload.share_with_group_lock).to be_falsey
end
end
context 'for a subgroup owner (who does not own the parent)' do
it 'does not allow disabling share_with_group_lock' do
subgroup_owner = create(:user)
subgroup.add_owner(subgroup_owner)
result = described_class.new(subgroup, subgroup_owner, share_with_group_lock: false).execute
expect(result).to be_falsey
expect(subgroup.errors.full_messages.first).to match(/cannot be disabled when the parent group "Share with group lock" is enabled, except by the owner of the parent group/)
expect(subgroup.reload.share_with_group_lock).to be_truthy
end
end
end
end
end
require 'spec_helper'
describe 'groups/edit.html.haml' do
include Devise::Test::ControllerHelpers
describe '"Share with group lock" setting' do
let(:root_owner) { create(:user) }
let(:root_group) { create(:group) }
before do
root_group.add_owner(root_owner)
end
shared_examples_for '"Share with group lock" setting' do |checkbox_options|
it 'should have the correct label, help text, and checkbox options' do
assign(:group, test_group)
allow(view).to receive(:can?).with(test_user, :admin_group, test_group).and_return(true)
allow(view).to receive(:can_change_group_visibility_level?).and_return(false)
allow(view).to receive(:current_user).and_return(test_user)
expect(view).to receive(:can_change_share_with_group_lock?).and_return(!checkbox_options[:disabled])
expect(view).to receive(:share_with_group_lock_help_text).and_return('help text here')
render
expect(rendered).to have_content("Prevent sharing a project within #{test_group.name} with other groups")
expect(rendered).to have_css('.descr', text: 'help text here')
expect(rendered).to have_field('group_share_with_group_lock', checkbox_options)
end
end
context 'for a root group' do
let(:test_group) { root_group }
let(:test_user) { root_owner }
it_behaves_like '"Share with group lock" setting', { disabled: false, checked: false }
end
context 'for a subgroup', :nested_groups do
let!(:subgroup) { create(:group, parent: root_group) }
let(:sub_owner) { create(:user) }
let(:test_group) { subgroup }
context 'when the root_group has "Share with group lock" disabled' do
context 'when the subgroup has "Share with group lock" disabled' do
context 'as the root_owner' do
let(:test_user) { root_owner }
it_behaves_like '"Share with group lock" setting', { disabled: false, checked: false }
end
context 'as the sub_owner' do
let(:test_user) { sub_owner }
it_behaves_like '"Share with group lock" setting', { disabled: false, checked: false }
end
end
context 'when the subgroup has "Share with group lock" enabled' do
before do
subgroup.update_column(:share_with_group_lock, true)
end
context 'as the root_owner' do
let(:test_user) { root_owner }
it_behaves_like '"Share with group lock" setting', { disabled: false, checked: true }
end
context 'as the sub_owner' do
let(:test_user) { sub_owner }
it_behaves_like '"Share with group lock" setting', { disabled: false, checked: true }
end
end
end
context 'when the root_group has "Share with group lock" enabled' do
before do
root_group.update_column(:share_with_group_lock, true)
end
context 'when the subgroup has "Share with group lock" disabled (parent overridden)' do
context 'as the root_owner' do
let(:test_user) { root_owner }
it_behaves_like '"Share with group lock" setting', { disabled: false, checked: false }
end
context 'as the sub_owner' do
let(:test_user) { sub_owner }
it_behaves_like '"Share with group lock" setting', { disabled: false, checked: false }
end
end
context 'when the subgroup has "Share with group lock" enabled (same as parent)' do
before do
subgroup.update_column(:share_with_group_lock, true)
end
context 'as the root_owner' do
let(:test_user) { root_owner }
it_behaves_like '"Share with group lock" setting', { disabled: false, checked: true }
end
context 'as the sub_owner' do
let(:test_user) { sub_owner }
it_behaves_like '"Share with group lock" setting', { disabled: true, checked: true }
end
end
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