diff --git a/app/views/groups/settings/_permissions.html.haml b/app/views/groups/settings/_permissions.html.haml
index f1a91ea1d7f1195a59b36a34a33fa6ffbe578319..dfc244825a213792fade3a6a34289e4448d94553 100644
--- a/app/views/groups/settings/_permissions.html.haml
+++ b/app/views/groups/settings/_permissions.html.haml
@@ -43,6 +43,7 @@
= render 'groups/settings/two_factor_auth', f: f, group: @group
= render 'groups/settings/membership', f: f, group: @group
= render_if_exists 'groups/settings/remove_dormant_members', f: f, group: @group
+ = render_if_exists 'groups/settings/disable_invite_members', f: f, group: @group
= render_if_exists 'groups/settings/extensions_marketplace', f: f, group: @group
= render_if_exists 'groups/settings/pages_access_control', f: f, group: @group
diff --git a/doc/user/group/manage.md b/doc/user/group/manage.md
index 19a3ab593d092b71515001d1a156dc162a043e97..5e87ed2e24dbc06698ac7cffe3f2addc74ca3bf6 100644
--- a/doc/user/group/manage.md
+++ b/doc/user/group/manage.md
@@ -250,6 +250,30 @@ To disable group mentions:
1. Select **Group mentions are disabled**.
1. Select **Save changes**.
+## Disable user invitations to a group
+
+{{< history >}}
+
+- [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/189898) in GitLab 18.0. Disabled by default.
+
+{{< /history >}}
+
+You can disable the ability for users to invite new members to sub-groups or projects in a top-level
+group. This also stops group Owners from sending invites. You must disable this setting before you
+can invite users again.
+
+Prerequisites:
+
+- You must have the Owner role for the group.
+
+To disable user invitations:
+
+1. On the left sidebar, select **Search or go to** and find your group.
+1. Select **Settings > General**.
+1. Expand the **Permissions and group features** section.
+1. Select **Disable Group/Project members invitation**.
+1. Select **Save changes**.
+
## Export members as CSV
{{< details >}}
diff --git a/ee/app/helpers/ee/groups/group_members_helper.rb b/ee/app/helpers/ee/groups/group_members_helper.rb
index 2ee025301714fb78e4678310fa44c17ba55ed387..0a55fb856e7c7dbdd705aaf44822d941c5274974 100644
--- a/ee/app/helpers/ee/groups/group_members_helper.rb
+++ b/ee/app/helpers/ee/groups/group_members_helper.rb
@@ -39,6 +39,15 @@ def group_members_app_data(
# rubocop:enable Metrics/ParameterLists
def group_member_header_subtext(group)
+ unless current_user && can?(current_user, :invite_group_members, group)
+ if Gitlab::Saas.feature_available?(:group_disable_invite_members)
+ return cannot_invite_member_subtext(group.name, "group owner")
+ end
+
+ return cannot_invite_member_subtext(group.name, "administrator")
+
+ end
+
if ::Namespaces::FreeUserCap::Enforcement.new(group.root_ancestor).enforce_cap? &&
can?(current_user, :admin_group_member, group.root_ancestor)
super + member_header_manage_namespace_members_text(group.root_ancestor)
@@ -56,4 +65,12 @@ def available_group_roles(group)
super + custom_role_options
end
+
+ private
+
+ def cannot_invite_member_subtext(group_name, actor)
+ safe_format(
+ _("You cannot invite a new member to %{strong_start}%{group_name}%{strong_end} since its disabled by %{actor}."),
+ tag_pair(tag.strong, :strong_start, :strong_end), group_name: group_name, actor: actor)
+ end
end
diff --git a/ee/app/helpers/ee/projects/project_members_helper.rb b/ee/app/helpers/ee/projects/project_members_helper.rb
index 1d7bcf7192a70c5680978fc581bb9fc5302ecdd0..971b471c8467f0afd52298dc780676fb80803ad3 100644
--- a/ee/app/helpers/ee/projects/project_members_helper.rb
+++ b/ee/app/helpers/ee/projects/project_members_helper.rb
@@ -25,6 +25,17 @@ def can_approve_access_requests(project)
end
def project_member_header_subtext(project)
+ unless can?(current_user, :invite_project_members, project)
+ if ::Gitlab::Saas.feature_available?(:group_disable_invite_members)
+ return "You cannot invite a new member to #{project.name}. " \
+ "User invitations are disabled by the group owner."
+ end
+
+ return "You cannot invite a new member to #{project.name}. " \
+ "User invitations are disabled by the instance administrator."
+
+ end
+
if project.group &&
::Namespaces::FreeUserCap::Enforcement.new(project.root_ancestor).enforce_cap? &&
can?(current_user, :admin_group_member, project.root_ancestor)
diff --git a/ee/app/policies/ee/group_policy.rb b/ee/app/policies/ee/group_policy.rb
index e9e0b7065b454450e0e9f92f8248cc70cac91ad5..da33504e954f98418cd60a52d05ee6a7b625b120 100644
--- a/ee/app/policies/ee/group_policy.rb
+++ b/ee/app/policies/ee/group_policy.rb
@@ -973,6 +973,12 @@ module GroupPolicy
@subject.licensed_feature_available?(:group_bulk_edit)
end
+ condition(:disable_invite_members_for_group, scope: :subject) do
+ ::Gitlab::Saas.feature_available?(:group_disable_invite_members) &&
+ @subject.licensed_feature_available?(:disable_invite_members) &&
+ @subject.root_ancestor.disable_invite_members?
+ end
+
condition(:disable_invite_members, scope: :global) do
::License.feature_available?(:disable_invite_members) &&
::Gitlab::CurrentSettings.current_application_settings.disable_invite_members?
@@ -982,7 +988,7 @@ module GroupPolicy
enable :bulk_admin_epic
end
- rule { ~admin & disable_invite_members }.policy do
+ rule { ~admin & ((~is_gitlab_com & disable_invite_members) | disable_invite_members_for_group) }.policy do
prevent :invite_group_members
end
diff --git a/ee/app/policies/ee/project_policy.rb b/ee/app/policies/ee/project_policy.rb
index e7a30106adad9c161084014f11a7a65202f7998e..1c7ad156f2bf16f241ba5e1afd279f645ca76ab0 100644
--- a/ee/app/policies/ee/project_policy.rb
+++ b/ee/app/policies/ee/project_policy.rb
@@ -72,6 +72,14 @@ module ProjectPolicy
::Gitlab::CurrentSettings.disable_overriding_approvers_per_merge_request
end
+ with_scope :subject
+ condition(:disable_invite_members_for_group) do
+ ::Gitlab::Saas.feature_available?(:group_disable_invite_members) &&
+ @subject.group &&
+ @subject.group.root_ancestor.licensed_feature_available?(:disable_invite_members) &&
+ @subject.group.root_ancestor.disable_invite_members?
+ end
+
with_scope :global
condition(:disable_invite_members) do
License.feature_available?(:disable_invite_members) &&
@@ -408,7 +416,7 @@ module ProjectPolicy
prevent(:read_issue_analytics)
end
- rule { ~admin & disable_invite_members }.policy do
+ rule { ~admin & ((~is_gitlab_com & disable_invite_members) | disable_invite_members_for_group) }.policy do
prevent :invite_project_members
end
diff --git a/ee/app/views/groups/settings/_disable_invite_members.html.haml b/ee/app/views/groups/settings/_disable_invite_members.html.haml
new file mode 100644
index 0000000000000000000000000000000000000000..9374936e459b88809b007b5f431bdbb9d3ee01d0
--- /dev/null
+++ b/ee/app/views/groups/settings/_disable_invite_members.html.haml
@@ -0,0 +1,11 @@
+- return unless group.root? && group.licensed_feature_available?(:disable_invite_members)
+
+%h5= _('User invitation restrictions')
+
+.form-group.gl-mb-3
+ = f.gitlab_ui_checkbox_component :disable_invite_members, checkbox_options: { checked: group.disable_invite_members? } do |c|
+ - c.with_label do
+ = s_('GroupSettings|Disable user invitations to groups and projects within %{group}').html_safe % { group: link_to_group(group) }
+ - c.with_help_text do
+ - learn_more_link = link_to(_('Learn more'), help_page_path('user/group/manage.md', anchor: 'disable-user-invitations-to-a-group'))
+ = s_("GroupSettings|If enabled, users can no longer invite members to groups or projects in the top-level group. %{learn_more_link}.").html_safe % { learn_more_link: learn_more_link }
diff --git a/ee/config/saas_features/group_disable_invite_members.yml b/ee/config/saas_features/group_disable_invite_members.yml
new file mode 100644
index 0000000000000000000000000000000000000000..dcf86eff77dee0a71541f2bfd49f2ddf32bee17c
--- /dev/null
+++ b/ee/config/saas_features/group_disable_invite_members.yml
@@ -0,0 +1,5 @@
+---
+name: group_disable_invite_members
+introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/189898
+milestone: '18.0'
+group: group::authentication
diff --git a/ee/lib/ee/gitlab/saas.rb b/ee/lib/ee/gitlab/saas.rb
index af164cb3af6937813b83100f297264881a5568dc..9e9ba46faf6af274ebd9b05d72a550f995bbeea2 100644
--- a/ee/lib/ee/gitlab/saas.rb
+++ b/ee/lib/ee/gitlab/saas.rb
@@ -40,6 +40,7 @@ module Saas
instance_push_limit
hide_project_instance_tab
cloud_connector_self_signed_tokens
+ group_disable_invite_members
].freeze
CONFIG_FILE_ROOT = 'ee/config/saas_features'
diff --git a/ee/spec/helpers/ee/groups/group_members_helper_spec.rb b/ee/spec/helpers/ee/groups/group_members_helper_spec.rb
index 15407734c554d7c808dea31ec18e44324e7c334e..4df54392d2dd70bb2b5eb4d7d1c8d82ea7375128 100644
--- a/ee/spec/helpers/ee/groups/group_members_helper_spec.rb
+++ b/ee/spec/helpers/ee/groups/group_members_helper_spec.rb
@@ -2,7 +2,7 @@
require "spec_helper"
-RSpec.describe Groups::GroupMembersHelper do
+RSpec.describe Groups::GroupMembersHelper, feature_category: :groups_and_projects do
include MembersPresentation
using RSpec::Parameterized::TableSyntax
@@ -206,18 +206,39 @@
describe '#group_member_header_subtext' do
let(:base_subtext) { "You're viewing members of #{group.name}." }
+ let(:cannot_invite_subtext_for_com) { "You cannot invite a new member to #{group.name} since its disabled by group owner." }
+ let(:cannot_invite_subtext_for_self_managed) { "You cannot invite a new member to #{group.name} since its disabled by administrator." }
+
let(:standard_subtext) { "^#{base_subtext}$" }
let(:enforcement_subtext) { "^#{base_subtext}
To manage seats for all members" }
- where(:can_admin_member, :enforce_free_user_cap, :subtext) do
- true | true | ref(:enforcement_subtext)
- true | false | ref(:standard_subtext)
- false | true | ref(:standard_subtext)
- false | false | ref(:standard_subtext)
+ where(:com, :can_invite_member, :can_admin_member, :enforce_free_user_cap, :subtext) do
+ true | true | true | true | ref(:enforcement_subtext)
+ true | false | true | true | ref(:cannot_invite_subtext_for_com)
+ false | false | true | true | ref(:cannot_invite_subtext_for_self_managed)
+
+ true | true | true | false | ref(:standard_subtext)
+ true | false | true | false | ref(:cannot_invite_subtext_for_com)
+ false | false | true | false | ref(:cannot_invite_subtext_for_self_managed)
+
+ true | true | false | true | ref(:standard_subtext)
+ true | false | false | true | ref(:cannot_invite_subtext_for_com)
+ false | false | false | true | ref(:cannot_invite_subtext_for_self_managed)
+
+ true | true | false | false | ref(:standard_subtext)
+ true | false | false | false | ref(:cannot_invite_subtext_for_com)
+ false | false | false | false | ref(:cannot_invite_subtext_for_self_managed)
end
before do
- allow(helper).to receive(:can?).with(current_user, :admin_group_member, group).and_return(can_admin_member)
+ allow(helper).to receive(:can?).with(current_user, :invite_group_members, group)
+ .and_return(can_invite_member)
+ allow(Gitlab::Saas).to receive(:feature_available?).with(:group_disable_invite_members).and_return(com)
+
+ if can_invite_member
+ allow(helper).to receive(:can?).with(current_user, :admin_group_member, group).and_return(can_admin_member)
+ end
+
allow_next_instance_of(::Namespaces::FreeUserCap::Enforcement, group) do |instance|
allow(instance).to receive(:enforce_cap?).and_return(enforce_free_user_cap)
end
diff --git a/ee/spec/helpers/projects/project_members_helper_spec.rb b/ee/spec/helpers/projects/project_members_helper_spec.rb
index 260f2f8180802e7dde75b6d421beb6d24195d668..2f9b294744229bb3c0576b1b1c7ec6ac03237895 100644
--- a/ee/spec/helpers/projects/project_members_helper_spec.rb
+++ b/ee/spec/helpers/projects/project_members_helper_spec.rb
@@ -165,26 +165,51 @@ def initialize(user)
end
end
- describe '#project_member_header_subtext' do
+ describe '#project_member_header_subtext', feature_category: :groups_and_projects do
let(:base_subtext) { "You can invite a new member to #{current_project.name} or invite another group." }
+ let(:cannot_invite_subtext_for_com) { "You cannot invite a new member to #{current_project.name}. User invitations are disabled by the group owner." }
+ let(:cannot_invite_subtext_for_self_managed) { "You cannot invite a new member to #{current_project.name}. User invitations are disabled by the instance administrator." }
let(:standard_subtext) { "^#{base_subtext}$" }
let(:enforcement_subtext) { "^#{base_subtext}
To manage seats for all members" }
let_it_be(:project_with_group) { create(:project, group: create(:group)) }
- where(:can_admin_member, :enforce_free_user_cap, :subtext, :current_project) do
- true | true | ref(:standard_subtext) | ref(:project)
- true | true | ref(:enforcement_subtext) | ref(:project_with_group)
- true | false | ref(:standard_subtext) | ref(:project_with_group)
- false | true | ref(:standard_subtext) | ref(:project_with_group)
- false | false | ref(:standard_subtext) | ref(:project_with_group)
+ where(:com, :can_invite_member, :can_admin_member, :enforce_free_user_cap, :subtext, :current_project) do
+ true | true | true | true | ref(:standard_subtext) | ref(:project)
+ false | false | true | true | ref(:cannot_invite_subtext_for_self_managed) | ref(:project)
+ true | false | true | true | ref(:cannot_invite_subtext_for_com) | ref(:project)
+
+ true | true | true | true | ref(:enforcement_subtext) | ref(:project_with_group)
+ false | false | true | true | ref(:cannot_invite_subtext_for_self_managed) | ref(:project_with_group)
+ true | false | true | true | ref(:cannot_invite_subtext_for_com) | ref(:project_with_group)
+
+ true | true | true | false | ref(:standard_subtext) | ref(:project_with_group)
+ false | false | true | false | ref(:cannot_invite_subtext_for_self_managed) | ref(:project_with_group)
+ true | false | true | false | ref(:cannot_invite_subtext_for_com) | ref(:project_with_group)
+
+ true | true | false | true | ref(:standard_subtext) | ref(:project_with_group)
+ false | false | false | true | ref(:cannot_invite_subtext_for_self_managed) | ref(:project_with_group)
+ true | false | false | true | ref(:cannot_invite_subtext_for_com) | ref(:project_with_group)
+
+ true | true | false | false | ref(:standard_subtext) | ref(:project_with_group)
+ false | false | false | false | ref(:cannot_invite_subtext_for_self_managed) | ref(:project_with_group)
+ true | false | false | false | ref(:cannot_invite_subtext_for_com) | ref(:project_with_group)
end
before do
assign(:project, current_project)
- allow(helper).to receive(:can?).with(current_user, :admin_project_member, current_project).and_return(true)
- allow(helper).to receive(:can?).with(current_user, :admin_group_member, current_project.root_ancestor)
- .and_return(can_admin_member)
+
+ allow(helper).to receive(:can?).with(current_user, :invite_project_members, current_project)
+ .and_return(can_invite_member)
+ allow(::Gitlab::Saas).to receive(:feature_available?).with(:group_disable_invite_members).and_return(com)
+
+ if can_invite_member
+ allow(helper).to receive(:can?).with(current_user, :admin_group_member, current_project.root_ancestor)
+ .and_return(can_admin_member)
+ allow(helper).to receive(:can?).with(current_user, :admin_project_member, current_project)
+ .and_return(can_invite_member)
+ end
+
allow_next_instance_of(::Namespaces::FreeUserCap::Enforcement, current_project.root_ancestor) do |instance|
allow(instance).to receive(:enforce_cap?).and_return(enforce_free_user_cap)
end
diff --git a/ee/spec/policies/group_policy_spec.rb b/ee/spec/policies/group_policy_spec.rb
index 0fe3eee42112b4e7259a354c84d4c80f5b496438..7199dcdfc89dafbf9796b58ced5c2763862a7d91 100644
--- a/ee/spec/policies/group_policy_spec.rb
+++ b/ee/spec/policies/group_policy_spec.rb
@@ -240,63 +240,140 @@ def stub_group_saml_config(enabled)
end
describe 'invite_group_members policy' do
- let(:app_setting) { :disable_invite_members }
- let(:policy) { :invite_group_members }
-
- context 'with disable_invite_members available in license' do
- where(:role, :setting, :admin_mode, :allowed) do
- :guest | true | nil | false
- :planner | true | nil | false
- :reporter | true | nil | false
- :developer | true | nil | false
- :maintainer | false | nil | false
- :maintainer | true | nil | false
- :owner | false | nil | true
- :owner | true | nil | false
- :admin | false | false | false
- :admin | false | true | true
- :admin | true | false | false
- end
+ context 'when on saas', :saas do
+ let(:policy) { :invite_group_members }
+ let(:app_setting) { :disable_invite_members }
- with_them do
- let(:current_user) { public_send(role) }
+ before do
+ stub_saas_features(group_disable_invite_members: true)
+ end
+
+ context 'with disable_invite_members is available in license' do
+ where(:role, :group_setting, :application_setting, :allowed) do
+ :guest | true | true | false
+ :planner | true | true | false
+ :reporter | true | true | false
+ :developer | true | true | false
+ :maintainer | false | true | false
+ :maintainer | true | true | false
+ :owner | false | true | true
+ :owner | false | false | true
+ :owner | true | true | false
+ :owner | true | false | false
+ :admin | false | true | true
+ :admin | false | false | true
+ :admin | false | true | true
+ :admin | false | false | true
+ end
- before do
- stub_licensed_features(disable_invite_members: true)
- stub_application_setting(app_setting => setting)
- enable_admin_mode!(current_user) if admin_mode
+ with_them do
+ let(:current_user) { public_send(role) }
+
+ before do
+ stub_licensed_features(disable_invite_members: true)
+ stub_application_setting(app_setting => application_setting)
+ allow(group).to receive(:disable_invite_members?).and_return(group_setting)
+ enable_admin_mode!(current_user) if role == :admin
+ end
+
+ it { is_expected.to(allowed ? be_allowed(policy) : be_disallowed(policy)) }
end
+ end
- it { is_expected.to(allowed ? be_allowed(policy) : be_disallowed(policy)) }
+ context 'with disable_invite_members not available in license' do
+ where(:role, :group_setting, :application_setting, :allowed) do
+ :guest | true | true | false
+ :planner | true | true | false
+ :reporter | true | true | false
+ :developer | true | true | false
+ :maintainer | false | true | false
+ :maintainer | true | true | false
+ :owner | false | true | true
+ :owner | false | false | true
+ :owner | true | false | true
+ :owner | true | true | true
+ :admin | false | true | true
+ :admin | true | false | true
+ end
+
+ with_them do
+ let(:current_user) { public_send(role) }
+
+ before do
+ stub_licensed_features(disable_invite_members: false)
+ stub_application_setting(app_setting => application_setting)
+ allow(group).to receive(:disable_invite_members?).and_return(group_setting)
+ enable_admin_mode!(current_user) if role == :admin
+ end
+
+ it { is_expected.to(allowed ? be_allowed(policy) : be_disallowed(policy)) }
+ end
end
end
- context 'with disable_invite_members not available in license' do
- where(:role, :setting, :admin_mode, :allowed) do
- :guest | true | nil | false
- :planner | true | nil | false
- :reporter | true | nil | false
- :developer | true | nil | false
- :maintainer | false | nil | false
- :maintainer | true | nil | false
- :owner | false | nil | true
- :owner | true | nil | true
- :admin | false | false | false
- :admin | false | true | true
- :admin | true | false | false
- :admin | true | true | true
+ context 'when self-managed' do
+ before do
+ allow(Gitlab).to receive(:com?).and_return(false)
end
- with_them do
- let(:current_user) { public_send(role) }
+ let(:app_setting) { :disable_invite_members }
+ let(:policy) { :invite_group_members }
- before do
- stub_licensed_features(disable_invite_members: false)
- stub_application_setting(app_setting => setting)
- enable_admin_mode!(current_user) if admin_mode
+ context 'with disable_invite_members available in license' do
+ where(:role, :setting, :admin_mode, :allowed) do
+ :guest | true | nil | false
+ :planner | true | nil | false
+ :reporter | true | nil | false
+ :developer | true | nil | false
+ :maintainer | false | nil | false
+ :maintainer | true | nil | false
+ :owner | false | nil | true
+ :owner | true | nil | false
+ :admin | false | false | false
+ :admin | false | true | true
+ :admin | true | false | false
end
- it { is_expected.to(allowed ? be_allowed(policy) : be_disallowed(policy)) }
+ with_them do
+ let(:current_user) { public_send(role) }
+
+ before do
+ stub_licensed_features(disable_invite_members: true)
+ stub_application_setting(app_setting => setting)
+ enable_admin_mode!(current_user) if admin_mode
+ end
+
+ it { is_expected.to(allowed ? be_allowed(policy) : be_disallowed(policy)) }
+ end
+ end
+
+ context 'with disable_invite_members not available in license' do
+ where(:role, :setting, :admin_mode, :allowed) do
+ :guest | true | nil | false
+ :planner | true | nil | false
+ :reporter | true | nil | false
+ :developer | true | nil | false
+ :maintainer | false | nil | false
+ :maintainer | true | nil | false
+ :owner | false | nil | true
+ :owner | true | nil | true
+ :admin | false | false | false
+ :admin | false | true | true
+ :admin | true | false | false
+ :admin | true | true | true
+ end
+
+ with_them do
+ let(:current_user) { public_send(role) }
+
+ before do
+ stub_licensed_features(disable_invite_members: false)
+ stub_application_setting(app_setting => setting)
+ enable_admin_mode!(current_user) if admin_mode
+ end
+
+ it { is_expected.to(allowed ? be_allowed(policy) : be_disallowed(policy)) }
+ end
end
end
end
diff --git a/ee/spec/policies/project_policy_spec.rb b/ee/spec/policies/project_policy_spec.rb
index b44adbad068619ae6ea35c54561da6b6e3bb89c4..ac0c2291781bff7b90308e2ece65e91297357ebf 100644
--- a/ee/spec/policies/project_policy_spec.rb
+++ b/ee/spec/policies/project_policy_spec.rb
@@ -2101,68 +2101,6 @@
end
end
- describe 'invite_group_members policy' do
- let(:app_setting) { :disable_invite_members }
- let(:policy) { :invite_project_members }
-
- context 'with disable_invite_members available in license' do
- where(:role, :setting, :admin_mode, :allowed) do
- :guest | true | nil | false
- :planner | true | nil | false
- :reporter | true | nil | false
- :developer | true | nil | false
- :maintainer | false | nil | true
- :maintainer | true | nil | false
- :owner | false | nil | true
- :owner | true | nil | false
- :admin | false | false | false
- :admin | false | true | true
- :admin | true | false | false
- end
-
- with_them do
- let(:current_user) { public_send(role) }
-
- before do
- stub_licensed_features(disable_invite_members: true)
- stub_application_setting(app_setting => setting)
- enable_admin_mode!(current_user) if admin_mode
- end
-
- it { is_expected.to(allowed ? be_allowed(policy) : be_disallowed(policy)) }
- end
- end
-
- context 'with disable_invite_members not available in license' do
- where(:role, :setting, :admin_mode, :allowed) do
- :guest | true | nil | false
- :planner | true | nil | false
- :reporter | true | nil | false
- :developer | true | nil | false
- :maintainer | false | nil | true
- :maintainer | true | nil | true
- :owner | false | nil | true
- :owner | true | nil | true
- :admin | false | false | false
- :admin | false | true | true
- :admin | true | false | false
- :admin | true | true | true
- end
-
- with_them do
- let(:current_user) { public_send(role) }
-
- before do
- stub_licensed_features(disable_invite_members: false)
- stub_application_setting(app_setting => setting)
- enable_admin_mode!(current_user) if admin_mode
- end
-
- it { is_expected.to(allowed ? be_allowed(policy) : be_disallowed(policy)) }
- end
- end
- end
-
context 'when project is read only on the namespace' do
let(:project) { public_project_in_group }
let(:current_user) { maintainer }
@@ -4945,6 +4883,143 @@ def create_member_role(member, abilities = member_role_abilities)
end
end
+ describe 'invite_group_members policy' do
+ let(:app_setting) { :disable_invite_members }
+ let(:policy) { :invite_project_members }
+ let(:group) { create(:group) }
+
+ context 'when on saas', :saas do
+ before do
+ allow(project).to receive(:group).and_return(group)
+
+ stub_saas_features(group_disable_invite_members: true)
+ end
+
+ context 'with disable_invite_members is available in license' do
+ where(:role, :parent_group_setting, :application_setting, :allowed) do
+ :guest | true | true | false
+ :planner | true | true | false
+ :reporter | true | true | false
+ :developer | true | true | false
+ :maintainer | false | true | true
+ :maintainer | false | false | true
+ :maintainer | true | true | false
+ :maintainer | true | false | false
+ :owner | false | true | true
+ :owner | false | false | true
+ :owner | true | true | false
+ :owner | true | false | false
+ :admin | false | true | true
+ :admin | false | false | true
+ :admin | false | true | true
+ :admin | false | false | true
+ end
+
+ with_them do
+ let(:current_user) { public_send(role) }
+
+ before do
+ stub_licensed_features(disable_invite_members: true)
+ stub_application_setting(app_setting => application_setting)
+ allow(project.group).to receive(:disable_invite_members?).and_return(parent_group_setting)
+ enable_admin_mode!(current_user) if role == :admin
+ end
+
+ it { is_expected.to(allowed ? be_allowed(policy) : be_disallowed(policy)) }
+ end
+ end
+
+ context 'with disable_invite_members not available in license' do
+ where(:role, :parent_group_setting, :application_setting, :allowed) do
+ :guest | true | true | false
+ :planner | true | true | false
+ :reporter | true | true | false
+ :developer | true | true | false
+ :maintainer | false | true | true
+ :maintainer | false | false | true
+ :maintainer | true | true | true
+ :maintainer | true | false | true
+ :owner | false | true | true
+ :owner | false | false | true
+ :owner | true | false | true
+ :owner | true | true | true
+ :admin | false | true | true
+ :admin | true | false | true
+ end
+
+ with_them do
+ let(:current_user) { public_send(role) }
+
+ before do
+ stub_licensed_features(disable_invite_members: false)
+ stub_application_setting(app_setting => application_setting)
+ allow(project.group).to receive(:disable_invite_members?).and_return(parent_group_setting)
+ enable_admin_mode!(current_user) if role == :admin
+ end
+
+ it { is_expected.to(allowed ? be_allowed(policy) : be_disallowed(policy)) }
+ end
+ end
+ end
+
+ context 'with disable_invite_members available in license' do
+ where(:role, :setting, :admin_mode, :allowed) do
+ :guest | true | nil | false
+ :planner | true | nil | false
+ :reporter | true | nil | false
+ :developer | true | nil | false
+ :maintainer | false | nil | true
+ :maintainer | true | nil | false
+ :owner | false | nil | true
+ :owner | true | nil | false
+ :admin | false | false | false
+ :admin | false | true | true
+ :admin | true | false | false
+ end
+
+ with_them do
+ let(:current_user) { public_send(role) }
+
+ before do
+ stub_licensed_features(disable_invite_members: true)
+ stub_application_setting(app_setting => setting)
+ enable_admin_mode!(current_user) if admin_mode
+ end
+
+ it { is_expected.to(allowed ? be_allowed(policy) : be_disallowed(policy)) }
+ end
+ end
+
+ context 'with disable_invite_members not available in license' do
+ where(:role, :setting, :admin_mode, :allowed) do
+ :guest | true | nil | false
+ :planner | true | nil | false
+ :reporter | true | nil | false
+ :developer | true | nil | false
+ :maintainer | false | nil | true
+ :maintainer | true | nil | true
+ :owner | false | nil | true
+ :owner | true | nil | true
+ :admin | false | false | false
+ :admin | false | true | true
+ :admin | true | false | false
+ :admin | true | true | true
+ end
+
+ with_them do
+ let(:current_user) { public_send(role) }
+
+ before do
+ stub_licensed_features(disable_invite_members: false)
+ stub_application_setting(app_setting => setting)
+ enable_admin_mode!(current_user) if admin_mode
+ end
+
+ it { is_expected.to(allowed ? be_allowed(policy) : be_disallowed(policy)) }
+ end
+ end
+ end
+
describe 'pages_multiple_versions_available' do
let(:current_user) { maintainer }
diff --git a/ee/spec/requests/api/invitations_spec.rb b/ee/spec/requests/api/invitations_spec.rb
index 8137cb2901aafe6acb23d08e1b35f6864e8eecb2..cc6d3f01c15074ac5774693f8bb2296910a63a1d 100644
--- a/ee/spec/requests/api/invitations_spec.rb
+++ b/ee/spec/requests/api/invitations_spec.rb
@@ -469,17 +469,15 @@
context 'when licensed feature for disable_invite_members is available' do
let(:email) { 'example1@example.com' }
let(:maintainer) { create(:user) }
+ let(:owner) { create(:user) }
before do
project.add_maintainer(maintainer)
+ project.add_maintainer(owner)
stub_licensed_features(disable_invite_members: true)
end
- context 'when setting to disable_group_invite_member is ON' do
- before do
- stub_application_setting(disable_invite_members: true)
- end
-
+ shared_examples "user is not allowed to invite members" do
context 'when user is maintainer/owner' do
it 'returns 403' do
post api(url, maintainer),
@@ -510,22 +508,85 @@
end
end
- context 'when setting to disable_group_invite_member is OFF' do
- let_it_be(:owner) { create(:user) }
-
- before do
- project.add_owner(owner)
- stub_application_setting(disable_invite_members: false)
+ shared_examples "user is allowed to invite members" do
+ it 'adds a new member by email for owner role' do
+ expect do
+ post api(url, owner),
+ params: { email: email, access_level: Member::MAINTAINER }
+ expect(response).to have_gitlab_http_status(:created)
+ end.to change { project.members.invite.count }.by(1)
end
- it 'adds a new member by email for owner/maintainer role' do
+ it 'adds a new member by email for maintainer role' do
expect do
- post api(url, owner),
+ post api(url, maintainer),
params: { email: email, access_level: Member::MAINTAINER }
expect(response).to have_gitlab_http_status(:created)
end.to change { project.members.invite.count }.by(1)
end
end
+
+ context 'when .com', :saas do
+ let_it_be(:group, refind: true) { create(:group_with_plan, plan: :premium_plan) }
+ let_it_be(:project, refind: true) { create(:project, namespace: group) }
+
+ context 'when saas feature is available' do
+ before do
+ stub_saas_features(group_disable_invite_members: true)
+ end
+
+ context 'when setting to disable_invite_member is ON' do
+ before do
+ group.update!(disable_invite_members: true)
+ end
+
+ it_behaves_like "user is not allowed to invite members"
+
+ context 'when disable_invite_members application setting is OFF' do
+ before do
+ stub_application_setting(disable_invite_members: false)
+ end
+
+ it_behaves_like "user is not allowed to invite members"
+ end
+ end
+
+ context 'when setting to disable_invite_member is OFF' do
+ before do
+ group.update!(disable_invite_members: false)
+ end
+
+ it_behaves_like "user is allowed to invite members"
+ end
+ end
+
+ context 'when saas feature is not available' do
+ before do
+ stub_saas_features(group_disable_invite_members: false)
+ group.update!(disable_invite_members: true)
+ end
+
+ it_behaves_like "user is allowed to invite members"
+ end
+ end
+
+ context 'when self-managed' do
+ context 'when setting to disable_invite_member is ON' do
+ before do
+ stub_application_setting(disable_invite_members: true)
+ end
+
+ it_behaves_like "user is not allowed to invite members"
+ end
+
+ context 'when setting to disable_invite_member is OFF' do
+ before do
+ stub_application_setting(disable_invite_members: false)
+ end
+
+ it_behaves_like "user is allowed to invite members"
+ end
+ end
end
context 'when licensed feature disable_invite_members is not available' do
@@ -537,17 +598,36 @@
stub_licensed_features(disable_invite_members: false)
end
- context 'setting to disable_invite_members is ON' do
- before do
- stub_application_setting(disable_invite_members: true)
+ context 'when .com', :saas do
+ context 'setting to disable_invite_members is ON' do
+ before do
+ stub_saas_features(group_disable_invite_members: true)
+ project.group.update!(disable_invite_members: true)
+ end
+
+ it "does not make any difference to invitation of new member" do
+ expect do
+ post api(url, maintainer),
+ params: { email: email, access_level: Member::MAINTAINER }
+ expect(response).to have_gitlab_http_status(:created)
+ end.to change { project.members.invite.count }.by(1)
+ end
end
+ end
- it "does not make any difference to invitation of new member" do
- expect do
- post api(url, maintainer),
- params: { email: email, access_level: Member::MAINTAINER }
- expect(response).to have_gitlab_http_status(:created)
- end.to change { project.members.invite.count }.by(1)
+ context 'when self-managed' do
+ context 'setting to disable_invite_members is ON' do
+ before do
+ stub_application_setting(disable_invite_members: true)
+ end
+
+ it "does not make any difference to invitation of new member" do
+ expect do
+ post api(url, maintainer),
+ params: { email: email, access_level: Member::MAINTAINER }
+ expect(response).to have_gitlab_http_status(:created)
+ end.to change { project.members.invite.count }.by(1)
+ end
end
end
end
diff --git a/ee/spec/views/groups/group_members/index.html.haml_spec.rb b/ee/spec/views/groups/group_members/index.html.haml_spec.rb
index 5d4ece1d86d4821ade997e969e765a0cea146b6b..8d8947b3c3c0921f64164a1c1343eb4288216c69 100644
--- a/ee/spec/views/groups/group_members/index.html.haml_spec.rb
+++ b/ee/spec/views/groups/group_members/index.html.haml_spec.rb
@@ -24,6 +24,7 @@
before do
allow(view).to receive(:can_admin_group_member?).with(group).and_return(true)
allow(view).to receive(:can?).with(user, :admin_group_member, group.root_ancestor).and_return(true)
+ allow(view).to receive(:can?).with(user, :invite_group_members, group.root_ancestor).and_return(true)
allow_next_instance_of(::Namespaces::FreeUserCap::Enforcement, group.root_ancestor) do |instance|
allow(instance).to receive(:enforce_cap?).and_return(true)
end
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index b98a755c41cfc3690717f38b5ad94f952cc4e915..ed67e96357e812df5bbad9ffcec2d815aafa3948 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -30078,6 +30078,9 @@ msgstr ""
msgid "GroupSettings|Disable personal access tokens for enterprise users"
msgstr ""
+msgid "GroupSettings|Disable user invitations to groups and projects within %{group}"
+msgstr ""
+
msgid "GroupSettings|Emails are not encrypted. Concerned administrators may want to disable diff previews."
msgstr ""
@@ -30147,6 +30150,9 @@ msgstr ""
msgid "GroupSettings|If enabled, individual user accounts will be able to use only issued SSH certificates for Git access. It doesn't apply to service accounts, deploy keys, and other types of internal accounts."
msgstr ""
+msgid "GroupSettings|If enabled, users can no longer invite members to groups or projects in the top-level group. %{learn_more_link}."
+msgstr ""
+
msgid "GroupSettings|If not specified at the group or instance level, the default is %{default_initial_branch_name}. Does not affect existing repositories."
msgstr ""
@@ -65605,6 +65611,9 @@ msgid_plural "Users in subscription"
msgstr[0] ""
msgstr[1] ""
+msgid "User invitation restrictions"
+msgstr ""
+
msgid "User is blocked"
msgstr ""
@@ -70287,6 +70296,9 @@ msgstr ""
msgid "You cannot impersonate an internal user"
msgstr ""
+msgid "You cannot invite a new member to %{strong_start}%{group_name}%{strong_end} since its disabled by %{actor}."
+msgstr ""
+
msgid "You cannot play this scheduled pipeline at the moment. Please wait a minute."
msgstr ""
diff --git a/spec/helpers/groups/group_members_helper_spec.rb b/spec/helpers/groups/group_members_helper_spec.rb
index ff5e188a348dd0f184b01b04ecb656a775ba189d..025ffb4ff30de2fa6290dbdeaf53433ae28c5f93 100644
--- a/spec/helpers/groups/group_members_helper_spec.rb
+++ b/spec/helpers/groups/group_members_helper_spec.rb
@@ -211,6 +211,13 @@
end
describe '#group_member_header_subtext' do
+ let(:current_user) { create(:user) }
+
+ before do
+ allow(helper).to receive(:current_user).and_return(current_user)
+ allow(helper).to receive(:can?).with(current_user, :invite_group_members, group).and_return(true)
+ end
+
it 'contains expected text with group name' do
expect(helper.group_member_header_subtext(group)).to match("You're viewing members of .*#{group.name}")
end
diff --git a/spec/helpers/projects/project_members_helper_spec.rb b/spec/helpers/projects/project_members_helper_spec.rb
index 6ade90c3273a6025ff674252cd5dadcc4acc25de..0f1f7f869b4ba9668598c09b60de4fd3957923d5 100644
--- a/spec/helpers/projects/project_members_helper_spec.rb
+++ b/spec/helpers/projects/project_members_helper_spec.rb
@@ -167,6 +167,7 @@
describe '#project_member_header_subtext' do
before do
+ allow(helper).to receive(:can?).with(current_user, :invite_project_members, project).and_return(true)
allow(helper).to receive(:can?).with(current_user, :admin_project_member, project).and_return(can_admin_member)
end