diff --git a/app/controllers/user_settings/personal_access_tokens_controller.rb b/app/controllers/user_settings/personal_access_tokens_controller.rb index a8e9a328c2606fa5677a071da4f91f77b5ce5a99..0bb71966b2141d7d6eea5984469a11f6eb5af678 100644 --- a/app/controllers/user_settings/personal_access_tokens_controller.rb +++ b/app/controllers/user_settings/personal_access_tokens_controller.rb @@ -74,3 +74,5 @@ def check_personal_access_tokens_enabled end end end + +UserSettings::PersonalAccessTokensController.prepend_mod diff --git a/app/views/groups/settings/_permissions.html.haml b/app/views/groups/settings/_permissions.html.haml index 804eb4f6554c8945fd5401003e67007bcea16a3f..f84e2ef9220ec8a9ee5aa6b7f690f4d6bbae91f0 100644 --- a/app/views/groups/settings/_permissions.html.haml +++ b/app/views/groups/settings/_permissions.html.haml @@ -50,6 +50,7 @@ = render 'groups/settings/two_factor_auth', f: f, group: @group = render_if_exists 'groups/personal_access_token_expiration_policy', f: f, group: @group = render 'groups/settings/membership', f: f, group: @group + = render_if_exists 'groups/settings/personal_access_tokens', f: f, group: @group %h5= _('Customer relations') .form-group.gl-mb-3 diff --git a/db/migrate/20240402120744_add_disable_personal_access_tokens_to_namespace_settings.rb b/db/migrate/20240402120744_add_disable_personal_access_tokens_to_namespace_settings.rb new file mode 100644 index 0000000000000000000000000000000000000000..ea6e7ca13ada10b16ef48eaf4a03dfffa3e8c675 --- /dev/null +++ b/db/migrate/20240402120744_add_disable_personal_access_tokens_to_namespace_settings.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +class AddDisablePersonalAccessTokensToNamespaceSettings < Gitlab::Database::Migration[2.2] + milestone '16.11' + + def change + add_column :namespace_settings, :disable_personal_access_tokens, :boolean, default: false, null: false + end +end diff --git a/db/schema_migrations/20240402120744 b/db/schema_migrations/20240402120744 new file mode 100644 index 0000000000000000000000000000000000000000..5583a96610de06fc1ebd855b0aa1d4267640ba90 --- /dev/null +++ b/db/schema_migrations/20240402120744 @@ -0,0 +1 @@ +bd1e2bdc4fda7c033a3407516450f15b45bfab4d6600a7bc940d10ca38f3e491 \ No newline at end of file diff --git a/db/structure.sql b/db/structure.sql index d3873e787420ff915a8118b0b4b0be77c2706605..9a79731504bb23dcfc07410903c6400d4a0b8b54 100644 --- a/db/structure.sql +++ b/db/structure.sql @@ -11882,6 +11882,7 @@ CREATE TABLE namespace_settings ( lock_math_rendering_limits_enabled boolean DEFAULT false NOT NULL, duo_features_enabled boolean, lock_duo_features_enabled boolean DEFAULT false NOT NULL, + disable_personal_access_tokens boolean DEFAULT false 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/ee/groups_controller.rb b/ee/app/controllers/ee/groups_controller.rb index 4e5b33a24f89ac5c6413107ebf33e87962528955..1f0d486eb4b4d0d49bd5abe865c5fccedc1dab18 100644 --- a/ee/app/controllers/ee/groups_controller.rb +++ b/ee/app/controllers/ee/groups_controller.rb @@ -97,6 +97,7 @@ def group_params_ee params_ee << { value_stream_dashboard_aggregation_attributes: [:enabled] } if can?(current_user, :modify_value_stream_dashboard_settings, current_group) params_ee << :experiment_features_enabled if experiment_settings_allowed? params_ee.push(%i[duo_features_enabled lock_duo_features_enabled]) if licensed_ai_features_available? + params_ee << :disable_personal_access_tokens end + security_policies_toggle_params end diff --git a/ee/app/controllers/ee/user_settings/personal_access_tokens_controller.rb b/ee/app/controllers/ee/user_settings/personal_access_tokens_controller.rb new file mode 100644 index 0000000000000000000000000000000000000000..d6a1ea92da6c809ce34c278b4ace718516546e6f --- /dev/null +++ b/ee/app/controllers/ee/user_settings/personal_access_tokens_controller.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +module EE + module UserSettings + module PersonalAccessTokensController + extend ::Gitlab::Utils::Override + + override :check_personal_access_tokens_enabled + def check_personal_access_tokens_enabled + super + + return unless current_user.enterprise_user? && current_user.enterprise_group.disable_personal_access_tokens? + + render_404 + end + end + end +end diff --git a/ee/app/models/ee/group.rb b/ee/app/models/ee/group.rb index 1b98ae375a849ed92f33b7ed48b5f522a85bd446..e341f8b3f01fc71b09ed546c2401c1be9683701e 100644 --- a/ee/app/models/ee/group.rb +++ b/ee/app/models/ee/group.rb @@ -97,6 +97,8 @@ module Group delegate :experiment_settings_allowed?, to: :namespace_settings delegate :user_cap_enabled?, to: :namespace_settings + delegate :disable_personal_access_tokens=, to: :namespace_settings + delegate :wiki_access_level, :wiki_access_level=, to: :group_feature, allow_nil: true # Use +checked_file_template_project+ instead, which implements important @@ -1005,6 +1007,14 @@ def code_suggestions_purchased? ::GitlabSubscriptions::AddOnPurchase.active.for_gitlab_duo_pro.exists?(namespace_id: id) end + # Disable personal access tokens for enterprise users of this group + def disable_personal_access_tokens? + ::Feature.enabled?(:enterprise_disable_personal_access_tokens, self) && + root? && + licensed_feature_available?(:disable_personal_access_tokens) && + namespace_settings.disable_personal_access_tokens? + end + private def active_project_tokens_of_root_ancestor diff --git a/ee/app/models/ee/user.rb b/ee/app/models/ee/user.rb index 6b4f21cf1a7fab65e79501e8351e79f932faa9b9..ec5e78b80832066a135d6066e88fd449fb4d3bcd 100644 --- a/ee/app/models/ee/user.rb +++ b/ee/app/models/ee/user.rb @@ -59,6 +59,8 @@ module User :onboarding_status_registration_type, :onboarding_status_registration_type=, :onboarding_status_email_opt_in, :onboarding_status_email_opt_in=, :onboarding_status, :onboarding_status=, :onboarding_status_initial_registration_type, :onboarding_status_initial_registration_type=, + :enterprise_group, :enterprise_group=, + :enterprise_group_id, :enterprise_group_id=, to: :user_detail, allow_nil: true delegate :enabled_zoekt?, :enabled_zoekt, :enabled_zoekt=, @@ -525,11 +527,13 @@ def authorized_by_provisioning_group?(group) end def enterprise_user_of_group?(group) - user_detail.enterprise_group_id == group.id + enterprise_group_id == group.id end def enterprise_user? - user_detail.enterprise_group.present? + # NOTE: Double check is added since enterprise_group_id is a lose foreign key and this is a high traffic method + # This would make sure that we don't fire a query in most cases on gitlab.com, as we have more normal users than enterprise users. + enterprise_group_id.present? && enterprise_group.present? end def gitlab_employee? diff --git a/ee/app/views/groups/settings/_personal_access_tokens.html.haml b/ee/app/views/groups/settings/_personal_access_tokens.html.haml new file mode 100644 index 0000000000000000000000000000000000000000..421d7182282a3b5705fb7028a524e0b314d2ade8 --- /dev/null +++ b/ee/app/views/groups/settings/_personal_access_tokens.html.haml @@ -0,0 +1,10 @@ +- return unless group.root? && group.licensed_feature_available?(:disable_personal_access_tokens) + +%h5= _('Personal access tokens') + +.form-group.gl-mb-3 + = f.gitlab_ui_checkbox_component :disable_personal_access_tokens, checkbox_options: { checked: group.disable_personal_access_tokens? } do |c| + - c.with_label do + = s_('GroupSettings|Disable personal access tokens') + - c.with_help_text do + = s_("GroupSettings|If enabled, enterprise user accounts will not be able to use personal access tokens.") diff --git a/ee/config/feature_flags/gitlab_com_derisk/enterprise_disable_personal_access_tokens.yml b/ee/config/feature_flags/gitlab_com_derisk/enterprise_disable_personal_access_tokens.yml new file mode 100644 index 0000000000000000000000000000000000000000..e09e1b4570053ad52cf6e17dd167a863f5ed6e69 --- /dev/null +++ b/ee/config/feature_flags/gitlab_com_derisk/enterprise_disable_personal_access_tokens.yml @@ -0,0 +1,9 @@ +--- +name: enterprise_disable_personal_access_tokens +feature_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/369504 +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/148415 +rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/455064 +milestone: '16.11' +group: group::authentication +type: gitlab_com_derisk +default_enabled: false diff --git a/ee/lib/ee/gitlab/auth/auth_finders.rb b/ee/lib/ee/gitlab/auth/auth_finders.rb index 31fe3a8152c03a400263c8a27f357bc5188c99a2..17ddfad5d7cb5ce888c7d6923db9d55d32da87b4 100644 --- a/ee/lib/ee/gitlab/auth/auth_finders.rb +++ b/ee/lib/ee/gitlab/auth/auth_finders.rb @@ -37,6 +37,17 @@ def find_oauth_access_token super end + override :find_user_from_access_token + def find_user_from_access_token + user = super + + disable_personal_access_tokens = user&.enterprise_user? && + user.enterprise_group.disable_personal_access_tokens? + return user unless disable_personal_access_tokens + + raise ::Gitlab::Auth::UnauthorizedError + end + def scim_request? current_request.path.starts_with?("/api/scim/") end diff --git a/ee/lib/ee/sidebars/user_settings/menus/access_tokens_menu.rb b/ee/lib/ee/sidebars/user_settings/menus/access_tokens_menu.rb new file mode 100644 index 0000000000000000000000000000000000000000..194e9ceddba46d7f897a1c43e79597a0592f7ed6 --- /dev/null +++ b/ee/lib/ee/sidebars/user_settings/menus/access_tokens_menu.rb @@ -0,0 +1,20 @@ +# frozen_string_literal: true + +module EE + module Sidebars + module UserSettings + module Menus + module AccessTokensMenu + extend ::Gitlab::Utils::Override + + override :render? + def render? + return false if context.current_user&.enterprise_group&.disable_personal_access_tokens? + + super + end + end + end + end + end +end diff --git a/ee/spec/lib/ee/gitlab/auth/auth_finders_spec.rb b/ee/spec/lib/ee/gitlab/auth/auth_finders_spec.rb index 77e0693d2c84489a01b5610341a754a2c5ac4a01..46928847cfc2ea84a41149d3fc0176b6e952d4c4 100644 --- a/ee/spec/lib/ee/gitlab/auth/auth_finders_spec.rb +++ b/ee/spec/lib/ee/gitlab/auth/auth_finders_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe Gitlab::Auth::AuthFinders do +RSpec.describe Gitlab::Auth::AuthFinders, feature_category: :system_access do include described_class include ::EE::GeoHelpers @@ -140,7 +140,7 @@ expect(find_user_from_access_token).to eq user end - context 'when personal access tokens are disabled' do + context 'when personal access tokens are disabled on instance level' do before do stub_licensed_features(disable_personal_access_tokens: true) stub_application_setting(disable_personal_access_tokens: true) @@ -150,6 +150,49 @@ expect { find_user_from_access_token }.to raise_error(Gitlab::Auth::UnauthorizedError) end end + + context 'when personal access tokens are disabled for the group for enterprise users' do + let(:user) { create(:enterprise_user) } + + before do + user.enterprise_group.update!(disable_personal_access_tokens: true) + stub_licensed_features(disable_personal_access_tokens: true) + end + + it 'raised unauthorized error' do + expect { find_user_from_access_token }.to raise_error(Gitlab::Auth::UnauthorizedError) + end + end + + context 'when personal access tokens are disabled for the group for service accounts' do + let(:user) { create(:service_account, provisioned_by_group: create(:group)) } + + before do + user.provisioned_by_group.update!(disable_personal_access_tokens: true) + end + + it 'returns user' do + expect(find_user_from_access_token).to eq user + end + end + + context 'when the feature flag enterprise_disable_personal_access_tokens is disabled' do + before do + stub_feature_flags(enterprise_disable_personal_access_tokens: false) + end + + context 'when personal access tokens are disabled for the group for enterprise users' do + let(:user) { create(:enterprise_user) } + + before do + user.enterprise_group.update!(disable_personal_access_tokens: true) + end + + it 'returns user' do + expect(find_user_from_access_token).to eq user + end + end + end end end diff --git a/ee/spec/lib/ee/sidebars/user_settings/menus/access_tokens_menu_spec.rb b/ee/spec/lib/ee/sidebars/user_settings/menus/access_tokens_menu_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..49568baf78b4649a32baab98b76fc98bb151e21a --- /dev/null +++ b/ee/spec/lib/ee/sidebars/user_settings/menus/access_tokens_menu_spec.rb @@ -0,0 +1,35 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Sidebars::UserSettings::Menus::AccessTokensMenu, feature_category: :system_access do + subject(:sidebar_item) { described_class.new(context) } + + let_it_be(:user) { build(:user) } + + context 'when personal access tokens are disabled for enterprise users' do + let(:user) { build(:enterprise_user) } + + before do + allow(user.enterprise_group).to receive(:disable_personal_access_tokens?).and_return(true) + end + + context 'when user is logged in' do + let(:context) { Sidebars::Context.new(current_user: user, container: nil) } + + it 'does not render' do + expect(sidebar_item.render?).to be false + end + end + + context 'when user is not logged in' do + let(:context) { Sidebars::Context.new(current_user: nil, container: nil) } + + subject { described_class.new(context) } + + it 'does not render' do + expect(sidebar_item.render?).to be false + end + end + end +end diff --git a/ee/spec/requests/api/epics_spec.rb b/ee/spec/requests/api/epics_spec.rb index d06feb40057dfb86f7db7b2e452bf82aeabb5adb..7904bff500ded34a09c84cf7bca3bef4be5da0af 100644 --- a/ee/spec/requests/api/epics_spec.rb +++ b/ee/spec/requests/api/epics_spec.rb @@ -673,6 +673,7 @@ context 'when the request is correct' do before do + allow(Gitlab::QueryLimiting::Transaction).to receive(:threshold).and_return(101) post api(url, user), params: params end @@ -822,6 +823,7 @@ context 'with rate limiter', :freeze_time, :clean_gitlab_redis_rate_limiting do before do + allow(Gitlab::QueryLimiting::Transaction).to receive(:threshold).and_return(101) stub_application_setting(issues_create_limit: 1) end @@ -873,7 +875,7 @@ it 'creates a new epic with labels param as array' do # TODO: remove threshold after epic-work item sync # issue: https://gitlab.com/gitlab-org/gitlab/-/issues/438295 - allow(Gitlab::QueryLimiting::Transaction).to receive(:threshold).and_return(135) + allow(Gitlab::QueryLimiting::Transaction).to receive(:threshold).and_return(136) params[:labels] = ['label1', 'label2', 'foo, bar', '&,?'] post api(url, user), params: params @@ -924,7 +926,7 @@ stub_licensed_features(epics: true, subepics: true) # TODO: reduce threshold after epic-work item sync # issue: https://gitlab.com/gitlab-org/gitlab/-/issues/438295 - allow(Gitlab::QueryLimiting::Transaction).to receive(:threshold).and_return(147) + allow(Gitlab::QueryLimiting::Transaction).to receive(:threshold).and_return(148) end it_behaves_like 'PUT request permissions for admin mode' do @@ -967,7 +969,7 @@ before do # TODO: reduce threshold after epic-work item sync # issue: https://gitlab.com/gitlab-org/gitlab/-/issues/438295 - allow(Gitlab::QueryLimiting::Transaction).to receive(:threshold).and_return(174) + allow(Gitlab::QueryLimiting::Transaction).to receive(:threshold).and_return(175) group.add_developer(user) end diff --git a/ee/spec/requests/ee/user_settings/personal_access_tokens_controller_spec.rb b/ee/spec/requests/ee/user_settings/personal_access_tokens_controller_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..2337dabc93d1820ec4cc8cda02ac120475ffe53c --- /dev/null +++ b/ee/spec/requests/ee/user_settings/personal_access_tokens_controller_spec.rb @@ -0,0 +1,37 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe UserSettings::PersonalAccessTokensController, type: :request, feature_category: :shared do + let(:user) { create(:enterprise_user, :with_namespace) } + + before do + sign_in(user) + end + + subject(:make_request) { get '/-/user_settings/personal_access_tokens' } + + context "when personal access tokens are disabled for enterprise users" do + before do + allow(user.enterprise_group).to receive(:disable_personal_access_tokens?).and_return(true) + stub_licensed_features(disable_personal_access_tokens: true) + end + + it 'returns not found' do + make_request + expect(response).to have_gitlab_http_status(:not_found) + end + end + + context "when personal access tokens are not disabled for enterprise users" do + before do + allow(user.enterprise_group).to receive(:disable_personal_access_tokens?).and_return(false) + stub_licensed_features(disable_personal_access_tokens: true) + end + + it 'render page' do + make_request + expect(response).to have_gitlab_http_status(:ok) + end + end +end diff --git a/lib/sidebars/user_settings/menus/access_tokens_menu.rb b/lib/sidebars/user_settings/menus/access_tokens_menu.rb index cb3f319ddde5feeab3830539d1d0fef5ce06b1e1..3f875eaa08ba3a51ab99a87eab043b9752e5b94e 100644 --- a/lib/sidebars/user_settings/menus/access_tokens_menu.rb +++ b/lib/sidebars/user_settings/menus/access_tokens_menu.rb @@ -21,7 +21,10 @@ def sprite_icon override :render? def render? - !!context.current_user && !Gitlab::CurrentSettings.personal_access_tokens_disabled? + return false unless context.current_user + return false if Gitlab::CurrentSettings.personal_access_tokens_disabled? + + true end override :active_routes @@ -37,3 +40,5 @@ def extra_container_html_options end end end + +Sidebars::UserSettings::Menus::AccessTokensMenu.prepend_mod diff --git a/locale/gitlab.pot b/locale/gitlab.pot index f20d7363e4aa44f259d82446c1224260fcf7baa4..7caa927d401a3ea9fd13d294a33f26b81027a82a 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -24806,6 +24806,9 @@ msgstr "" msgid "GroupSettings|Default to Auto DevOps pipeline for all projects within this group" msgstr "" +msgid "GroupSettings|Disable personal access tokens" +msgstr "" + msgid "GroupSettings|Duo features" msgstr "" @@ -24854,6 +24857,9 @@ msgstr "" msgid "GroupSettings|How do I manage group SSH certificates?" msgstr "" +msgid "GroupSettings|If enabled, enterprise user accounts will not be able to use personal access tokens." +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 "" @@ -36882,6 +36888,9 @@ msgstr "" msgid "Personal access token" msgstr "" +msgid "Personal access tokens" +msgstr "" + msgid "Personal projects" msgstr "" diff --git a/spec/lib/sidebars/user_settings/menus/access_tokens_menu_spec.rb b/spec/lib/sidebars/user_settings/menus/access_tokens_menu_spec.rb index eebd089ad3f946cdaab06dcb3305e2a23301b2b9..ad96c1e4324e5a3246d6c9b30384fd1e5bb0889f 100644 --- a/spec/lib/sidebars/user_settings/menus/access_tokens_menu_spec.rb +++ b/spec/lib/sidebars/user_settings/menus/access_tokens_menu_spec.rb @@ -14,7 +14,7 @@ let_it_be(:user) { build(:user) } - context 'when personal access tokens are disabled' do + context 'when personal access tokens are disabled in the instance' do before do allow(::Gitlab::CurrentSettings).to receive_messages(personal_access_tokens_disabled?: true) end diff --git a/spec/requests/api/ci/jobs_spec.rb b/spec/requests/api/ci/jobs_spec.rb index 9e1203bc720988af01b15442055c80b2895c9cf9..9393ddb15ec57a990af79fa10ad1194902618cbc 100644 --- a/spec/requests/api/ci/jobs_spec.rb +++ b/spec/requests/api/ci/jobs_spec.rb @@ -821,6 +821,7 @@ def go let(:job) { create(:ci_build, :canceled, pipeline: pipeline) } before do + allow(Gitlab::QueryLimiting::Transaction).to receive(:threshold).and_return(102) post api("/projects/#{project.id}/jobs/#{job.id}/retry", api_user) end