From 5d9a1ef1e9e39c2e1f2a2fb2368663e3f04d3ec6 Mon Sep 17 00:00:00 2001 From: Fred Reinink Date: Tue, 2 Dec 2025 17:40:45 -0600 Subject: [PATCH] Prevent personal snippet creation for enterprise users when disallowed This restriction does not apply to project snippets. Since personal snippets don't really "belong" to a resource, and the existing personal snippet authorization checks authorize against a :global subject, we do the same thing here and add the new policy to GlobalPolicy. EE: true Changelog: added --- ee/app/models/ee/group.rb | 16 +++-- ee/app/policies/ee/global_policy.rb | 4 ++ ee/spec/models/ee/group_spec.rb | 63 +++++++++++++++++++ ee/spec/policies/global_policy_spec.rb | 25 ++++++++ .../graphql/mutations/snippets/create_spec.rb | 42 ++++++++++++- spec/requests/api/project_snippets_spec.rb | 18 ++++++ spec/requests/api/snippets_spec.rb | 25 ++++++++ 7 files changed, 188 insertions(+), 5 deletions(-) diff --git a/ee/app/models/ee/group.rb b/ee/app/models/ee/group.rb index ee1ccc80cf84d8..68609d2d9ba920 100644 --- a/ee/app/models/ee/group.rb +++ b/ee/app/models/ee/group.rb @@ -1062,6 +1062,12 @@ def disable_personal_access_tokens_available? licensed_feature_available?(:disable_personal_access_tokens) end + # Disable personal access tokens for enterprise users of this group + def disable_personal_access_tokens? + disable_personal_access_tokens_available? && + namespace_settings.disable_personal_access_tokens? + end + def allow_personal_snippets_available?(user = nil) root? && enterprise_user_settings_available?(user) && @@ -1069,10 +1075,12 @@ def allow_personal_snippets_available?(user = nil) licensed_feature_available?(:allow_personal_snippets) end - # Disable personal access tokens for enterprise users of this group - def disable_personal_access_tokens? - disable_personal_access_tokens_available? && - namespace_settings.disable_personal_access_tokens? + def disallow_personal_snippets? + root? && + ::Feature.enabled?(:allow_personal_snippets_setting, self) && + ::Gitlab::Saas.feature_available?(:allow_personal_snippets) && + licensed_feature_available?(:allow_personal_snippets) && + !namespace_settings.allow_personal_snippets? end def disable_ssh_keys_available? diff --git a/ee/app/policies/ee/global_policy.rb b/ee/app/policies/ee/global_policy.rb index 6bdc44620fc3f7..c29de9d5dde1b3 100644 --- a/ee/app/policies/ee/global_policy.rb +++ b/ee/app/policies/ee/global_policy.rb @@ -163,6 +163,8 @@ module GlobalPolicy License.feature_available?(:data_management) && ::Feature.enabled?(:geo_primary_verification_view, @user) end + condition(:enterprise_user_disallowed_personal_snippets) { @user&.enterprise_group&.disallow_personal_snippets? } + rule { ~anonymous & remote_development_feature_licensed }.policy do enable :access_workspaces_feature end @@ -315,6 +317,8 @@ module GlobalPolicy end rule { designated_account_beneficiaries_available }.enable :create_designated_account_beneficiaries + + rule { enterprise_user_disallowed_personal_snippets }.prevent :create_snippet end # Check whether a user is allowed to use Duo Chat powered by self-hosted models diff --git a/ee/spec/models/ee/group_spec.rb b/ee/spec/models/ee/group_spec.rb index bc62020bc07ab1..3eca5dbf23f6b3 100644 --- a/ee/spec/models/ee/group_spec.rb +++ b/ee/spec/models/ee/group_spec.rb @@ -3218,6 +3218,69 @@ def webhook_headers end end + describe '#disallow_personal_snippets?' do + before do + group.namespace_settings.update!(allow_personal_snippets: false) + end + + context 'when not licensed' do + it 'returns false' do + expect(group.disallow_personal_snippets?).to be_falsey + end + end + + context 'when licensed' do + before do + stub_licensed_features(allow_personal_snippets: true) + end + + it 'returns false' do + expect(group.disallow_personal_snippets?).to be_falsey + end + + context 'on SaaS', :saas do + before do + stub_saas_features(allow_personal_snippets: true) + stub_feature_flags(allow_personal_snippets_setting: true) + end + + it 'returns true' do + expect(group.disallow_personal_snippets?).to be_truthy + end + + context 'for a subgroup' do + let(:subgroup) { create(:group, parent: group) } + + it 'returns false' do + subgroup.namespace_settings.update!(allow_personal_snippets: false) + + expect(subgroup.disallow_personal_snippets?).to be_falsey + end + end + + context 'with the feature flag disabled' do + before do + stub_feature_flags(allow_personal_snippets_setting: false) + end + + it 'returns false' do + expect(group.disallow_personal_snippets?).to be_falsey + end + end + + context 'when allow_personal_snippets is true' do + before do + group.namespace_settings.update!(allow_personal_snippets: true) + end + + it 'returns false' do + expect(group.disallow_personal_snippets?).to be_falsey + end + end + end + end + end + describe '#hide_email_on_profile?' do it 'returns false by default' do expect(group.hide_email_on_profile?).to be_falsey diff --git a/ee/spec/policies/global_policy_spec.rb b/ee/spec/policies/global_policy_spec.rb index 3c4639307f484a..b8570592d0232e 100644 --- a/ee/spec/policies/global_policy_spec.rb +++ b/ee/spec/policies/global_policy_spec.rb @@ -1055,4 +1055,29 @@ it { is_expected.to check_policy } end end + + describe 'enterprise user disallowed personal snippets' do + where(:is_enterprise_user, :allow_personal_snippets, :check_policy) do + true | false | be_disallowed(:create_snippet) + true | true | be_allowed(:create_snippet) + false | false | be_allowed(:create_snippet) + false | true | be_allowed(:create_snippet) + end + + before do + stub_licensed_features(allow_personal_snippets: true) + stub_saas_features(allow_personal_snippets: true) + end + + with_them do + let(:enterprise_group) do + create(:group).tap { |group| group.update!(allow_personal_snippets: allow_personal_snippets) } + end + + let(:enterprise_user) { create(:user, enterprise_group: enterprise_group) } + let(:current_user) { is_enterprise_user ? enterprise_user : user } + + it { is_expected.to check_policy } + end + end end diff --git a/spec/requests/api/graphql/mutations/snippets/create_spec.rb b/spec/requests/api/graphql/mutations/snippets/create_spec.rb index b7a9afea934964..235cbf04cc5d80 100644 --- a/spec/requests/api/graphql/mutations/snippets/create_spec.rb +++ b/spec/requests/api/graphql/mutations/snippets/create_spec.rb @@ -118,6 +118,31 @@ def mutation_response end.to change { Snippet.where(organization_id: current_organization.id).count }.by(1) end end + + context 'with an enterprise user with allow_personal_snippets: false' do + let(:enterprise_group) { create(:group).tap { |group| group.update!(allow_personal_snippets: false) } } + let(:current_user) { create(:user, enterprise_group: enterprise_group) } + + before do + stub_licensed_features(allow_personal_snippets: true) + stub_saas_features(allow_personal_snippets: true) + end + + it_behaves_like 'a mutation that returns top-level errors', + errors: [Gitlab::Graphql::Authorize::AuthorizeResource::RESOURCE_ACCESS_ERROR] + + context 'when the allow_personal_snippets_setting feature flag is disabled' do + before do + stub_feature_flags(allow_personal_snippets_setting: false) + end + + it 'creates a snippet' do + expect do + subject + end.to change { Snippet.count }.by(1) + end + end + end end context 'with ProjectSnippet' do @@ -153,7 +178,22 @@ def mutation_response errors: [Gitlab::Graphql::Authorize::AuthorizeResource::RESOURCE_ACCESS_ERROR] end - it_behaves_like 'snippet edit usage data counters' + context 'with an enterprise user with allow_personal_snippets: false' do + let(:enterprise_group) { create(:group).tap { |group| group.update!(allow_personal_snippets: false) } } + let(:current_user) { create(:user, enterprise_group: enterprise_group) } + + before do + stub_licensed_features(allow_personal_snippets: true) + stub_saas_features(allow_personal_snippets: true) + project.add_developer(current_user) + end + + it 'creates a snippet' do + expect do + subject + end.to change { Snippet.count }.by(1) + end + end end context 'when there are ActiveRecord validation errors' do diff --git a/spec/requests/api/project_snippets_spec.rb b/spec/requests/api/project_snippets_spec.rb index 91d7bc1e403e34..542642df162ef5 100644 --- a/spec/requests/api/project_snippets_spec.rb +++ b/spec/requests/api/project_snippets_spec.rb @@ -239,6 +239,24 @@ end end + context 'with an enterprise user with allow_personal_snippets: false' do + let(:enterprise_group) { create(:group).tap { |group| group.update!(allow_personal_snippets: false) } } + let(:actor) { create(:user, enterprise_group: enterprise_group) } + + before do + stub_licensed_features(allow_personal_snippets: true) + stub_saas_features(allow_personal_snippets: true) + + project.add_developer(actor) + end + + it 'project snippets are unaffected' do + request + + expect(response).to have_gitlab_http_status(:created) + end + end + context 'when save fails because the repository could not be created' do let(:actor) { admin } let(:admin_mode) { true } diff --git a/spec/requests/api/snippets_spec.rb b/spec/requests/api/snippets_spec.rb index ecd5310e361d16..cc8f93509c67d5 100644 --- a/spec/requests/api/snippets_spec.rb +++ b/spec/requests/api/snippets_spec.rb @@ -364,6 +364,31 @@ end end + context 'with an enterprise user with allow_personal_snippets: false' do + let(:enterprise_group) { create(:group).tap { |group| group.update!(allow_personal_snippets: false) } } + let(:user) { create(:user, enterprise_group: enterprise_group) } + let(:user_token) { create(:personal_access_token, user: user) } + + before do + stub_licensed_features(allow_personal_snippets: true) + stub_saas_features(allow_personal_snippets: true) + end + + it 'does not create a new snippet' do + subject + + expect(response).to have_gitlab_http_status(:forbidden) + end + + context 'when the allow_personal_snippets_setting feature flag is disabled' do + before do + stub_feature_flags(allow_personal_snippets_setting: false) + end + + it_behaves_like 'snippet creation' + end + end + it 'returns 400 for missing parameters' do params.delete(:title) -- GitLab