diff --git a/app/controllers/concerns/groups/params.rb b/app/controllers/concerns/groups/params.rb index 95cf0571fd7e9288198d11197c25f0c0377e20b3..77a59a1ac2addc271de5ff6621d431338f03d8a3 100644 --- a/app/controllers/concerns/groups/params.rb +++ b/app/controllers/concerns/groups/params.rb @@ -56,7 +56,8 @@ def group_params_attributes :crm_enabled, :crm_source_group_id, :force_pages_access_control, - :enable_namespace_descendants_cache + :enable_namespace_descendants_cache, + :step_up_auth_required_oauth_provider ] + [group_feature_attributes: group_feature_attributes] end diff --git a/app/helpers/groups_helper.rb b/app/helpers/groups_helper.rb index da9c8c031d8c50952017ddaee28b7f9d6cd1f4de..24421a89a5ee03f411113cb562b4400815fd718c 100644 --- a/app/helpers/groups_helper.rb +++ b/app/helpers/groups_helper.rb @@ -254,6 +254,14 @@ def group_merge_requests(group) MergeRequestsFinder.new(current_user, group_id: group.id, include_subgroups: true, non_archived: true).execute end + def step_up_auth_provider_options_for_select + available_step_up_auth_providers_for_namespace.map do |provider| + provider_config = Gitlab::Auth::OAuth::Provider.config_for(provider.to_s) + provider_label = provider_config[:label].presence || provider.to_s.humanize + [provider_label, provider.to_s] + end + end + private def group_title_link(group, hidable: false, show_avatar: false) @@ -331,6 +339,12 @@ def localized_jobs_to_be_done_choices other: _('A different reason') }.with_indifferent_access.freeze end + + def available_step_up_auth_providers_for_namespace + Gitlab::Auth::Oidc::StepUpAuthentication.enabled_providers( + scope: Gitlab::Auth::Oidc::StepUpAuthentication::STEP_UP_AUTH_SCOPE_NAMESPACE + ) + end end GroupsHelper.prepend_mod_with('GroupsHelper') diff --git a/app/models/group.rb b/app/models/group.rb index b64ef3ba9fc5469201f3ebc0c17e92a9478a7850..149ecb09e89d20e430750707c25515aa7ce33e01 100644 --- a/app/models/group.rb +++ b/app/models/group.rb @@ -182,6 +182,8 @@ def self.supported_keyset_orderings :require_dpop_for_manage_api_endpoints=, :seat_control, :setup_for_company, + :step_up_auth_required_oauth_provider, + :step_up_auth_required_oauth_provider=, to: :namespace_settings ) diff --git a/app/models/namespace_setting.rb b/app/models/namespace_setting.rb index 89d4dc2c35482dd64b21cda1ab58a9c088351ddb..e925c1d625bca4881ca840d4e016c9f07f3b878a 100644 --- a/app/models/namespace_setting.rb +++ b/app/models/namespace_setting.rb @@ -58,10 +58,18 @@ class NamespaceSetting < ApplicationRecord validate :validate_enterprise_bypass_expires_at, if: ->(record) { record.allow_enterprise_bypass_placeholder_confirmation? && (record.new_record? || record.will_save_change_to_enterprise_bypass_expires_at?) } + validates :step_up_auth_required_oauth_provider, presence: true, allow_nil: true + validates :step_up_auth_required_oauth_provider, inclusion: { in: ->(_) { + Gitlab::Auth::Oidc::StepUpAuthentication + .enabled_providers(scope: Gitlab::Auth::Oidc::StepUpAuthentication::STEP_UP_AUTH_SCOPE_NAMESPACE) + .map(&:to_s) + } }, allow_nil: true sanitizes! :default_branch_name nullify_if_blank :default_branch_name + nullify_if_blank :step_up_auth_required_oauth_provider + before_validation :set_pipeline_variables_default_role, on: :create after_update :invalidate_namespace_descendants_cache, if: -> { saved_change_to_archived? } diff --git a/app/services/groups/update_service.rb b/app/services/groups/update_service.rb index 33f5a0578186f7dfb43182724750a7c9f8f51a3d..4d4a1411617a77d800051af3ebfd5a5560324b73 100644 --- a/app/services/groups/update_service.rb +++ b/app/services/groups/update_service.rb @@ -109,6 +109,10 @@ def reject_parent_id! # overridden in EE def remove_unallowed_params + if Feature.disabled?(:omniauth_step_up_auth_for_namespace, group) + params.delete(:step_up_auth_required_oauth_provider) + end + params.delete(:emails_enabled) unless can?(current_user, :set_emails_disabled, group) params.delete(:max_artifacts_size) unless can?(current_user, :update_max_artifacts_size, group) diff --git a/app/views/groups/settings/_permissions.html.haml b/app/views/groups/settings/_permissions.html.haml index d5abcfab34247c5154bf9e4384f5369fa3e30081..237ce0dd951898db044b89993b98050b55cdcccc 100644 --- a/app/views/groups/settings/_permissions.html.haml +++ b/app/views/groups/settings/_permissions.html.haml @@ -51,6 +51,8 @@ = render_if_exists 'groups/settings/extended_grat_expiry_webhook_execute', f: f, group: @group = render_if_exists 'groups/settings/enforce_ssh_certificates', f: f, group: @group = render 'groups/settings/two_factor_auth', f: f, group: @group + - if Feature.enabled?(:omniauth_step_up_auth_for_namespace, @group) + = render 'groups/settings/step_up_authentication', f: f, group: @group = render 'groups/settings/membership', f: f, group: @group = render_if_exists 'groups/settings/placeholder_confirmation_bypass', f: f, group: @group = render_if_exists 'groups/settings/remove_dormant_members', f: f, group: @group diff --git a/app/views/groups/settings/_step_up_authentication.html.haml b/app/views/groups/settings/_step_up_authentication.html.haml new file mode 100644 index 0000000000000000000000000000000000000000..470d1db15d5aa55143e294e08a9318ff8e8780c8 --- /dev/null +++ b/app/views/groups/settings/_step_up_authentication.html.haml @@ -0,0 +1,15 @@ +%h5= s_('GroupSettings|Step-up authentication') +%p + = link_to s_('GroupSettings|What is step-up authentication?'), + help_page_path('administration/auth/oidc.md', anchor: 'force-step-up-authentication-for-a-group'), + target: '_blank', + rel: 'noopener noreferrer' + +.form-group + = f.select :step_up_auth_required_oauth_provider, + step_up_auth_provider_options_for_select, + { include_blank: s_('GroupSettings|Disabled') }, + { class: 'form-control gl-form-select' } + + .form-text.gl-text-subtle + = s_('GroupSettings|Forces users to complete step-up authentication before they can access this group.') diff --git a/config/audit_events/types/step_up_auth_required_oauth_provider_updated.yml b/config/audit_events/types/step_up_auth_required_oauth_provider_updated.yml new file mode 100644 index 0000000000000000000000000000000000000000..4b8b59c3696e0c97e0fe7df959f5f3fff5140b6d --- /dev/null +++ b/config/audit_events/types/step_up_auth_required_oauth_provider_updated.yml @@ -0,0 +1,10 @@ +--- +name: step_up_auth_required_oauth_provider_updated +description: Step-up authentication OAuth provider requirement is updated +introduced_by_issue: https://gitlab.com/gitlab-org/gitlab/-/issues/556943 +introduced_by_mr: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/199423 +feature_category: system_access +milestone: '18.4' +saved_to_database: true +streamed: true +scope: [Group] \ No newline at end of file diff --git a/config/feature_flags/gitlab_com_derisk/omniauth_step_up_auth_for_namespace.yml b/config/feature_flags/gitlab_com_derisk/omniauth_step_up_auth_for_namespace.yml new file mode 100644 index 0000000000000000000000000000000000000000..f37c56ffd3a303259cf8ba070cf3b7b41bfa80be --- /dev/null +++ b/config/feature_flags/gitlab_com_derisk/omniauth_step_up_auth_for_namespace.yml @@ -0,0 +1,9 @@ +--- +name: omniauth_step_up_auth_for_namespace +feature_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/556943 +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/199423 +rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/510951 +milestone: '18.4' +group: group::authentication +type: gitlab_com_derisk +default_enabled: false diff --git a/db/migrate/20250820070256_add_step_up_auth_required_oauth_provider_to_namespace_settings.rb b/db/migrate/20250820070256_add_step_up_auth_required_oauth_provider_to_namespace_settings.rb new file mode 100644 index 0000000000000000000000000000000000000000..074c5ebeec3ba3d5928a2e1b2674570cca3c9829 --- /dev/null +++ b/db/migrate/20250820070256_add_step_up_auth_required_oauth_provider_to_namespace_settings.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +class AddStepUpAuthRequiredOauthProviderToNamespaceSettings < Gitlab::Database::Migration[2.3] + milestone '18.4' + + # rubocop:disable Migration/AddLimitToTextColumns -- Limit is added in 20250820070257_add_text_limit_to_step_up_auth_required_oauth_provider + def up + add_column :namespace_settings, :step_up_auth_required_oauth_provider, :text + end + # rubocop:enable Migration/AddLimitToTextColumns + + def down + remove_column :namespace_settings, :step_up_auth_required_oauth_provider + end +end diff --git a/db/migrate/20250820070257_add_text_limit_to_step_up_auth_required_oauth_provider.rb b/db/migrate/20250820070257_add_text_limit_to_step_up_auth_required_oauth_provider.rb new file mode 100644 index 0000000000000000000000000000000000000000..082a5d2018e2010f8945e9a0752d452ce57a859a --- /dev/null +++ b/db/migrate/20250820070257_add_text_limit_to_step_up_auth_required_oauth_provider.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +class AddTextLimitToStepUpAuthRequiredOauthProvider < Gitlab::Database::Migration[2.3] + milestone '18.4' + + disable_ddl_transaction! + + def up + add_text_limit :namespace_settings, :step_up_auth_required_oauth_provider, 255 + end + + def down + remove_text_limit :namespace_settings, :step_up_auth_required_oauth_provider + end +end diff --git a/db/schema_migrations/20250820070256 b/db/schema_migrations/20250820070256 new file mode 100644 index 0000000000000000000000000000000000000000..3c0463ea243f88e7047050bff1d64eebf7b0ba28 --- /dev/null +++ b/db/schema_migrations/20250820070256 @@ -0,0 +1 @@ +72a1b0ec5efc1c04c2deb6af2d28ee4511cadfdfe74b36ab41322a873cdfa346 \ No newline at end of file diff --git a/db/schema_migrations/20250820070257 b/db/schema_migrations/20250820070257 new file mode 100644 index 0000000000000000000000000000000000000000..565e59590d969529f7c1b4c0f2b65bba77de09b2 --- /dev/null +++ b/db/schema_migrations/20250820070257 @@ -0,0 +1 @@ +1a9e7379e005315bbfe02e13b0e996d76e8a8ddc42ade169846acdfb5e8f4326 \ No newline at end of file diff --git a/db/structure.sql b/db/structure.sql index 739ba0424b4f4aa841a10e0c42e786880af908b7..8e1bd9253eff7b4356253b502f257a49c5c732bc 100644 --- a/db/structure.sql +++ b/db/structure.sql @@ -19511,7 +19511,9 @@ CREATE TABLE namespace_settings ( allow_personal_snippets boolean DEFAULT true NOT NULL, auto_duo_code_review_enabled boolean, lock_auto_duo_code_review_enabled boolean DEFAULT false NOT NULL, + step_up_auth_required_oauth_provider text, CONSTRAINT check_0ba93c78c7 CHECK ((char_length(default_branch_name) <= 255)), + CONSTRAINT check_d9644d516f CHECK ((char_length(step_up_auth_required_oauth_provider) <= 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)), CONSTRAINT namespace_settings_unique_project_download_limit_allowlist_size CHECK ((cardinality(unique_project_download_limit_allowlist) <= 100)) diff --git a/doc/administration/auth/oidc.md b/doc/administration/auth/oidc.md index 7b1ea34582682dbb0b0e14ad44d7efd5aa634a14..b15b67b03fcc1812dbc764f19d6006cfdbb512f5 100644 --- a/doc/administration/auth/oidc.md +++ b/doc/administration/auth/oidc.md @@ -1409,7 +1409,7 @@ To configure a custom duration for your ID tokens: {{< /tabs >}} -## Step-up authentication for Admin Mode +## Step-up authentication {{< details >}} @@ -1419,12 +1419,6 @@ To configure a custom duration for your ID tokens: {{< /details >}} -{{< history >}} - -- [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/474650) in GitLab 17.11 [with a flag](../feature_flags/_index.md) named `omniauth_step_up_auth_for_admin_mode`. Disabled by default. - -{{< /history >}} - {{< alert type="flag" >}} The availability of this feature is controlled by a feature flag. @@ -1448,6 +1442,11 @@ This feature is an [experiment](../../policy/development_stages_support.md) and ### Enable step-up authentication for Admin Mode +{{< history >}} + +- [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/474650) in GitLab 17.11 [with a flag](../feature_flags/_index.md) named `omniauth_step_up_auth_for_admin_mode`. Disabled by default. + +{{< /history >}} To enable step-up authentication for Admin Mode: 1. Edit your GitLab configuration file (`gitlab.yml` or `/etc/gitlab/gitlab.rb`) to enable @@ -1638,6 +1637,96 @@ To require step-up authentication for Admin Mode with Microsoft Entra ID: 1. Save the configuration file and restart GitLab for the changes to take effect. +### Add a step-up authentication provider for groups + +{{< history >}} + +- [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/556943) in GitLab 18.4 [with a flag](../feature_flags/_index.md) named `omniauth_step_up_auth_for_namespace`. Disabled by default. + +{{< /history >}} + +You can also add step-up authentication providers available to all groups in your instance. This does not force groups to use step-up authentication, each group must still [set up](#force-step-up-authentication-for-a-group) this feature individually. + +To add a step-up authentication provider for groups: + +1. Edit your GitLab configuration file (`gitlab.yml` or `/etc/gitlab/gitlab.rb`) to enable + step-up authentication for an specific OmniAuth provider. + + ```yaml + production: &base + omniauth: + providers: + - { name: 'openid_connect', + label: 'Provider name', + args: { + name: 'openid_connect', + # ... + allow_authorize_params: ["claims"], # Match this to the parameters defined in `step_up_auth => admin_mode => params` + }, + step_up_auth: { + # Unlike step-up authentication configuration for Admin Mode, you use the `namespace` + # object. This is because you're adding step-up authentication to access the entire + # group, not just Admin Mode. + namespace : { + # The `id_token` field defines the claims that must be included with the token. + # You can specify claims in one or both of the `required` or `included` fields. + # The token must include matching values for every claim you define in these fields. + id_token: { + # The `required` field defines key-value pairs that must be included with the ID token. + # The values must match exactly what is defined. + # In this example, the 'acr' (Authentication Context Class Reference) claim + # must have the value 'gold' to pass the step-up authentication challenge. + # This ensures a specific level of authentication assurance. + required: { + acr: 'gold' + }, + # The `included` field also defines key-value pairs that must be included with the ID token. + # Multiple accepted values can be defined in an array. If an array is not used, the value must match exactly. + # In this example, the 'amr' (Authentication Method References) claim + # must have a value of either 'mfa' or 'fpt' to pass the step-up authentication challenge. + # This is useful for scenarios where the user must provide additional authentication factors. + included: { + amr: ['mfa', 'fpt'] + }, + }, + # The `params` field defines any additional parameters that are sent during the authentication process. + # In this example, the `claims` parameter is added to the authorization request and instructs the + # identity provider to include an 'acr' claim with the value 'gold' in the ID token. + # The 'essential: true' indicates that this claim is required for successful authentication. + params: { + claims: { + id_token: { + acr: { + essential: true, + values: ['gold'] + } + } + } + } + }, + } + } + ``` + +1. Save the configuration file and restart GitLab for the changes to take effect. + +### Force step-up authentication for a group + +You can force users to complete step-up authentication before they access a group. This setting is managed for each group individually, but requires a step-up authentication provider that was previously added for the entire instance. + +Prerequisites: + +- [A step-up authentication provider for groups in your instance](#add-a-step-up-authentication-provider-for-groups). +- You must have the Owner role. + +To force step-up authentication for a group: + +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. Under Step-up authentication, select an available authentication provider. +1. Select **Save changes**. + ### Add custom documentation links for step-up authentication When step-up authentication fails, GitLab can display custom documentation links to help users understand diff --git a/doc/user/compliance/audit_event_types.md b/doc/user/compliance/audit_event_types.md index 0c6697106d519ad23518ac9f8a2d696789e9c2e4..e4a45b39e7ffba968748bcbf166a32d740e16200 100644 --- a/doc/user/compliance/audit_event_types.md +++ b/doc/user/compliance/audit_event_types.md @@ -655,6 +655,7 @@ Audit event types belong to the following product categories. | [`authenticated_with_password`](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/198216) | User successfully signed in with password | {{< icon name="check-circle" >}} Yes | GitLab [18.3](https://gitlab.com/gitlab-org/gitlab/-/issues/555101) | User | | [`authenticated_with_two_factor`](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/198216) | User successfully signed in with two-factor authentication | {{< icon name="check-circle" >}} Yes | GitLab [18.3](https://gitlab.com/gitlab-org/gitlab/-/issues/555101) | User | | [`authenticated_with_webauthn`](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/198216) | User successfully signed in with WebAuthn device | {{< icon name="check-circle" >}} Yes | GitLab [18.3](https://gitlab.com/gitlab-org/gitlab/-/issues/555101) | User | +| [`step_up_auth_required_oauth_provider_updated`](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/199423) | Step-up authentication OAuth provider requirement is updated | {{< icon name="check-circle" >}} Yes | GitLab [18.4](https://gitlab.com/gitlab-org/gitlab/-/issues/556943) | Group | ### Team planning diff --git a/ee/lib/namespaces/namespace_setting_changes_auditor.rb b/ee/lib/namespaces/namespace_setting_changes_auditor.rb index a3462123aa43457b2eccdbaaee0c8c85745b155c..a9f0a663fe0366ae91320949f9327e82d7f7d56e 100644 --- a/ee/lib/namespaces/namespace_setting_changes_auditor.rb +++ b/ee/lib/namespaces/namespace_setting_changes_auditor.rb @@ -21,7 +21,8 @@ class NamespaceSettingChangesAuditor < ::AuditEvents::BaseChangesAuditor remove_dormant_members: 'remove_dormant_members_updated', remove_dormant_members_period: 'remove_dormant_members_period_updated', prevent_sharing_groups_outside_hierarchy: 'prevent_sharing_groups_outside_hierarchy_updated', - seat_control: 'seat_control_updated' + seat_control: 'seat_control_updated', + step_up_auth_required_oauth_provider: 'step_up_auth_required_oauth_provider_updated' }.freeze def initialize(current_user, namespace_setting, group) diff --git a/lib/gitlab/auth/oidc/step_up_authentication.rb b/lib/gitlab/auth/oidc/step_up_authentication.rb index 833dd12ba577f43e52ef27ae8b926dd620c52cd5..cc36795933c51a39ad0d1cce0e3735f7240f1b1e 100644 --- a/lib/gitlab/auth/oidc/step_up_authentication.rb +++ b/lib/gitlab/auth/oidc/step_up_authentication.rb @@ -11,6 +11,12 @@ module StepUpAuthentication SESSION_STORE_KEY = 'omniauth_step_up_auth' STEP_UP_AUTH_SCOPE_ADMIN_MODE = :admin_mode + STEP_UP_AUTH_SCOPE_NAMESPACE = :namespace + + ALLOWED_SCOPES = [ + STEP_UP_AUTH_SCOPE_ADMIN_MODE, + STEP_UP_AUTH_SCOPE_NAMESPACE + ].freeze class << self # Checks if step-up authentication is enabled for the step-up auth scope 'admin_mode' @@ -32,21 +38,19 @@ def enabled_for_provider?(provider_name:, scope: STEP_UP_AUTH_SCOPE_ADMIN_MODE) has_included_claims?(provider_name, scope) end + def enabled_providers(scope: STEP_UP_AUTH_SCOPE_ADMIN_MODE) + oauth_providers.select do |provider| + enabled_for_provider?(provider_name: provider, scope: scope) + end + end + # Verifies if step-up authentication has succeeded for any provider # with the step-up auth scope 'admin_mode' # # @param session [Hash] the session hash containing authentication state # @return [Boolean] true if step-up authentication is authenticated def succeeded?(session, scope: STEP_UP_AUTH_SCOPE_ADMIN_MODE) - step_up_auth_flows = - omniauth_step_up_auth_session_data(session) - &.to_h - &.flat_map do |provider, step_up_auth_object| - step_up_auth_object.map do |step_up_auth_scope, _| - build_flow(provider: provider, session: session, scope: step_up_auth_scope) - end - end - step_up_auth_flows + step_up_auth_flows(session) .select do |step_up_auth_flow| step_up_auth_flow.scope.to_s == scope.to_s end diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 0a28f572b1ee4a3c3a0f82c3274a36e441c33a7d..71292b582aa914661723dea3a5f59eaa06aa95f4 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -31901,6 +31901,9 @@ msgstr "" msgid "GroupSettings|Disable user invitations to groups and projects within %{group}" msgstr "" +msgid "GroupSettings|Disabled" +msgstr "" + msgid "GroupSettings|Emails are not encrypted. Concerned administrators may want to disable diff previews." msgstr "" @@ -31940,6 +31943,9 @@ msgstr "" msgid "GroupSettings|Failed to save changes." msgstr "" +msgid "GroupSettings|Forces users to complete step-up authentication before they can access this group." +msgstr "" + msgid "GroupSettings|Git abuse rate limit" msgstr "" @@ -32045,6 +32051,9 @@ msgstr "" msgid "GroupSettings|Settings that apply only to enterprise users associated with this group." msgstr "" +msgid "GroupSettings|Step-up authentication" +msgstr "" + msgid "GroupSettings|The Auto DevOps pipeline runs if no alternative CI configuration file is found." msgstr "" @@ -32090,6 +32099,9 @@ msgstr "" msgid "GroupSettings|What is Insights?" msgstr "" +msgid "GroupSettings|What is step-up authentication?" +msgstr "" + msgid "GroupSettings|When disabled, members cannot use runner registration tokens to register runners. Members can use runner authentication tokens instead as the more secure registration method." msgstr "" diff --git a/spec/features/groups/settings/step_up_authentication_spec.rb b/spec/features/groups/settings/step_up_authentication_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..89db69e64be9ab4e4f25871962cba8cc842a34af --- /dev/null +++ b/spec/features/groups/settings/step_up_authentication_spec.rb @@ -0,0 +1,57 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe 'Group Step-up Authentication Settings', :js, feature_category: :groups_and_projects do + let_it_be(:group) { create(:group) } + let_it_be(:user) { create(:user, owner_of: group) } + + let(:ommiauth_provider_config_oidc) do + GitlabSettings::Options.new( + name: 'openid_connect', + label: 'OpenID Connect', + step_up_auth: { + namespace: { + id_token: { + required: { + acr: 'gold' + } + } + } + } + ) + end + + before do + sign_in(user) + + stub_omniauth_setting(enabled: true, providers: [ommiauth_provider_config_oidc]) + end + + it 'displays step-up authentication settings in group permissions and allows enabling step-up authentication' do + visit edit_group_path(group, anchor: 'js-permissions-settings') + + expect(page).to have_content('Step-up authentication') + expect(page).to have_select('group_step_up_auth_required_oauth_provider') + + select 'OpenID Connect', from: 'group_step_up_auth_required_oauth_provider' + click_button 'Save changes' + + expect(page).to have_content("Group '#{group.name}' was successfully updated.") + + expect(group.reload.namespace_settings.step_up_auth_required_oauth_provider).to eq('openid_connect') + end + + context 'when feature flag is disabled' do + before do + stub_feature_flags(omniauth_step_up_auth_for_namespace: false) + end + + it 'does not display step-up authentication settings' do + visit edit_group_path(group, anchor: 'js-permissions-settings') + + expect(page).not_to have_content('Step-up authentication') + expect(page).not_to have_select('group_step_up_auth_required_oauth_provider') + end + end +end diff --git a/spec/helpers/groups_helper_spec.rb b/spec/helpers/groups_helper_spec.rb index 31f14a62c6e7cc915c30014ac317f69a7010cca6..ca24b51226fd965ffaf61e54226f338805e29351 100644 --- a/spec/helpers/groups_helper_spec.rb +++ b/spec/helpers/groups_helper_spec.rb @@ -829,4 +829,63 @@ expect(helper.group_merge_requests(group)).to contain_exactly(merge_request) end end + + describe '#step_up_auth_provider_options_for_select' do + using RSpec::Parameterized::TableSyntax + + let_it_be(:group) { create(:group) } + let_it_be(:current_user) { create(:user, owner_of: group) } + + let(:omniauth_provider_oidc) do + GitlabSettings::Options.new( + name: "openid_connect", + step_up_auth: { + namespace: { + id_token: { + required: { + acr: 'gold' + } + } + } + } + ) + end + + let(:omniauth_provider_oidc_only_namespace) do + GitlabSettings::Options.new( + name: "openid_connect_only_namespace", + label: "OpenID Connect (Only namespace)", + args: { + strategy_class: 'OmniAuth::Strategies::OpenIDConnect' + }, + step_up_auth: { + namespace: { + id_token: { + required: { + acr: 'gold' + } + } + } + } + ) + end + + subject { helper.step_up_auth_provider_options_for_select } + + before do + allow(helper).to receive(:current_user).and_return(current_user) + + stub_omniauth_setting(enabled: true, providers: providers) + allow(Devise).to receive(:omniauth_providers).and_return(providers.map(&:name)) + end + + where(:providers, :expected_options) do + [ref(:omniauth_provider_oidc), ref(:omniauth_provider_oidc_only_namespace)] | [['Openid connect', 'openid_connect'], ['OpenID Connect (Only namespace)', 'openid_connect_only_namespace']] + [ref(:omniauth_provider_oidc)] | [['Openid connect', 'openid_connect']] + [] | [] + end + with_them do + it { is_expected.to match_array(expected_options) } + end + end end diff --git a/spec/lib/gitlab/auth/oidc/step_up_authentication_spec.rb b/spec/lib/gitlab/auth/oidc/step_up_authentication_spec.rb index e73ce036caa2162c69a7502fd0b877b416d5d607..9a734f610878d2aa8cdd7cbaa6b7c8a6cf411e48 100644 --- a/spec/lib/gitlab/auth/oidc/step_up_authentication_spec.rb +++ b/spec/lib/gitlab/auth/oidc/step_up_authentication_spec.rb @@ -302,4 +302,68 @@ it { is_expected.to all be_failed.and(be_enabled_by_config) } end end + + describe '.enabled_providers' do + subject { described_class.enabled_providers(scope: scope) } + + let(:omniauth_provider_oidc) do + GitlabSettings::Options.new( + name: "openid_connect", + step_up_auth: { + admin_mode: { + id_token: { + required: { + acr: 'gold' + } + } + }, + namespace: { + id_token: { + required: { + acr: 'gold' + } + } + } + } + ) + end + + let(:omniauth_provider_oidc_only_namespace) do + GitlabSettings::Options.new( + name: "openid_connect_aad", + step_up_auth: { + namespace: { + id_token: { + required: { + acr: 'gold' + } + } + } + } + ) + end + + before do + stub_omniauth_setting(enabled: true, providers: provider_configs) + allow(Devise).to receive(:omniauth_providers).and_return(provider_configs.map(&:name)) + end + + # rubocop:disable Layout/LineLength -- Avoid formatting to ensure one-line table syntax + where(:scope, :provider_configs, :expected_result) do + :admin_mode | [ref(:omniauth_provider_oidc)] | ['openid_connect'] + 'admin_mode' | [ref(:omniauth_provider_oidc)] | ['openid_connect'] + :admin_mode | [ref(:omniauth_provider_oidc_only_namespace)] | [] + :namespace | [ref(:omniauth_provider_oidc), ref(:omniauth_provider_oidc_only_namespace)] | %w[openid_connect openid_connect_aad] + 'namespace' | [ref(:omniauth_provider_oidc)] | ['openid_connect'] + :namespace | [ref(:omniauth_provider_oidc)] | ['openid_connect'] + :namespace | [] | [] + :unknown_scope | [ref(:omniauth_provider_oidc), ref(:omniauth_provider_oidc_only_namespace)] | [] + nil | [ref(:omniauth_provider_oidc), ref(:omniauth_provider_oidc_only_namespace)] | [] + end + # rubocop:enable Layout/LineLength + + with_them do + it { is_expected.to match_array(expected_result) } + end + end end diff --git a/spec/models/group_spec.rb b/spec/models/group_spec.rb index 3543a2c51958b9baea0670cb69a69872262042d0..f89868d9a3f5cb3d5e11b622fcd3c802c41b5435 100644 --- a/spec/models/group_spec.rb +++ b/spec/models/group_spec.rb @@ -262,6 +262,11 @@ it { is_expected.to include_module(Referable) } end + describe 'delegations' do + it { is_expected.to delegate_method(:step_up_auth_required_oauth_provider).to(:namespace_settings) } + it { is_expected.to delegate_method(:step_up_auth_required_oauth_provider=).to(:namespace_settings).with_arguments(:args) } + end + describe 'validations' do let_it_be(:private_organization) { create(:organization, :private) } let_it_be(:public_organization) { create(:organization, :public) } diff --git a/spec/models/namespace_setting_spec.rb b/spec/models/namespace_setting_spec.rb index 53aff3ac4356157b57978247d7ff0ef8824edd3d..f0b902acd333a3bbe34e1e6f28f514c176df479f 100644 --- a/spec/models/namespace_setting_spec.rb +++ b/spec/models/namespace_setting_spec.rb @@ -702,4 +702,39 @@ end end end + + describe '#step_up_auth_required_oauth_provider' do + subject { namespace_settings } + + context 'without omniauth provider configured for step-up authentication' do + it { is_expected.to validate_presence_of(:step_up_auth_required_oauth_provider).allow_nil } + it { is_expected.to validate_inclusion_of(:step_up_auth_required_oauth_provider).in_array([]).allow_nil } + it { is_expected.to nullify_if_blank(:step_up_auth_required_oauth_provider) } + end + + context 'with omniauth providers configured for step-up authentication' do + let(:ommiauth_provider_config_with_step_up_auth) do + GitlabSettings::Options.new( + name: "openid_connect", + step_up_auth: { + namespace: { + id_token: { + required: { acr: 'gold' } + } + } + } + ) + end + + before do + stub_omniauth_setting(enabled: true, providers: [ommiauth_provider_config_with_step_up_auth]) + end + + it { is_expected.to validate_inclusion_of(:step_up_auth_required_oauth_provider).in_array([ommiauth_provider_config_with_step_up_auth.name]) } + + it { is_expected.to allow_value(ommiauth_provider_config_with_step_up_auth.name).for(:step_up_auth_required_oauth_provider) } + it { is_expected.to allow_value('').for(:step_up_auth_required_oauth_provider) } + it { is_expected.not_to allow_value('google_oauth2').for(:step_up_auth_required_oauth_provider).with_message('is not included in the list') } + end + end end diff --git a/spec/services/groups/update_service_spec.rb b/spec/services/groups/update_service_spec.rb index d4eeddcdb3504b3a467b9ba3182e25a161e6f292..5b27f610d189b563e22890d90dfb49a2df3fe88f 100644 --- a/spec/services/groups/update_service_spec.rb +++ b/spec/services/groups/update_service_spec.rb @@ -573,6 +573,104 @@ end end + describe 'when updating namespace setting #step_up_auth_required_oauth_provider' do + let_it_be_with_reload(:group) { create(:group, :private) } + let_it_be(:user) { create(:user, owner_of: group) } + + let(:ommiauth_provider_config_oidc) do + GitlabSettings::Options.new( + name: 'openid_connect', + step_up_auth: { + namespace: { + id_token: { + required: { + acr: 'gold' + } + } + } + } + ) + end + + let(:ommiauth_provider_config_oidc_aad) do + GitlabSettings::Options.new( + name: 'openid_connect_aad', + step_up_auth: { + namespace: { + id_token: { + required: { + acr: 'gold' + } + } + } + } + ) + end + + let(:params) { {} } + + subject(:execute_update) { update_group(group, user, params) } + + before do + stub_omniauth_setting(enabled: true, providers: [ommiauth_provider_config_oidc, ommiauth_provider_config_oidc_aad]) + allow(Devise).to receive(:omniauth_providers).and_return([ommiauth_provider_config_oidc.name, ommiauth_provider_config_oidc_aad.name]) + end + + context 'when updating with valid provider' do + let(:params) { { step_up_auth_required_oauth_provider: 'openid_connect' } } + + it 'successfully updates the setting' do + expect(execute_update).to be_truthy + expect(group.reload.namespace_settings.step_up_auth_required_oauth_provider).to eq('openid_connect') + end + + context 'when feature flag is disabled' do + before do + stub_feature_flags(omniauth_step_up_auth_for_namespace: false) + end + + it 'does not update the setting when feature flag is disabled' do + expect(execute_update).to be_truthy + expect(group.reload.namespace_settings.step_up_auth_required_oauth_provider).to be_nil + end + end + end + + context 'when updating to disabled (nil)' do + let(:params) { { step_up_auth_required_oauth_provider: nil } } + + before do + group.namespace_settings.update!(step_up_auth_required_oauth_provider: 'openid_connect') + end + + it 'successfully disables step-up auth' do + expect(execute_update).to be_truthy + expect(group.reload.namespace_settings.step_up_auth_required_oauth_provider).to be_nil + end + end + + context 'when updating to disabled (empty string)' do + let(:params) { { step_up_auth_required_oauth_provider: '' } } + + before do + group.namespace_settings.update!(step_up_auth_required_oauth_provider: 'openid_connect') + end + + it 'successfully disables step-up auth' do + expect(execute_update).to be_truthy + expect(group.reload.namespace_settings.step_up_auth_required_oauth_provider).to be_nil + end + end + + context 'when updating with invalid provider' do + let(:params) { { step_up_auth_required_oauth_provider: 'invalid_provider' } } + + it 'fails to update with validation error' do + expect(execute_update).to be_falsey + end + end + end + context 'updating default_branch_protection' do let(:service) do described_class.new(internal_group, user, default_branch_protection: Gitlab::Access::PROTECTION_DEV_CAN_PUSH)