From 31cc8de8297454cad4307e008b3b915475c0a889 Mon Sep 17 00:00:00 2001 From: Michael Auerswald Date: Thu, 2 Mar 2023 09:00:51 +0100 Subject: [PATCH] feat(core): Add SAML settings and consolidate LDAP under SSO (#5574) * consolidate SSO settings * update saml settings * fix type error --- packages/cli/src/Interfaces.ts | 12 ++++++--- packages/cli/src/Ldap/constants.ts | 4 +-- packages/cli/src/Server.ts | 23 ++++++++++++---- packages/cli/src/config/schema.ts | 26 +++++++++--------- packages/cli/src/sso/saml/constants.ts | 6 +++++ .../saml/middleware/samlEnabledMiddleware.ts | 4 +-- .../routes/saml.controller.protected.ee.ts | 18 +++++++------ packages/cli/src/sso/saml/saml.service.ee.ts | 27 ++++++++++--------- packages/cli/src/sso/saml/samlHelpers.ts | 27 ++++++++++++++++--- .../cli/src/sso/saml/types/samlPreferences.ts | 3 ++- packages/editor-ui/src/Interface.ts | 16 ++++++++--- packages/editor-ui/src/stores/settings.ts | 18 ++++++++++--- 12 files changed, 128 insertions(+), 56 deletions(-) diff --git a/packages/cli/src/Interfaces.ts b/packages/cli/src/Interfaces.ts index d1ea87ffac..24f7f1f755 100644 --- a/packages/cli/src/Interfaces.ts +++ b/packages/cli/src/Interfaces.ts @@ -488,9 +488,15 @@ export interface IN8nUISettings { personalizationSurveyEnabled: boolean; defaultLocale: string; userManagement: IUserManagementSettings; - ldap: { - loginLabel: string; - loginEnabled: boolean; + sso: { + saml: { + loginLabel: string; + loginEnabled: boolean; + }; + ldap: { + loginLabel: string; + loginEnabled: boolean; + }; }; publicApi: IPublicApiSettings; workflowTagsDisabled: boolean; diff --git a/packages/cli/src/Ldap/constants.ts b/packages/cli/src/Ldap/constants.ts index 630f4b6b77..3b7b369b80 100644 --- a/packages/cli/src/Ldap/constants.ts +++ b/packages/cli/src/Ldap/constants.ts @@ -4,9 +4,9 @@ export const LDAP_FEATURE_NAME = 'features.ldap'; export const LDAP_ENABLED = 'enterprise.features.ldap'; -export const LDAP_LOGIN_LABEL = 'ldap.loginLabel'; +export const LDAP_LOGIN_LABEL = 'sso.ldap.loginLabel'; -export const LDAP_LOGIN_ENABLED = 'ldap.loginEnabled'; +export const LDAP_LOGIN_ENABLED = 'sso.ldap.loginEnabled'; export const BINARY_AD_ATTRIBUTES = ['objectGUID', 'objectSid']; diff --git a/packages/cli/src/Server.ts b/packages/cli/src/Server.ts index 13cb901ac6..af3a5b5e56 100644 --- a/packages/cli/src/Server.ts +++ b/packages/cli/src/Server.ts @@ -145,7 +145,7 @@ import { eventBus } from './eventbus'; import { Container } from 'typedi'; import { InternalHooks } from './InternalHooks'; import { getStatusUsingPreviousExecutionStatusMethod } from './executions/executionHelpers'; -import { isSamlLicensed } from './sso/saml/samlHelpers'; +import { getSamlLoginLabel, isSamlLoginEnabled, isSamlLicensed } from './sso/saml/samlHelpers'; import { samlControllerPublic } from './sso/saml/routes/saml.controller.public.ee'; import { SamlService } from './sso/saml/saml.service.ee'; import { samlControllerProtected } from './sso/saml/routes/saml.controller.protected.ee'; @@ -258,9 +258,15 @@ class Server extends AbstractServer { config.getEnv('userManagement.skipInstanceOwnerSetup') === false, smtpSetup: isEmailSetUp(), }, - ldap: { - loginEnabled: false, - loginLabel: '', + sso: { + saml: { + loginEnabled: false, + loginLabel: '', + }, + ldap: { + loginEnabled: false, + loginLabel: '', + }, }, publicApi: { enabled: !config.getEnv('publicApi.disabled'), @@ -325,12 +331,19 @@ class Server extends AbstractServer { }); if (isLdapEnabled()) { - Object.assign(this.frontendSettings.ldap, { + Object.assign(this.frontendSettings.sso.ldap, { loginLabel: getLdapLoginLabel(), loginEnabled: isLdapLoginEnabled(), }); } + if (isSamlLicensed()) { + Object.assign(this.frontendSettings.sso.saml, { + loginLabel: getSamlLoginLabel(), + loginEnabled: isSamlLoginEnabled(), + }); + } + if (config.get('nodes.packagesMissing').length > 0) { this.frontendSettings.missingPackages = true; } diff --git a/packages/cli/src/config/schema.ts b/packages/cli/src/config/schema.ts index 78e728c33a..0ec5c9a623 100644 --- a/packages/cli/src/config/schema.ts +++ b/packages/cli/src/config/schema.ts @@ -1023,23 +1023,25 @@ export const schema = { doc: 'Whether to automatically redirect users from login dialog to initialize SSO flow.', }, saml: { - enabled: { + loginEnabled: { format: Boolean, default: false, doc: 'Whether to enable SAML SSO.', }, + loginLabel: { + format: String, + default: '', + }, }, - }, - - // TODO: move into sso settings - ldap: { - loginEnabled: { - format: Boolean, - default: false, - }, - loginLabel: { - format: String, - default: '', + ldap: { + loginEnabled: { + format: Boolean, + default: false, + }, + loginLabel: { + format: String, + default: '', + }, }, }, diff --git a/packages/cli/src/sso/saml/constants.ts b/packages/cli/src/sso/saml/constants.ts index 16565fa712..715c20b932 100644 --- a/packages/cli/src/sso/saml/constants.ts +++ b/packages/cli/src/sso/saml/constants.ts @@ -23,3 +23,9 @@ export class SamlUrls { } export const SAML_PREFERENCES_DB_KEY = 'features.saml'; + +export const SAML_ENTERPRISE_FEATURE_ENABLED = 'enterprise.features.saml'; + +export const SAML_LOGIN_LABEL = 'sso.saml.loginLabel'; + +export const SAML_LOGIN_ENABLED = 'sso.saml.loginEnabled'; diff --git a/packages/cli/src/sso/saml/middleware/samlEnabledMiddleware.ts b/packages/cli/src/sso/saml/middleware/samlEnabledMiddleware.ts index bcd1005e1f..bf15030d83 100644 --- a/packages/cli/src/sso/saml/middleware/samlEnabledMiddleware.ts +++ b/packages/cli/src/sso/saml/middleware/samlEnabledMiddleware.ts @@ -1,7 +1,7 @@ import type { RequestHandler } from 'express'; import type { AuthenticatedRequest } from '../../../requests'; import { isSamlCurrentAuthenticationMethod } from '../../ssoHelpers'; -import { isSamlEnabled, isSamlLicensed } from '../samlHelpers'; +import { isSamlLoginEnabled, isSamlLicensed } from '../samlHelpers'; export const samlLicensedOwnerMiddleware: RequestHandler = ( req: AuthenticatedRequest, @@ -16,7 +16,7 @@ export const samlLicensedOwnerMiddleware: RequestHandler = ( }; export const samlLicensedAndEnabledMiddleware: RequestHandler = (req, res, next) => { - if (isSamlEnabled() && isSamlLicensed() && isSamlCurrentAuthenticationMethod()) { + if (isSamlLoginEnabled() && isSamlLicensed() && isSamlCurrentAuthenticationMethod()) { next(); } else { res.status(401).json({ status: 'error', message: 'Unauthorized' }); diff --git a/packages/cli/src/sso/saml/routes/saml.controller.protected.ee.ts b/packages/cli/src/sso/saml/routes/saml.controller.protected.ee.ts index 9879cbe82c..5a4376f692 100644 --- a/packages/cli/src/sso/saml/routes/saml.controller.protected.ee.ts +++ b/packages/cli/src/sso/saml/routes/saml.controller.protected.ee.ts @@ -6,8 +6,9 @@ import { import { SamlService } from '../saml.service.ee'; import { SamlUrls } from '../constants'; import type { SamlConfiguration } from '../types/requests'; -import { AuthError } from '../../../ResponseHelper'; +import { AuthError, BadRequestError } from '@/ResponseHelper'; import { issueCookie } from '../../../auth/jwt'; +import { isSamlPreferences } from '../samlHelpers'; export const samlControllerProtected = express.Router(); @@ -18,8 +19,8 @@ export const samlControllerProtected = express.Router(); samlControllerProtected.get( SamlUrls.config, samlLicensedOwnerMiddleware, - async (req: SamlConfiguration.Read, res: express.Response) => { - const prefs = await SamlService.getInstance().getSamlPreferences(); + (req: SamlConfiguration.Read, res: express.Response) => { + const prefs = SamlService.getInstance().getSamlPreferences(); return res.send(prefs); }, ); @@ -32,11 +33,12 @@ samlControllerProtected.post( SamlUrls.config, samlLicensedOwnerMiddleware, async (req: SamlConfiguration.Update, res: express.Response) => { - const result = await SamlService.getInstance().setSamlPreferences({ - metadata: req.body.metadata, - mapping: req.body.mapping, - }); - return res.send(result); + if (isSamlPreferences(req.body)) { + const result = await SamlService.getInstance().setSamlPreferences(req.body); + return res.send(result); + } else { + throw new BadRequestError('Body is not a SamlPreferences object'); + } }, ); diff --git a/packages/cli/src/sso/saml/saml.service.ee.ts b/packages/cli/src/sso/saml/saml.service.ee.ts index b5a2fe637b..72dc3bf2c2 100644 --- a/packages/cli/src/sso/saml/saml.service.ee.ts +++ b/packages/cli/src/sso/saml/saml.service.ee.ts @@ -14,6 +14,10 @@ import { IdentityProvider } from 'samlify'; import { createUserFromSamlAttributes, getMappedSamlAttributesFromFlowResult, + getSamlLoginLabel, + isSamlLoginEnabled, + setSamlLoginEnabled, + setSamlLoginLabel, updateUserFromSamlAttributes, } from './samlHelpers'; @@ -142,16 +146,20 @@ export class SamlService { return undefined; } - async getSamlPreferences(): Promise { + getSamlPreferences(): SamlPreferences { return { mapping: this.attributeMapping, metadata: this.metadata, + loginEnabled: isSamlLoginEnabled(), + loginLabel: getSamlLoginLabel(), }; } async setSamlPreferences(prefs: SamlPreferences): Promise { this.attributeMapping = prefs.mapping; this.metadata = prefs.metadata; + setSamlLoginEnabled(prefs.loginEnabled); + setSamlLoginLabel(prefs.loginLabel); this.getIdentityProviderInstance(true); await this.saveSamlPreferences(); } @@ -163,10 +171,9 @@ export class SamlService { if (samlPreferences) { const prefs = jsonParse(samlPreferences.value); if (prefs) { - this.attributeMapping = prefs.mapping; - this.metadata = prefs.metadata; + await this.setSamlPreferences(prefs); + return prefs; } - return prefs; } return; } @@ -175,20 +182,14 @@ export class SamlService { const samlPreferences = await Db.collections.Settings.findOne({ where: { key: SAML_PREFERENCES_DB_KEY }, }); + const settingsValue = JSON.stringify(this.getSamlPreferences()); if (samlPreferences) { - samlPreferences.value = JSON.stringify({ - mapping: this.attributeMapping, - metadata: this.metadata, - }); - samlPreferences.loadOnStartup = true; + samlPreferences.value = settingsValue; await Db.collections.Settings.save(samlPreferences); } else { await Db.collections.Settings.save({ key: SAML_PREFERENCES_DB_KEY, - value: JSON.stringify({ - mapping: this.attributeMapping, - metadata: this.metadata, - }), + value: settingsValue, loadOnStartup: true, }); } diff --git a/packages/cli/src/sso/saml/samlHelpers.ts b/packages/cli/src/sso/saml/samlHelpers.ts index 20733e1fe5..c6c0c6c30f 100644 --- a/packages/cli/src/sso/saml/samlHelpers.ts +++ b/packages/cli/src/sso/saml/samlHelpers.ts @@ -9,24 +9,43 @@ import type { SamlPreferences } from './types/samlPreferences'; import type { SamlUserAttributes } from './types/samlUserAttributes'; import type { FlowResult } from 'samlify/types/src/flow'; import type { SamlAttributeMapping } from './types/samlAttributeMapping'; +import { SAML_ENTERPRISE_FEATURE_ENABLED, SAML_LOGIN_ENABLED, SAML_LOGIN_LABEL } from './constants'; /** * Check whether the SAML feature is licensed and enabled in the instance */ -export function isSamlEnabled(): boolean { - return config.getEnv('sso.saml.enabled'); +export function isSamlLoginEnabled(): boolean { + return config.getEnv(SAML_LOGIN_ENABLED); +} + +export function getSamlLoginLabel(): string { + return config.getEnv(SAML_LOGIN_LABEL); +} + +export function setSamlLoginEnabled(enabled: boolean): void { + config.set(SAML_LOGIN_ENABLED, enabled); +} + +export function setSamlLoginLabel(label: string): void { + config.set(SAML_LOGIN_LABEL, label); } export function isSamlLicensed(): boolean { const license = getLicense(); return ( isUserManagementEnabled() && - (license.isSamlEnabled() || config.getEnv('enterprise.features.saml')) + (license.isSamlEnabled() || config.getEnv(SAML_ENTERPRISE_FEATURE_ENABLED)) ); } export const isSamlPreferences = (candidate: unknown): candidate is SamlPreferences => { const o = candidate as SamlPreferences; - return typeof o === 'object' && typeof o.metadata === 'string' && typeof o.mapping === 'object'; + return ( + typeof o === 'object' && + typeof o.metadata === 'string' && + typeof o.mapping === 'object' && + o.mapping !== null && + o.loginEnabled !== undefined + ); }; export function generatePassword(): string { diff --git a/packages/cli/src/sso/saml/types/samlPreferences.ts b/packages/cli/src/sso/saml/types/samlPreferences.ts index d57f10b9ae..4edc58fc95 100644 --- a/packages/cli/src/sso/saml/types/samlPreferences.ts +++ b/packages/cli/src/sso/saml/types/samlPreferences.ts @@ -3,5 +3,6 @@ import type { SamlAttributeMapping } from './samlAttributeMapping'; export interface SamlPreferences { mapping: SamlAttributeMapping; metadata: string; - //TODO:SAML: add fields for separate SAML settins to generate metadata from + loginEnabled: boolean; + loginLabel: string; } diff --git a/packages/editor-ui/src/Interface.ts b/packages/editor-ui/src/Interface.ts index 6d56eaad97..8503e38759 100644 --- a/packages/editor-ui/src/Interface.ts +++ b/packages/editor-ui/src/Interface.ts @@ -766,9 +766,15 @@ export interface IN8nUISettings { enabled: boolean; }; }; - ldap: { - loginLabel: string; - loginEnabled: boolean; + sso: { + saml: { + loginLabel: string; + loginEnabled: boolean; + }; + ldap: { + loginLabel: string; + loginEnabled: boolean; + }; }; onboardingCallPromptEnabled: boolean; allowedModules: { @@ -1197,6 +1203,10 @@ export interface ISettingsState { loginLabel: string; loginEnabled: boolean; }; + saml: { + loginLabel: string; + loginEnabled: boolean; + }; onboardingCallPromptEnabled: boolean; saveDataErrorExecution: string; saveDataSuccessExecution: string; diff --git a/packages/editor-ui/src/stores/settings.ts b/packages/editor-ui/src/stores/settings.ts index 9fca2f63f9..07e374a636 100644 --- a/packages/editor-ui/src/stores/settings.ts +++ b/packages/editor-ui/src/stores/settings.ts @@ -24,7 +24,7 @@ import { WorkflowCallerPolicyDefaultOption, ILdapConfig, } from '@/Interface'; -import { ITelemetrySettings } from 'n8n-workflow'; +import { IDataObject, ITelemetrySettings } from 'n8n-workflow'; import { defineStore } from 'pinia'; import Vue from 'vue'; import { useRootStore } from './n8nRootStore'; @@ -54,6 +54,10 @@ export const useSettingsStore = defineStore(STORES.SETTINGS, { loginLabel: '', loginEnabled: false, }, + saml: { + loginLabel: '', + loginEnabled: false, + }, onboardingCallPromptEnabled: false, saveDataErrorExecution: 'all', saveDataSuccessExecution: 'all', @@ -87,6 +91,12 @@ export const useSettingsStore = defineStore(STORES.SETTINGS, { ldapLoginLabel(): string { return this.ldap.loginLabel; }, + isSamlLoginEnabled(): boolean { + return this.saml.loginEnabled; + }, + samlLoginLabel(): string { + return this.saml.loginLabel; + }, showSetupPage(): boolean { return this.userManagement.showSetupOnFirstLoad === true; }, @@ -168,8 +178,10 @@ export const useSettingsStore = defineStore(STORES.SETTINGS, { this.userManagement.smtpSetup = settings.userManagement.smtpSetup; this.api = settings.publicApi; this.onboardingCallPromptEnabled = settings.onboardingCallPromptEnabled; - this.ldap.loginEnabled = settings.ldap.loginEnabled; - this.ldap.loginLabel = settings.ldap.loginLabel; + this.ldap.loginEnabled = settings.sso.ldap.loginEnabled; + this.ldap.loginLabel = settings.sso.ldap.loginLabel; + this.saml.loginEnabled = settings.sso.saml.loginEnabled; + this.saml.loginLabel = settings.sso.saml.loginLabel; }, async getSettings(): Promise { const rootStore = useRootStore();