From 056ad384119079c93ad7a8e724d14381a00b77b8 Mon Sep 17 00:00:00 2001 From: Smriti Garg <sgarg@gitlab.com> Date: Tue, 4 Mar 2025 16:58:34 +0000 Subject: [PATCH] Added new column and controller setting New column with name dpop_check_for_manage_api_endpoints is added to namespace_settings. This setting will control whether we need to enforce dpop authentication on /manage endpoints or not Changelog: added EE: true Added new column and controller setting --- ...d_require_dpop_for_manage_api_endpoints.rb | 12 ++ db/schema_migrations/20250227044611 | 1 + db/structure.sql | 1 + .../controllers/concerns/ee/groups/params.rb | 5 + ee/app/models/ee/group.rb | 2 + .../namespace_setting_changes_auditor_spec.rb | 2 +- ee/spec/models/ee/group_spec.rb | 3 + ee/spec/models/namespace_setting_spec.rb | 22 ++++ ee/spec/requests/groups_controller_spec.rb | 110 ++++++++++++------ 9 files changed, 124 insertions(+), 34 deletions(-) create mode 100644 db/migrate/20250227044611_add_require_dpop_for_manage_api_endpoints.rb create mode 100644 db/schema_migrations/20250227044611 diff --git a/db/migrate/20250227044611_add_require_dpop_for_manage_api_endpoints.rb b/db/migrate/20250227044611_add_require_dpop_for_manage_api_endpoints.rb new file mode 100644 index 0000000000000..31a6d54334ad9 --- /dev/null +++ b/db/migrate/20250227044611_add_require_dpop_for_manage_api_endpoints.rb @@ -0,0 +1,12 @@ +# frozen_string_literal: true + +# See https://docs.gitlab.com/ee/development/migration_style_guide.html +# for more information on how to write migrations for GitLab. + +class AddRequireDpopForManageApiEndpoints < Gitlab::Database::Migration[2.2] + milestone '17.10' + + def change + add_column :namespace_settings, :require_dpop_for_manage_api_endpoints, :boolean, null: false, default: true + end +end diff --git a/db/schema_migrations/20250227044611 b/db/schema_migrations/20250227044611 new file mode 100644 index 0000000000000..bcaa3664b8ebb --- /dev/null +++ b/db/schema_migrations/20250227044611 @@ -0,0 +1 @@ +1a7161f562a83a1453dff1c15ba73cce126dca54ae217754617556ae78c48bc4 \ No newline at end of file diff --git a/db/structure.sql b/db/structure.sql index 56db66feda0a2..b237086a5a70f 100644 --- a/db/structure.sql +++ b/db/structure.sql @@ -17067,6 +17067,7 @@ CREATE TABLE namespace_settings ( extended_grat_expiry_webhooks_execute boolean DEFAULT false NOT NULL, jwt_ci_cd_job_token_enabled boolean DEFAULT false NOT NULL, jwt_ci_cd_job_token_opted_out boolean DEFAULT false NOT NULL, + require_dpop_for_manage_api_endpoints boolean DEFAULT true NOT NULL, CONSTRAINT check_0ba93c78c7 CHECK ((char_length(default_branch_name) <= 255)), CONSTRAINT namespace_settings_unique_project_download_limit_alertlist_size CHECK ((cardinality(unique_project_download_limit_alertlist) <= 100)), CONSTRAINT namespace_settings_unique_project_download_limit_allowlist_size CHECK ((cardinality(unique_project_download_limit_allowlist) <= 100)) diff --git a/ee/app/controllers/concerns/ee/groups/params.rb b/ee/app/controllers/concerns/ee/groups/params.rb index 89fcd7d7b227b..f9fecab0d2679 100644 --- a/ee/app/controllers/concerns/ee/groups/params.rb +++ b/ee/app/controllers/concerns/ee/groups/params.rb @@ -85,6 +85,11 @@ def group_params_ee current_group.licensed_feature_available?(:group_webhooks) params_ee << :extended_grat_expiry_webhooks_execute end + + if ::Feature.enabled?(:manage_pat_by_group_owners_ready, current_group) && + can?(current_user, :admin_group, current_group) + params_ee << :require_dpop_for_manage_api_endpoints + 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 da876ae5595e9..df16407b6eef0 100644 --- a/ee/app/models/ee/group.rb +++ b/ee/app/models/ee/group.rb @@ -118,6 +118,8 @@ module Group delegate :enable_auto_assign_gitlab_duo_pro_seats, :enable_auto_assign_gitlab_duo_pro_seats=, :enable_auto_assign_gitlab_duo_pro_seats_human_readable, :enable_auto_assign_gitlab_duo_pro_seats_human_readable=, to: :namespace_settings, allow_nil: true delegate :extended_grat_expiry_webhooks_execute, :extended_grat_expiry_webhooks_execute=, to: :namespace_settings + delegate :require_dpop_for_manage_api_endpoints, :require_dpop_for_manage_api_endpoints=, to: :namespace_settings + delegate :require_dpop_for_manage_api_endpoints?, to: :namespace_settings # Use +checked_file_template_project+ instead, which implements important # visibility checks 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 d4e74df24cf94..c509ddcd3d2e4 100644 --- a/ee/spec/lib/namespaces/namespace_setting_changes_auditor_spec.rb +++ b/ee/spec/lib/namespaces/namespace_setting_changes_auditor_spec.rb @@ -124,7 +124,7 @@ lock_spp_repository_pipeline_access spp_repository_pipeline_access archived 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] + jwt_ci_cd_job_token_enabled jwt_ci_cd_job_token_opted_out require_dpop_for_manage_api_endpoints] 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 f5c1c4f068bd2..940e5d672e27d 100644 --- a/ee/spec/models/ee/group_spec.rb +++ b/ee/spec/models/ee/group_spec.rb @@ -360,6 +360,9 @@ it { is_expected.to delegate_method(:duo_availability=).to(:namespace_settings).with_arguments(:args) } it { is_expected.to delegate_method(:experiment_settings_allowed?).to(:namespace_settings) } it { is_expected.to delegate_method(:user_cap_enabled?).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) } + it { is_expected.to delegate_method(:require_dpop_for_manage_api_endpoints=).to(:namespace_settings).with_arguments(:args) } 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 747f3666ef968..a84cf07b405f3 100644 --- a/ee/spec/models/namespace_setting_spec.rb +++ b/ee/spec/models/namespace_setting_spec.rb @@ -329,6 +329,28 @@ end end + describe '#require_dpop_for_manage_api_endpoints?' do + context 'when require_dpop_for_manage_api_endpoints = true' do + before do + setting.require_dpop_for_manage_api_endpoints = true + end + + it 'returns true' do + expect(setting.require_dpop_for_manage_api_endpoints?).to eq(true) + end + end + + context 'when require_dpop_for_manage_api_endpoints = false' do + before do + setting.require_dpop_for_manage_api_endpoints = false + end + + it 'returns false' do + expect(setting.require_dpop_for_manage_api_endpoints?).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 b4769698f4792..7f9b664a5faf0 100644 --- a/ee/spec/requests/groups_controller_spec.rb +++ b/ee/spec/requests/groups_controller_spec.rb @@ -12,7 +12,7 @@ login_as(user) end - subject do + subject(:request) do put(group_path(group), params: params) end @@ -31,7 +31,7 @@ context 'valid param' do shared_examples 'creates ip restrictions' do it 'creates ip restrictions' do - expect { subject } + expect { request } .to change { group.reload.ip_restrictions.map(&:range) } .from([]).to(contain_exactly(*range.split(','))) expect(response).to have_gitlab_http_status(:found) @@ -55,7 +55,7 @@ let(:range) { 'boom!' } it 'adds error message' do - expect { subject } + expect { request } .not_to(change { group.reload.ip_restrictions.count }.from(0)) expect(response).to have_gitlab_http_status(:ok) expect(response.body).to include('Ip restrictions range is an invalid IP address range') @@ -71,7 +71,7 @@ context 'valid param' do shared_examples 'updates ip restrictions' do it 'updates ip restrictions' do - expect { subject } + expect { request } .to change { group.reload.ip_restrictions.map(&:range) } .from(['10.0.0.0/8']).to(contain_exactly(*range.split(','))) expect(response).to have_gitlab_http_status(:found) @@ -102,13 +102,13 @@ context 'invalid param' do shared_examples 'does not update existing ip restrictions' do it 'does not change ip restriction records' do - expect { subject } + expect { request } .not_to(change { group.reload.ip_restrictions.map(&:range) } .from(['10.0.0.0/8'])) end it 'adds error message' do - subject + request expect(response).to have_gitlab_http_status(:ok) expect(response.body).to include('Ip restrictions range is an invalid IP address range') @@ -135,7 +135,7 @@ let(:range) { '' } it 'deletes ip restriction' do - expect { subject } + expect { request } .to(change { group.reload.ip_restrictions.count }.to(0)) expect(response).to have_gitlab_http_status(:found) end @@ -147,7 +147,7 @@ let(:group) { create(:group, :nested) } it 'does not create ip restriction' do - expect { subject } + expect { request } .not_to change { group.reload.ip_restrictions.count }.from(0) expect(response).to have_gitlab_http_status(:ok) expect(response.body).to include('Ip restrictions IP subnet restriction only allowed for top-level groups') @@ -161,13 +161,13 @@ end it 'updates group setting' do - expect { subject } + expect { request } .to change { group.reload.two_factor_grace_period }.from(48).to(42) expect(response).to have_gitlab_http_status(:found) end it 'does not create ip restriction' do - expect { subject }.not_to change { IpRestriction.count } + expect { request }.not_to change { IpRestriction.count } end end @@ -177,7 +177,7 @@ end it 'does not create ip restriction' do - expect { subject } + expect { request } .not_to change { group.reload.ip_restrictions.count }.from(0) expect(response).to have_gitlab_http_status(:found) end @@ -196,7 +196,7 @@ context 'valid param' do shared_examples 'creates email domain restrictions' do it 'creates email domain restrictions' do - subject + request expect(response).to have_gitlab_http_status(:found) expect(group.reload.allowed_email_domains.domain_names).to match_array(domains.split(",")) @@ -220,7 +220,7 @@ let(:domains) { 'boom!' } it 'adds error message' do - expect { subject } + expect { request } .not_to(change { group.reload.allowed_email_domains.count }.from(0)) expect(response).to have_gitlab_http_status(:ok) expect(response.body).to include('The domain you entered is misformatted') @@ -235,7 +235,7 @@ context 'valid param' do shared_examples 'updates allowed email domain restrictions' do it 'updates allowed email domain restrictions' do - subject + request expect(response).to have_gitlab_http_status(:found) expect(group.reload.allowed_email_domains.domain_names).to match_array(domains.split(",")) @@ -266,13 +266,13 @@ context 'invalid param' do shared_examples 'does not update existing email domain restrictions' do it 'does not change allowed_email_domains records' do - expect { subject } + expect { request } .not_to(change { group.reload.allowed_email_domains.domain_names } .from(['gitlab.com'])) end it 'adds error message' do - subject + request expect(response).to have_gitlab_http_status(:ok) expect(response.body).to include('The domain you entered is misformatted') @@ -299,7 +299,7 @@ let(:domains) { '' } it 'deletes all email domain restrictions' do - expect { subject } + expect { request } .to(change { group.reload.allowed_email_domains.count }.to(0)) expect(response).to have_gitlab_http_status(:found) end @@ -312,7 +312,7 @@ let(:domains) { 'gitlab.com' } it 'does not create email domain restriction' do - expect { subject } + expect { request } .not_to change { group.reload.allowed_email_domains.count }.from(0) expect(response).to have_gitlab_http_status(:ok) expect(response.body).to include('Allowed email domain restriction only permitted for top-level groups') @@ -327,7 +327,7 @@ end it 'does not create email domain restrictions' do - expect { subject } + expect { request } .not_to change { group.reload.allowed_email_domains.count }.from(0) expect(response).to have_gitlab_http_status(:found) end @@ -338,7 +338,7 @@ let(:params) { { group: { enforce_ssh_certificates: true } } } it 'does not change the column' do - expect { subject }.not_to change { group.reload.enforce_ssh_certificates? } + expect { request }.not_to change { group.reload.enforce_ssh_certificates? } expect(response).to have_gitlab_http_status(:found) end @@ -348,7 +348,7 @@ end it 'successfully changes the column' do - expect { subject }.to change { group.reload.enforce_ssh_certificates? } + expect { request }.to change { group.reload.enforce_ssh_certificates? } expect(response).to have_gitlab_http_status(:found) end @@ -356,13 +356,57 @@ let(:group) { create(:group, :nested) } it 'does not change the column' do - expect { subject }.not_to change { group.reload.enforce_ssh_certificates? } + expect { request }.not_to change { group.reload.enforce_ssh_certificates? } expect(response).to have_gitlab_http_status(:found) end end end end + context 'when settings require_dpop_for_manage_api_endpoints' do + let(:params) { { group: { require_dpop_for_manage_api_endpoints: false } } } + + context 'when feature flag is enabled' do + before do + stub_feature_flags(manage_pat_by_group_owners_ready: true) + end + + context 'when not group owner' do + let(:user) { create(:user) } + + before do + group.add_developer(user) + login_as(user) + end + + it 'does not change the column and returns not_found' do + expect(group.require_dpop_for_manage_api_endpoints?).to be(true) + + request + + expect(response).to have_gitlab_http_status(:not_found) + expect(group.reload.require_dpop_for_manage_api_endpoints?).to be(true) + end + end + + it 'successfully changes the column' do + expect { request }.to change { group.reload.require_dpop_for_manage_api_endpoints? } + expect(response).to have_gitlab_http_status(:found) + end + end + + context 'when feature flag is disabled' do + before do + stub_feature_flags(manage_pat_by_group_owners_ready: false) + end + + it 'does not change the column' do + expect { request }.not_to change { group.reload.require_dpop_for_manage_api_endpoints? } + expect(response).to have_gitlab_http_status(:found) + end + end + end + context 'when settings extended_grat_expiry_webhooks_execute' do let(:params) { { group: { extended_grat_expiry_webhooks_execute: true } } } @@ -372,7 +416,7 @@ end it 'does not change the column' do - expect { subject }.not_to change { group.reload.extended_grat_expiry_webhooks_execute? } + expect { request }.not_to change { group.reload.extended_grat_expiry_webhooks_execute? } expect(response).to have_gitlab_http_status(:found) end end @@ -383,7 +427,7 @@ end it 'successfully changes the column' do - expect { subject }.to change { group.reload.extended_grat_expiry_webhooks_execute? } + expect { request }.to change { group.reload.extended_grat_expiry_webhooks_execute? } expect(response).to have_gitlab_http_status(:found) end @@ -393,7 +437,7 @@ end it 'does not change the column' do - expect { subject }.not_to change { group.reload.extended_grat_expiry_webhooks_execute? } + expect { request }.not_to change { group.reload.extended_grat_expiry_webhooks_execute? } expect(response).to have_gitlab_http_status(:found) end end @@ -411,7 +455,7 @@ end it 'does not change the column' do - expect { subject }.not_to change { group.reload.enterprise_users_extensions_marketplace_enabled? } + expect { request }.not_to change { group.reload.enterprise_users_extensions_marketplace_enabled? } expect(response).to have_gitlab_http_status(:found) end @@ -421,7 +465,7 @@ end it 'successfully changes the column' do - expect { subject }.to change { group.reload.enterprise_users_extensions_marketplace_enabled? } + expect { request }.to change { group.reload.enterprise_users_extensions_marketplace_enabled? } expect(response).to have_gitlab_http_status(:found) end end @@ -439,7 +483,7 @@ it 'does not change the column' do expect(group.enable_auto_assign_gitlab_duo_pro_seats?).to be_falsy - subject + request expect(response).to have_gitlab_http_status(:found) expect(group.reload.enable_auto_assign_gitlab_duo_pro_seats?).to be_falsy @@ -460,7 +504,7 @@ it 'does not change the column' do expect(group.enable_auto_assign_gitlab_duo_pro_seats?).to be_falsy - subject + request expect(response).to have_gitlab_http_status(:found) expect(group.reload.enable_auto_assign_gitlab_duo_pro_seats?).to be_falsy @@ -475,7 +519,7 @@ it 'does not change the column' do expect(group.enable_auto_assign_gitlab_duo_pro_seats?).to be_falsy - subject + request expect(response).to have_gitlab_http_status(:found) expect(group.reload.enable_auto_assign_gitlab_duo_pro_seats?).to be_falsy @@ -493,7 +537,7 @@ it 'does not change the column and returns not_found' do expect(group.enable_auto_assign_gitlab_duo_pro_seats?).to be_falsy - subject + request expect(response).to have_gitlab_http_status(:not_found) expect(group.reload.enable_auto_assign_gitlab_duo_pro_seats?).to be_falsy @@ -508,7 +552,7 @@ it 'does not change the column' do expect(group.enable_auto_assign_gitlab_duo_pro_seats?).to be_falsy - subject + request expect(response).to have_gitlab_http_status(:found) expect(group.reload.enable_auto_assign_gitlab_duo_pro_seats?).to be_falsy @@ -524,7 +568,7 @@ it 'successfully updates the column' do expect(group.enable_auto_assign_gitlab_duo_pro_seats?).to be_falsy - subject + request expect(response).to have_gitlab_http_status(:found) expect(group.reload.enable_auto_assign_gitlab_duo_pro_seats?).to be_truthy -- GitLab