diff --git a/db/migrate/20250430112641_add_disable_invite_members_setting.rb b/db/migrate/20250430112641_add_disable_invite_members_setting.rb new file mode 100644 index 0000000000000000000000000000000000000000..c9815e3824a8110a7080632be4d0edc0f18f7860 --- /dev/null +++ b/db/migrate/20250430112641_add_disable_invite_members_setting.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +class AddDisableInviteMembersSetting < Gitlab::Database::Migration[2.3] + milestone '18.0' + + def change + add_column :namespace_settings, :disable_invite_members, :boolean, null: false, default: false + end +end diff --git a/db/schema_migrations/20250430112641 b/db/schema_migrations/20250430112641 new file mode 100644 index 0000000000000000000000000000000000000000..adae7c850ae5107530337693eae39ab3e91beae2 --- /dev/null +++ b/db/schema_migrations/20250430112641 @@ -0,0 +1 @@ +ed77819b8477efd235ab2c1eb8868922fee96fce0ab91aee94eae9fffb69af9f \ No newline at end of file diff --git a/db/structure.sql b/db/structure.sql index 3a7881ae6d83907137616b173a936f474bbf1a25..8acb0c9d1985e6c67318119dae52c7ac5eeaafcd 100644 --- a/db/structure.sql +++ b/db/structure.sql @@ -17917,6 +17917,7 @@ CREATE TABLE namespace_settings ( job_token_policies_enabled boolean DEFAULT false NOT NULL, security_policies jsonb DEFAULT '{}'::jsonb NOT NULL, duo_nano_features_enabled boolean, + disable_invite_members boolean DEFAULT false NOT NULL, CONSTRAINT check_0ba93c78c7 CHECK ((char_length(default_branch_name) <= 255)), CONSTRAINT check_namespace_settings_security_policies_is_hash CHECK ((jsonb_typeof(security_policies) = 'object'::text)), CONSTRAINT namespace_settings_unique_project_download_limit_alertlist_size CHECK ((cardinality(unique_project_download_limit_alertlist) <= 100)), diff --git a/ee/app/controllers/concerns/ee/groups/params.rb b/ee/app/controllers/concerns/ee/groups/params.rb index 7cfbb98b02ec86e88f5f0d3d2473b7bbd138033a..e7a6be9a7d405c5c880a0240c7865bff99d57720 100644 --- a/ee/app/controllers/concerns/ee/groups/params.rb +++ b/ee/app/controllers/concerns/ee/groups/params.rb @@ -89,6 +89,11 @@ def group_params_ee can?(current_user, :admin_group, current_group) params_ee << :require_dpop_for_manage_api_endpoints end + + if current_group&.root? && + can?(current_user, :owner_access, current_group) + params_ee << :disable_invite_members + end end end # rubocop:enable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity, Metrics/AbcSize diff --git a/ee/app/models/ee/group.rb b/ee/app/models/ee/group.rb index c975d82fec231ac3acdc75a283b667baf2760887..0d56630537e6f4bca3b581baebbfd235623c283c 100644 --- a/ee/app/models/ee/group.rb +++ b/ee/app/models/ee/group.rb @@ -119,6 +119,9 @@ module Group delegate :extended_grat_expiry_webhooks_execute, :extended_grat_expiry_webhooks_execute=, to: :namespace_settings + delegate :disable_invite_members, :disable_invite_members=, to: :namespace_settings + delegate :disable_invite_members?, to: :namespace_settings + # Use +checked_file_template_project+ instead, which implements important # visibility checks private :file_template_project diff --git a/ee/app/services/ee/namespace_settings/assign_attributes_service.rb b/ee/app/services/ee/namespace_settings/assign_attributes_service.rb index fe9c8bf8552d676bb495c1cfa4a71cddd384102a..5c4420753c0d0bb272451722e7565f6cdd482229 100644 --- a/ee/app/services/ee/namespace_settings/assign_attributes_service.rb +++ b/ee/app/services/ee/namespace_settings/assign_attributes_service.rb @@ -31,7 +31,10 @@ def execute param_key: :lock_duo_features_enabled, user_policy: :admin_group ) - + validate_settings_param_for_root_group( + param_key: :disable_invite_members, + user_policy: :owner_access + ) super end diff --git a/ee/spec/lib/namespaces/namespace_setting_changes_auditor_spec.rb b/ee/spec/lib/namespaces/namespace_setting_changes_auditor_spec.rb index 73d6fb655a8ecf1a8382778dca47d9afbb7a7639..118526a41c3eb3e6de67e8a29f610446b64d5436 100644 --- a/ee/spec/lib/namespaces/namespace_setting_changes_auditor_spec.rb +++ b/ee/spec/lib/namespaces/namespace_setting_changes_auditor_spec.rb @@ -125,7 +125,7 @@ resource_access_token_notify_inherited lock_resource_access_token_notify_inherited pipeline_variables_default_role extended_grat_expiry_webhooks_execute force_pages_access_control jwt_ci_cd_job_token_enabled jwt_ci_cd_job_token_opted_out require_dpop_for_manage_api_endpoints - job_token_policies_enabled security_policies duo_nano_features_enabled] + disable_invite_members job_token_policies_enabled security_policies duo_nano_features_enabled] columns_to_audit = Namespaces::NamespaceSettingChangesAuditor::EVENT_NAME_PER_COLUMN.keys.map(&:to_s) diff --git a/ee/spec/models/ee/group_spec.rb b/ee/spec/models/ee/group_spec.rb index 203280cb9a38aaee2d49caa142770a1ed62e7a58..5fb1994ced378ff8cbc6b84c23158b84a5279bd9 100644 --- a/ee/spec/models/ee/group_spec.rb +++ b/ee/spec/models/ee/group_spec.rb @@ -313,6 +313,8 @@ it { is_expected.to delegate_method(:require_dpop_for_manage_api_endpoints?).to(:namespace_settings) } it { is_expected.to delegate_method(:require_dpop_for_manage_api_endpoints).to(:namespace_settings) } it { is_expected.to delegate_method(:require_dpop_for_manage_api_endpoints=).to(:namespace_settings).with_arguments(:args) } + it { is_expected.to delegate_method(:disable_invite_members=).to(:namespace_settings).with_arguments(:args) } + it { is_expected.to delegate_method(:disable_invite_members?).to(:namespace_settings) } it { is_expected.to delegate_method(:enterprise_users_extensions_marketplace_enabled=).to(:namespace_settings).with_arguments(:args) } end diff --git a/ee/spec/models/namespace_setting_spec.rb b/ee/spec/models/namespace_setting_spec.rb index fffe8c63cb46d49e65c93ada49ca88fb9b4ff19b..4d67ba1cf97f9644f50868834ac8f1352a107763 100644 --- a/ee/spec/models/namespace_setting_spec.rb +++ b/ee/spec/models/namespace_setting_spec.rb @@ -486,6 +486,28 @@ end end + describe "#disable_invite_members" do + context 'when disable_invite_members = true' do + before do + setting.disable_invite_members = true + end + + it 'returns true' do + expect(setting.disable_invite_members?).to eq(true) + end + end + + context 'when disable_invite_members = false' do + before do + setting.disable_invite_members = false + end + + it 'returns false' do + expect(setting.disable_invite_members?).to eq(false) + end + end + end + describe '#user_cap_enabled?', feature_category: :consumables_cost_management do where(:seat_control, :new_user_signups_cap, :root_namespace, :expectation) do :off | nil | false | false diff --git a/ee/spec/requests/groups_controller_spec.rb b/ee/spec/requests/groups_controller_spec.rb index 6dd53bc867e637b2ba32aaa0ae2813627a411a87..0c3406cc6f3d1d1371a7f86d93ab2667853815fe 100644 --- a/ee/spec/requests/groups_controller_spec.rb +++ b/ee/spec/requests/groups_controller_spec.rb @@ -363,6 +363,48 @@ end end + context 'when setting disable_invite_members' do + let(:params) { { group: { disable_invite_members: true } } } + + context 'when group is a top level group' do + context 'when user is a group owner' do + before do + group.add_owner(user) + end + + it 'successfully changes the column' do + expect { request }.to change { group.reload.disable_invite_members? } + expect(response).to have_gitlab_http_status(:found) + end + end + + context 'when user is not group owner' do + before do + group.owners.delete(user) + group.add_maintainer(user) + end + + it 'does not change the column and returns not found' do + expect { request }.not_to change { group.reload.disable_invite_members? } + expect(response).to have_gitlab_http_status(:not_found) + end + end + end + + context 'when group is not a top level group' do + let(:group) { create(:group, :nested) } + + it 'does not change the column and returns not found' do + expect(group.disable_invite_members?).to be(false) + + request + + expect(response).to have_gitlab_http_status(:found) + expect(group.reload.disable_invite_members?).to be(false) + end + end + end + context 'when setting the require_dpop_for_manage_api_endpoints param (default disabled)' do let(:params) { { group: { require_dpop_for_manage_api_endpoints: true } } } diff --git a/ee/spec/services/ee/namespace_settings/assign_attributes_service_spec.rb b/ee/spec/services/ee/namespace_settings/assign_attributes_service_spec.rb index dde2bacdd9fe5000003162fd7148e99ea4c015fd..ce4c420f6f2abfa5d961bd7c21e1dc5259e62320 100644 --- a/ee/spec/services/ee/namespace_settings/assign_attributes_service_spec.rb +++ b/ee/spec/services/ee/namespace_settings/assign_attributes_service_spec.rb @@ -9,6 +9,56 @@ subject(:update_settings) { NamespaceSettings::AssignAttributesService.new(user, group, params).execute } describe '#execute' do + context 'when disable_invite_members param present' do + let(:params) { { disable_invite_members: true } } + + context 'as a non-owner' do + it 'does not change settings' do + group.update!(disable_invite_members: true) + group.save! + + update_settings + + expect(group.disable_invite_members?).to eq(true) + end + end + + context 'as a group owner' do + before_all do + group.add_owner(user) + end + + it 'changes settings' do + update_settings + + expect(group.disable_invite_members?).to eq(true) + end + end + + context 'as a non-group owner' do + before_all do + group.add_maintainer(user) + end + + it "does not change settings" do + expect { update_settings } + .not_to(change { group.disable_invite_members? }) + end + end + + context 'when not top-level group' do + before do + group.parent = create(:group) + group.save! + end + + it 'does not change settings' do + expect { update_settings } + .not_to(change { group.disable_invite_members? }) + end + end + end + context 'when prevent_forking_outside_group param present' do let(:params) { { prevent_forking_outside_group: true } }