From 4ddd7089b3632db6fe326f956dc27fc74918f59d Mon Sep 17 00:00:00 2001 From: Konstantin Tieber <46342664+konstantintieber@users.noreply.github.com> Date: Fri, 21 Nov 2025 15:52:02 +0100 Subject: [PATCH] feat(core): Shorten copy text on confirm provisioning dialog (#22086) --- .../frontend/@n8n/i18n/src/locales/en.json | 49 ++-- .../sso/components/OidcSettingsForm.vue | 35 +-- .../sso/components/SamlSettingsForm.vue | 180 +++++++++------ .../components/ConfirmProvisioningDialog.vue | 211 ++++++++---------- .../UserRoleProvisioningDropdown.vue | 17 +- .../src/features/settings/sso/sso.store.ts | 6 +- .../settings/sso/styles/sso-form.module.scss | 5 + .../settings/sso/views/SettingsSso.test.ts | 71 ++++-- 8 files changed, 305 insertions(+), 269 deletions(-) diff --git a/packages/frontend/@n8n/i18n/src/locales/en.json b/packages/frontend/@n8n/i18n/src/locales/en.json index b3a6c620b56..84ed186bab1 100644 --- a/packages/frontend/@n8n/i18n/src/locales/en.json +++ b/packages/frontend/@n8n/i18n/src/locales/en.json @@ -2542,27 +2542,21 @@ "settings.provisioning.toggle": "Provision instance and project roles", "settings.provisioning.toggle.help": "Project access can only be defined on external provider. Any existing project access configured in n8n, but not on the provider, will be removed once a user logs in.", "settings.provisioningConfirmDialog.enable.title": "Enable user role provisioning", - "settings.provisioningConfirmDialog.disable.title": "Disable user role provisioning", - "settings.provisioningConfirmDialog.breakingChangeDescription.firstLine": "When you enable Just-in-time provisioning, your external SSO provider becomes the source of truth for all instance and project roles in n8n.", - "settings.provisioningConfirmDialog.breakingChangeDescription.list.one": "If your SSO provider doesn't specify a role for a member, we'll automatically assign the default role: global:member.", - "settings.provisioningConfirmDialog.breakingChangeDescription.list.two": "Any existing instance and project roles in n8n will be replaced by the roles defined in your SSO provider once the user logs in via SSO.", - "settings.provisioningConfirmDialog.breakingChangeRequiredSteps": "To enable you to migrate your current access settings to your SSO provider, download the two CSV files below. This step is mandatory before enabling JIT.", - "settings.provisioningConfirmDialog.disable.description": "You're switching instance role management back to n8n.", - "settings.provisioningConfirmDialog.disable.whatWillHappen": "What will happen:", - "settings.provisioningConfirmDialog.disable.list.one": "The SSO n8n_instance_role attribute will be ignored.", - "settings.provisioningConfirmDialog.disable.list.two": "Instance roles must be reassigned manually inside n8n.", - "settings.provisioningConfirmDialog.disable.beforeSaving": "Before saving, make sure:", - "settings.provisioningConfirmDialog.disable.checklist.one": "You are ready to reassign instance roles for all users inside n8n.", - "settings.provisioningConfirmDialog.disable.checklist.two": "You understand that role changes made in SSO will no longer be applied.", - "settings.provisioningConfirmDialog.enable.checkbox": "I have downloaded and reviewed the CSV export. My SSO provider is correctly configured to become the source of truth for user role provisioning on this n8n instance.", + "settings.provisioningConfirmDialog.disable.title": "Manage user role provisioning in n8n only", + "settings.provisioningConfirmDialog.breakingChangeDescription.firstSentence.partOne": "Your SSO provider will control all user roles in n8n.", + "settings.provisioningConfirmDialog.breakingChangeDescription.firstSentence.partOne.withProjectRoles": "Your SSO provider will control all user and project roles in n8n.", + "settings.provisioningConfirmDialog.breakingChangeDescription.firstSentence.partTwo": "Roles not assigned by your SSO provider will default to global:member.", + "settings.provisioningConfirmDialog.breakingChangeDescription.secondLine": "Before enabling: Download and review your current access settings below to ensure your SSO provider is configured correctly.", + "settings.provisioningConfirmDialog.disable.description": "You're switching instance role management from SSO back to n8n.", + "settings.provisioningConfirmDialog.enable.checkbox": "I have downloaded and reviewed the CSV export. My SSO provider is correctly configured to control user roles on this n8n instance.", "settings.provisioningConfirmDialog.disable.checkbox": "I confirm that I want to no longer provision user roles from my SSO provider.", "settings.provisioningConfirmDialog.link.docs": "Link to docs", "settings.provisioningConfirmDialog.button.enable.confirm": "Save and enable", - "settings.provisioningConfirmDialog.button.disable.confirm": "Save and disable", + "settings.provisioningConfirmDialog.button.disable.confirm": "Save", "settings.provisioningConfirmDialog.button.cancel": "Cancel", "settings.provisioningConfirmDialog.button.generateCsvExport": "Generate access settings CSV export", - "settings.provisioningConfirmDialog.button.downloadProjectRolesCsv": "Download existing project access settings csv", - "settings.provisioningConfirmDialog.button.downloadInstanceRolesCsv": "Download existing instance role settings csv", + "settings.provisioningConfirmDialog.button.downloadProjectRolesCsv": "Existing project access settings csv", + "settings.provisioningConfirmDialog.button.downloadInstanceRolesCsv": "Existing instance role settings csv", "settings.provisioningInstanceRolesHandledBySsoProvider.description": "User management and instance roles are controlled by your SSO provider. Contact your n8n instance owner or admin to make changes.", "settings.provisioningProjectRolesHandledBySsoProvider.description": "User management and project roles are controlled by your SSO provider. Contact your n8n instance owner or admin to make changes.", "settings.externalSecrets.title": "External Secrets", @@ -3470,9 +3464,7 @@ "settings.sso.subtitle": "SAML 2.0 Configuration", "settings.sso.info": "Activate SAML or OIDC to enable passwordless login via your existing user management tool and enhance security through unified authentication.", "settings.sso.info.link": "Learn how to configure SAML or OIDC.", - "settings.sso.activation.tooltip": "You need to save the settings first before activating SAML", - "settings.sso.activated": "Activated", - "settings.sso.deactivated": "Deactivated", + "settings.sso.activated": "Enable Single Sign On", "settings.sso.settings.redirectUrl.label": "Redirect URL", "settings.sso.settings.redirectUrl.copied": "Redirect URL copied to clipboard", "settings.sso.settings.redirectUrl.help": "Copy the Redirect URL to configure your SAML provider", @@ -3494,26 +3486,25 @@ "settings.sso.settings.userRoleProvisioning.label": "User role provisioning", "settings.sso.settings.userRoleProvisioning.help": "Manage instance and project roles from your SSO provider.", "settings.sso.settings.userRoleProvisioning.help.linkText": "Link to docs", - "settings.sso.settings.userRoleProvisioning.option.disabled.label": "Disabled", - "settings.sso.settings.userRoleProvisioning.option.disabled.description": "User and project roles are managed inside the n8n settings.", + "settings.sso.settings.userRoleProvisioning.option.disabled.label": "Managed in n8n", "settings.sso.settings.userRoleProvisioning.option.instanceRole.label": "Instance role", - "settings.sso.settings.userRoleProvisioning.option.instanceRole.description": "The instance role of a user is configured in the \"n8n_instance_role\" attribute on your SSO provider. If none is set on the SSO provider, the member role is used as fallback.", "settings.sso.settings.userRoleProvisioning.option.instanceAndProjectRoles.label": "Instance and project roles", - "settings.sso.settings.userRoleProvisioning.option.instanceAndProjectRoles.description": "The list of projects a user has access to is configured on the \"n8n_projects\" string array attribute on your SSO provider. Project access cannot be granted from within n8n.", "settings.sso.settings.test": "Test settings", "settings.sso.settings.save": "Save settings", - "settings.sso.settings.save.activate.title": "Test and activate SAML SSO", - "settings.sso.settings.save.activate.message": "SAML SSO configuration saved successfully. Test your SAML SSO settings first, then activate to enable single sign-on for your organization.", + "settings.sso.settings.save.testConnection.title": "Test and activate SAML SSO", + "settings.sso.settings.save.testConnection.message": "You are about to activate SSO via SAML. Test your SAML SSO settings first before proceeding.", + "settings.sso.settings.save.testConnection.test": "Test settings", + "settings.sso.settings.save.confirmTestConnection.title": "Confirm successful connection test", + "settings.sso.settings.save.confirmTestConnection.message": "Was the connection test successful? Confirm to activate SSO via SAML.", + "settings.sso.settings.save.confirmTestConnection.confirm": "Confirm", "settings.sso.settings.save.activate.cancel": "Cancel", - "settings.sso.settings.save.activate.test": "Test settings", "settings.sso.settings.save.error": "Error saving SAML SSO configuration", "settings.sso.settings.save.error_oidc": "Error saving OIDC SSO configuration", - "settings.sso.settings.footer.hint": "Don't forget to activate SAML SSO once you've saved the settings.", "settings.sso.actionBox.title": "Available on the Enterprise plan", "settings.sso.actionBox.description": "Use Single Sign On to consolidate authentication into a single platform to improve security and agility.", "settings.sso.actionBox.buttonText": "See plans", - "settings.oidc.confirmMessage.beforeSaveForm.headline": "Are you sure you want to disable OIDC login?", - "settings.oidc.confirmMessage.beforeSaveForm.message": "If you do so, all OIDC users will be converted to email users.", + "settings.sso.confirmMessage.beforeSaveForm.headline": "Are you sure you want to disable {protocol} login?", + "settings.sso.confirmMessage.beforeSaveForm.message": "If you do so, all {protocol} users will be converted to email users.", "settings.mfa.secret": "Secret {secret}", "settings.mfa": "MFA", "settings.mfa.title": "Multi-factor Authentication", diff --git a/packages/frontend/editor-ui/src/features/settings/sso/components/OidcSettingsForm.vue b/packages/frontend/editor-ui/src/features/settings/sso/components/OidcSettingsForm.vue index f7b3ac124a4..bbb69c10249 100644 --- a/packages/frontend/editor-ui/src/features/settings/sso/components/OidcSettingsForm.vue +++ b/packages/frontend/editor-ui/src/features/settings/sso/components/OidcSettingsForm.vue @@ -4,7 +4,7 @@ import { MODAL_CONFIRM } from '@/app/constants'; import { SupportedProtocols, useSSOStore } from '../sso.store'; import { useI18n } from '@n8n/i18n'; -import { ElSwitch } from 'element-plus'; +import { ElCheckbox } from 'element-plus'; import { N8nActionBox, N8nButton, N8nInput, N8nOption, N8nSelect } from '@n8n/design-system'; import { computed, onMounted, ref } from 'vue'; import { useToast } from '@/app/composables/useToast'; @@ -26,6 +26,8 @@ const toast = useToast(); const message = useMessage(); const pageRedirectionHelper = usePageRedirectionHelper(); +const savingForm = ref(false); + const discoveryEndpoint = ref(''); const clientId = ref(''); const clientSecret = ref(''); @@ -60,12 +62,6 @@ const promptDescriptions: PromptDescription[] = [ { label: i18n.baseText('settings.sso.settings.oidc.prompt.create'), value: 'create' }, ]; -const oidcActivatedLabel = computed(() => - ssoStore.isOidcLoginEnabled - ? i18n.baseText('settings.sso.activated') - : i18n.baseText('settings.sso.deactivated'), -); - const authenticationContextClassReference = ref(''); const getOidcConfig = async () => { @@ -114,8 +110,12 @@ const cannotSaveOidcSettings = computed(() => { async function onOidcSettingsSave(provisioningChangesConfirmed: boolean = false) { if (ssoStore.oidcConfig?.loginEnabled && !ssoStore.isOidcLoginEnabled) { const confirmAction = await message.confirm( - i18n.baseText('settings.oidc.confirmMessage.beforeSaveForm.message'), - i18n.baseText('settings.oidc.confirmMessage.beforeSaveForm.headline'), + i18n.baseText('settings.sso.confirmMessage.beforeSaveForm.message', { + interpolate: { protocol: 'OIDC' }, + }), + i18n.baseText('settings.sso.confirmMessage.beforeSaveForm.headline', { + interpolate: { protocol: 'OIDC' }, + }), { cancelButtonText: i18n.baseText( 'settings.ldap.confirmMessage.beforeSaveForm.cancelButtonText', @@ -139,6 +139,7 @@ async function onOidcSettingsSave(provisioningChangesConfirmed: boolean = false) .filter(Boolean); try { + savingForm.value = true; const newConfig = await ssoStore.saveOidcConfig({ clientId: clientId.value, clientSecret: clientSecret.value, @@ -163,6 +164,7 @@ async function onOidcSettingsSave(provisioningChangesConfirmed: boolean = false) toast.showError(error, i18n.baseText('settings.sso.settings.save.error_oidc')); return; } finally { + savingForm.value = false; await getOidcConfig(); } } @@ -252,6 +254,7 @@ onMounted(async () => { :new-provisioning-setting="userRoleProvisioning" auth-protocol="oidc" @confirm-provisioning="onOidcSettingsSave(true)" + @cancel="showUserRoleProvisioningDialog = false" />
@@ -267,20 +270,18 @@ onMounted(async () => { commas in order of preference.
-
- +
+ {{ + i18n.baseText('settings.sso.activated') + }}
{{ i18n.baseText('settings.sso.settings.save') }} diff --git a/packages/frontend/editor-ui/src/features/settings/sso/components/SamlSettingsForm.vue b/packages/frontend/editor-ui/src/features/settings/sso/components/SamlSettingsForm.vue index 192d68d485a..925131aa41a 100644 --- a/packages/frontend/editor-ui/src/features/settings/sso/components/SamlSettingsForm.vue +++ b/packages/frontend/editor-ui/src/features/settings/sso/components/SamlSettingsForm.vue @@ -5,8 +5,8 @@ import { SupportedProtocols, useSSOStore } from '../sso.store'; import { useI18n } from '@n8n/i18n'; import { captureMessage } from '@sentry/vue'; -import { ElSwitch } from 'element-plus'; -import { N8nActionBox, N8nButton, N8nInput, N8nRadioButtons, N8nTooltip } from '@n8n/design-system'; +import { ElCheckbox } from 'element-plus'; +import { N8nActionBox, N8nButton, N8nInput, N8nRadioButtons } from '@n8n/design-system'; import { useToast } from '@/app/composables/useToast'; import { usePageRedirectionHelper } from '@/app/composables/usePageRedirectionHelper'; import { useMessage } from '@/app/composables/useMessage'; @@ -18,6 +18,7 @@ import { useUserRoleProvisioningForm } from '../provisioning/composables/useUser import { useRootStore } from '@n8n/stores/useRootStore'; import { useTelemetry } from '@/app/composables/useTelemetry'; import ConfirmProvisioningDialog from '../provisioning/components/ConfirmProvisioningDialog.vue'; +import { MODAL_CONFIRM } from '@/app/constants/modals'; const i18n = useI18n(); const ssoStore = useSSOStore(); @@ -26,7 +27,10 @@ const toast = useToast(); const message = useMessage(); const pageRedirectionHelper = usePageRedirectionHelper(); +const savingForm = ref(false); + const redirectUrl = ref(); +const samlLoginEnabled = ref(false); const IdentityProviderSettingsType = { URL: 'url', @@ -45,17 +49,9 @@ const ipsOptions = ref([ ]); const ipsType = ref(IdentityProviderSettingsType.URL); -const ssoActivatedLabel = computed(() => - ssoStore.isSamlLoginEnabled - ? i18n.baseText('settings.sso.activated') - : i18n.baseText('settings.sso.deactivated'), -); - const metadataUrl = ref(); const metadata = ref(); -const ssoSettingsSaved = ref(false); - const entityId = ref(); const showUserRoleProvisioningDialog = ref(false); @@ -90,25 +86,32 @@ const getSamlConfig = async () => { metadata.value = config?.metadata; metadataUrl.value = config?.metadataUrl; - ssoSettingsSaved.value = !!config?.metadata; + samlLoginEnabled.value = config.loginEnabled ?? false; }; const isSaveEnabled = computed(() => { - if (isUserRoleProvisioningChanged()) { - return true; - } else if (ipsType.value === IdentityProviderSettingsType.URL) { - return !!metadataUrl.value && metadataUrl.value !== ssoStore.samlConfig?.metadataUrl; - } else if (ipsType.value === IdentityProviderSettingsType.XML) { - return !!metadata.value && metadata.value !== ssoStore.samlConfig?.metadata; + if (savingForm.value) { + return false; } - return false; + const isIdentityProviderChanged = () => { + if (ipsType.value === IdentityProviderSettingsType.URL) { + return !!metadataUrl.value && metadataUrl.value !== ssoStore.samlConfig?.metadataUrl; + } else if (ipsType.value === IdentityProviderSettingsType.XML) { + return !!metadata.value && metadata.value !== ssoStore.samlConfig?.metadata; + } + return false; + }; + const isSamlLoginEnabledChanged = ssoStore.isSamlLoginEnabled !== samlLoginEnabled.value; + return ( + isUserRoleProvisioningChanged() || isIdentityProviderChanged() || isSamlLoginEnabledChanged + ); }); const isTestEnabled = computed(() => { if (ipsType.value === IdentityProviderSettingsType.URL) { - return !!metadataUrl.value && ssoSettingsSaved.value; + return !!metadataUrl.value; } else if (ipsType.value === IdentityProviderSettingsType.XML) { - return !!metadata.value && ssoSettingsSaved.value; + return !!metadata.value; } return false; }); @@ -127,20 +130,94 @@ const sendTrackingEvent = (config?: SamlPreferences) => { telemetry.track('User updated single sign on settings', trackingMetadata); }; +const promptConfirmDisablingSamlLogin = async () => { + const confirmAction = await message.confirm( + i18n.baseText('settings.sso.confirmMessage.beforeSaveForm.message', { + interpolate: { protocol: 'SAML' }, + }), + i18n.baseText('settings.sso.confirmMessage.beforeSaveForm.headline', { + interpolate: { protocol: 'SAML' }, + }), + { + cancelButtonText: i18n.baseText( + 'settings.ldap.confirmMessage.beforeSaveForm.cancelButtonText', + ), + confirmButtonText: i18n.baseText( + 'settings.ldap.confirmMessage.beforeSaveForm.confirmButtonText', + ), + }, + ); + return confirmAction; +}; + +const prompTestSamlConnectionBeforeActivating = async () => { + const promptOpeningTestConnectionPage = await message.confirm( + i18n.baseText('settings.sso.settings.save.testConnection.message'), + i18n.baseText('settings.sso.settings.save.testConnection.title'), + { + confirmButtonText: i18n.baseText('settings.sso.settings.save.testConnection.test'), + cancelButtonText: i18n.baseText('settings.sso.settings.save.activate.cancel'), + }, + ); + + if (promptOpeningTestConnectionPage === MODAL_CONFIRM) { + await onTest(); + + const promptConfirmingSuccessfulTest = await message.confirm( + i18n.baseText('settings.sso.settings.save.confirmTestConnection.message'), + i18n.baseText('settings.sso.settings.save.confirmTestConnection.title'), + { + confirmButtonText: i18n.baseText( + 'settings.sso.settings.save.confirmTestConnection.confirm', + ), + cancelButtonText: i18n.baseText('settings.sso.settings.save.activate.cancel'), + }, + ); + return promptConfirmingSuccessfulTest; + } + return promptOpeningTestConnectionPage; +}; + const onSave = async (provisioningChangesConfirmed: boolean = false) => { try { + savingForm.value = true; validateSamlInput(); - if (isUserRoleProvisioningChanged() && !provisioningChangesConfirmed) { + const isDisablingSamlLogin = ssoStore.isSamlLoginEnabled && !samlLoginEnabled.value; + + if (isDisablingSamlLogin) { + const confirmDisablingSaml = await promptConfirmDisablingSamlLogin(); + if (confirmDisablingSaml !== MODAL_CONFIRM) { + return; + } + } + + if (!isDisablingSamlLogin && isUserRoleProvisioningChanged() && !provisioningChangesConfirmed) { showUserRoleProvisioningDialog.value = true; return; } - const config: Partial = + const metaDataConfig: Partial = ipsType.value === IdentityProviderSettingsType.URL ? { metadataUrl: metadataUrl.value } : { metadata: metadata.value }; - const configResponse = await ssoStore.saveSamlConfig(config); + + const isActivatingSamlLogin = !ssoStore.isSamlLoginEnabled && samlLoginEnabled.value; + + if (isActivatingSamlLogin) { + // metadata settings need to be saved for test to work + await ssoStore.saveSamlConfig(metaDataConfig); + + const confirmTest = await prompTestSamlConnectionBeforeActivating(); + if (confirmTest !== MODAL_CONFIRM) { + return; + } + } + + const configResponse = await ssoStore.saveSamlConfig({ + ...metaDataConfig, + loginEnabled: samlLoginEnabled.value, + }); if (isUserRoleProvisioningChanged()) { await saveProvisioningConfig(); @@ -149,30 +226,14 @@ const onSave = async (provisioningChangesConfirmed: boolean = false) => { // Update store with saved protocol selection ssoStore.selectedAuthProtocol = SupportedProtocols.SAML; - // Update store with saved metadata config - ssoStore.samlConfig!.metadata = config.metadata; - ssoStore.samlConfig!.metadataUrl = config.metadataUrl; - - if (!ssoStore.isSamlLoginEnabled) { - const answer = await message.confirm( - i18n.baseText('settings.sso.settings.save.activate.message'), - i18n.baseText('settings.sso.settings.save.activate.title'), - { - confirmButtonText: i18n.baseText('settings.sso.settings.save.activate.test'), - cancelButtonText: i18n.baseText('settings.sso.settings.save.activate.cancel'), - }, - ); - - if (answer === 'confirm') { - await onTest(); - } - } await getSamlConfig(); sendTrackingEvent(configResponse); } catch (error) { toast.showError(error, i18n.baseText('settings.sso.settings.save.error')); return; + } finally { + savingForm.value = false; } }; @@ -207,15 +268,6 @@ const validateSamlInput = () => { } }; -const isToggleSsoDisabled = computed(() => { - /** Allow users to disable SSO even if config request fails */ - if (ssoStore.isSamlLoginEnabled) { - return false; - } - - return !ssoSettingsSaved.value; -}); - const goToUpgrade = () => { void pageRedirectionHelper.goToUpgrade('sso', 'upgrade-sso'); }; @@ -276,30 +328,18 @@ onMounted(async () => { :new-provisioning-setting="userRoleProvisioning" auth-protocol="saml" @confirm-provisioning="onSave(true)" + @cancel="showUserRoleProvisioningDialog = false" /> -
- - - - +
+ {{ + i18n.baseText('settings.sso.activated') + }}
{ {{ i18n.baseText('settings.sso.settings.test') }}
- -
- {{ i18n.baseText('settings.sso.settings.footer.hint') }} -
import { useI18n } from '@n8n/i18n'; import { ElDialog } from 'element-plus'; -import { N8nButton, N8nCheckbox, N8nIcon, N8nText } from '@n8n/design-system'; +import { N8nButton, N8nCard, N8nCheckbox, N8nIcon, N8nText } from '@n8n/design-system'; import { ref, watch, computed } from 'vue'; import { useAccessSettingsCsvExport } from '@/features/settings/sso/provisioning/composables/useAccessSettingsCsvExport'; import type { UserRoleProvisioningSetting } from './UserRoleProvisioningDropdown.vue'; @@ -77,22 +77,23 @@ const onConfirmProvisioningSetting = () => { >
- - {{ - locale.baseText(`settings.provisioningConfirmDialog.${messagingKey}.checkbox`) - }} - + + + {{ + locale.baseText(`settings.provisioningConfirmDialog.${messagingKey}.checkbox`) + }} + +
diff --git a/packages/frontend/editor-ui/src/features/settings/sso/provisioning/components/UserRoleProvisioningDropdown.vue b/packages/frontend/editor-ui/src/features/settings/sso/provisioning/components/UserRoleProvisioningDropdown.vue index d8e364d1a87..54733267321 100644 --- a/packages/frontend/editor-ui/src/features/settings/sso/provisioning/components/UserRoleProvisioningDropdown.vue +++ b/packages/frontend/editor-ui/src/features/settings/sso/provisioning/components/UserRoleProvisioningDropdown.vue @@ -49,7 +49,6 @@ const getUserRoleProvisioningValueFromConfig = ( type UserRoleProvisioningDescription = { label: string; - description: string; value: UserRoleProvisioningSetting; }; @@ -57,25 +56,16 @@ const userRoleProvisioningDescriptions: UserRoleProvisioningDescription[] = [ { label: i18n.baseText('settings.sso.settings.userRoleProvisioning.option.disabled.label'), value: 'disabled', - description: i18n.baseText( - 'settings.sso.settings.userRoleProvisioning.option.disabled.description', - ), }, { label: i18n.baseText('settings.sso.settings.userRoleProvisioning.option.instanceRole.label'), value: 'instance_role', - description: i18n.baseText( - 'settings.sso.settings.userRoleProvisioning.option.instanceRole.description', - ), }, { label: i18n.baseText( 'settings.sso.settings.userRoleProvisioning.option.instanceAndProjectRoles.label', ), value: 'instance_and_project_roles', - description: i18n.baseText( - 'settings.sso.settings.userRoleProvisioning.option.instanceAndProjectRoles.description', - ), }, ]; @@ -104,12 +94,7 @@ onMounted(async () => { :label="option.label" data-test-id="oidc-user-role-provisioning-option" :value="option.value" - > -
-
{{ option.label }}
-
{{ option.description }}
-
- + /> {{ i18n.baseText('settings.sso.settings.userRoleProvisioning.help') }} diff --git a/packages/frontend/editor-ui/src/features/settings/sso/sso.store.ts b/packages/frontend/editor-ui/src/features/settings/sso/sso.store.ts index 59bbcc9cd75..0635ec72bb7 100644 --- a/packages/frontend/editor-ui/src/features/settings/sso/sso.store.ts +++ b/packages/frontend/editor-ui/src/features/settings/sso/sso.store.ts @@ -88,7 +88,6 @@ export const useSSOStore = defineStore('sso', () => { get: () => saml.value.loginEnabled, set: (value: boolean) => { saml.value.loginEnabled = value; - void toggleLoginEnabled(value); }, }); @@ -98,14 +97,13 @@ export const useSSOStore = defineStore('sso', () => { () => authenticationMethod.value === UserManagementAuthenticationMethod.Saml, ); - const toggleLoginEnabled = async (enabled: boolean) => - await ssoApi.toggleSamlConfig(rootStore.restApiContext, { loginEnabled: enabled }); - const getSamlMetadata = async () => await ssoApi.getSamlMetadata(rootStore.restApiContext); const getSamlConfig = async () => { const config = await ssoApi.getSamlConfig(rootStore.restApiContext); samlConfig.value = config; + saml.value.loginEnabled = config.loginEnabled; + saml.value.loginLabel = config.loginLabel; return config; }; diff --git a/packages/frontend/editor-ui/src/features/settings/sso/styles/sso-form.module.scss b/packages/frontend/editor-ui/src/features/settings/sso/styles/sso-form.module.scss index f90a1d7c2d3..fceb66e6557 100644 --- a/packages/frontend/editor-ui/src/features/settings/sso/styles/sso-form.module.scss +++ b/packages/frontend/editor-ui/src/features/settings/sso/styles/sso-form.module.scss @@ -38,6 +38,11 @@ } } +.checkboxGroup label > *:first-child { + // center checkbox next to label + vertical-align: text-top; +} + .actionBox { margin: var(--spacing--2xl) 0 0; } diff --git a/packages/frontend/editor-ui/src/features/settings/sso/views/SettingsSso.test.ts b/packages/frontend/editor-ui/src/features/settings/sso/views/SettingsSso.test.ts index d903e8fc1f5..861b429db40 100644 --- a/packages/frontend/editor-ui/src/features/settings/sso/views/SettingsSso.test.ts +++ b/packages/frontend/editor-ui/src/features/settings/sso/views/SettingsSso.test.ts @@ -121,31 +121,43 @@ describe('SettingsSso View', () => { const { getByTestId } = renderView(); const toggle = getByTestId('sso-toggle'); + const checkbox = toggle.querySelector('input[type="checkbox"]') as HTMLInputElement; - expect(toggle.textContent).toContain('Deactivated'); + expect(checkbox).not.toBeChecked(); await userEvent.click(toggle); - expect(toggle.textContent).toContain('Activated'); + expect(checkbox).toBeChecked(); await userEvent.click(toggle); - expect(toggle.textContent).toContain('Deactivated'); + expect(checkbox).not.toBeChecked(); }); it("allows user to fill Identity Provider's URL", async () => { - confirmMessage.mockResolvedValueOnce('confirm'); + // Mock two confirm dialogs: 1) test connection prompt, 2) confirm successful test + confirmMessage.mockResolvedValueOnce('confirm').mockResolvedValueOnce('confirm'); const windowOpenSpy = vi.spyOn(window, 'open'); ssoStore.isEnterpriseSamlEnabled = true; ssoStore.isEnterpriseOidcEnabled = true; ssoStore.isSamlLoginEnabled = false; - ssoStore.samlConfig = { ...samlConfig, metadataUrl: undefined, metadata: undefined }; + ssoStore.samlConfig = { + ...samlConfig, + metadataUrl: undefined, + metadata: undefined, + loginEnabled: false, + }; ssoStore.getSamlConfig.mockResolvedValue({ ...samlConfig, metadataUrl: undefined, metadata: undefined, + loginEnabled: false, + }); + ssoStore.saveSamlConfig.mockResolvedValue({ + ...samlConfig, + metadata: undefined, + loginEnabled: true, }); - ssoStore.saveSamlConfig.mockResolvedValue({ ...samlConfig, metadata: undefined }); ssoStore.testSamlConfig.mockResolvedValue('https://test-url.com'); const { getByTestId } = renderView(); @@ -158,11 +170,18 @@ describe('SettingsSso View', () => { expect(urlInput).toBeVisible(); await userEvent.type(urlInput, samlConfig.metadataUrl as string); + // Enable SSO toggle + const toggle = getByTestId('sso-toggle'); + await userEvent.click(toggle); + expect(saveButton).not.toBeDisabled(); await userEvent.click(saveButton); expect(ssoStore.saveSamlConfig).toHaveBeenCalledWith( - expect.objectContaining({ metadataUrl: samlConfig.metadataUrl }), + expect.objectContaining({ + metadataUrl: samlConfig.metadataUrl, + loginEnabled: true, + }), ); expect(ssoStore.testSamlConfig).toHaveBeenCalled(); @@ -177,7 +196,8 @@ describe('SettingsSso View', () => { }); it("allows user to fill Identity Provider's XML", async () => { - confirmMessage.mockResolvedValueOnce('confirm'); + // Mock two confirm dialogs: 1) test connection prompt, 2) confirm successful test + confirmMessage.mockResolvedValueOnce('confirm').mockResolvedValueOnce('confirm'); const windowOpenSpy = vi.spyOn(window, 'open'); @@ -185,8 +205,18 @@ describe('SettingsSso View', () => { ssoStore.isEnterpriseOidcEnabled = true; ssoStore.isSamlLoginEnabled = false; ssoStore.samlConfig = { ...samlConfig, metadataUrl: undefined, metadata: undefined }; + ssoStore.getSamlConfig.mockResolvedValue({ + ...samlConfig, + metadataUrl: undefined, + metadata: undefined, + loginEnabled: false, + }); // Mock should return config with metadata but WITHOUT metadataUrl (since user filled XML) - ssoStore.saveSamlConfig.mockResolvedValue({ ...samlConfig, metadataUrl: undefined }); + ssoStore.saveSamlConfig.mockResolvedValue({ + ...samlConfig, + metadataUrl: undefined, + loginEnabled: true, + }); ssoStore.testSamlConfig.mockResolvedValue('https://test-url.com'); const { getByTestId } = renderView(); @@ -201,11 +231,18 @@ describe('SettingsSso View', () => { expect(xmlInput).toBeVisible(); await userEvent.type(xmlInput, samlConfig.metadata!); + // Enable SSO toggle + const toggle = getByTestId('sso-toggle'); + await userEvent.click(toggle); + expect(saveButton).not.toBeDisabled(); await userEvent.click(saveButton); expect(ssoStore.saveSamlConfig).toHaveBeenCalledWith( - expect.objectContaining({ metadata: samlConfig.metadata }), + expect.objectContaining({ + metadata: samlConfig.metadata, + loginEnabled: true, + }), ); expect(ssoStore.testSamlConfig).toHaveBeenCalled(); @@ -281,19 +318,21 @@ describe('SettingsSso View', () => { ssoStore.isEnterpriseOidcEnabled = true; ssoStore.isOidcLoginEnabled = false; - const error = new Error('Request failed with status code 404'); - ssoStore.getSamlConfig.mockRejectedValue(error); + ssoStore.getSamlConfig.mockResolvedValue({ + ...samlConfig, + loginEnabled: true, + }); const { getByTestId } = renderView(); expect(ssoStore.getSamlConfig).toHaveBeenCalledTimes(1); await waitFor(async () => { - expect(showError).toHaveBeenCalledWith(error, 'error'); const toggle = getByTestId('sso-toggle'); - expect(toggle.textContent).toContain('Activated'); + const checkbox = toggle.querySelector('input[type="checkbox"]') as HTMLInputElement; + expect(checkbox).toBeChecked(); await userEvent.click(toggle); - expect(toggle.textContent).toContain('Deactivated'); + expect(checkbox).not.toBeChecked(); }); }); @@ -312,7 +351,7 @@ describe('SettingsSso View', () => { expect(container.querySelector('textarea[name="metadata"]')).toHaveValue(samlConfig.metadata); - expect(getByRole('switch')).toBeEnabled(); + expect(getByRole('checkbox')).toBeEnabled(); expect(getByTestId('sso-test')).toBeEnabled(); }); });