feat(core): Add SAML settings and consolidate LDAP under SSO (#5574)

* consolidate SSO settings

* update saml settings

* fix type error
This commit is contained in:
Michael Auerswald 2023-03-02 09:00:51 +01:00 committed by GitHub
parent f61d779667
commit 31cc8de829
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
12 changed files with 128 additions and 56 deletions

View file

@ -488,10 +488,16 @@ export interface IN8nUISettings {
personalizationSurveyEnabled: boolean; personalizationSurveyEnabled: boolean;
defaultLocale: string; defaultLocale: string;
userManagement: IUserManagementSettings; userManagement: IUserManagementSettings;
sso: {
saml: {
loginLabel: string;
loginEnabled: boolean;
};
ldap: { ldap: {
loginLabel: string; loginLabel: string;
loginEnabled: boolean; loginEnabled: boolean;
}; };
};
publicApi: IPublicApiSettings; publicApi: IPublicApiSettings;
workflowTagsDisabled: boolean; workflowTagsDisabled: boolean;
logLevel: 'info' | 'debug' | 'warn' | 'error' | 'verbose' | 'silent'; logLevel: 'info' | 'debug' | 'warn' | 'error' | 'verbose' | 'silent';

View file

@ -4,9 +4,9 @@ export const LDAP_FEATURE_NAME = 'features.ldap';
export const LDAP_ENABLED = 'enterprise.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']; export const BINARY_AD_ATTRIBUTES = ['objectGUID', 'objectSid'];

View file

@ -145,7 +145,7 @@ import { eventBus } from './eventbus';
import { Container } from 'typedi'; import { Container } from 'typedi';
import { InternalHooks } from './InternalHooks'; import { InternalHooks } from './InternalHooks';
import { getStatusUsingPreviousExecutionStatusMethod } from './executions/executionHelpers'; 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 { samlControllerPublic } from './sso/saml/routes/saml.controller.public.ee';
import { SamlService } from './sso/saml/saml.service.ee'; import { SamlService } from './sso/saml/saml.service.ee';
import { samlControllerProtected } from './sso/saml/routes/saml.controller.protected.ee'; import { samlControllerProtected } from './sso/saml/routes/saml.controller.protected.ee';
@ -258,10 +258,16 @@ class Server extends AbstractServer {
config.getEnv('userManagement.skipInstanceOwnerSetup') === false, config.getEnv('userManagement.skipInstanceOwnerSetup') === false,
smtpSetup: isEmailSetUp(), smtpSetup: isEmailSetUp(),
}, },
sso: {
saml: {
loginEnabled: false,
loginLabel: '',
},
ldap: { ldap: {
loginEnabled: false, loginEnabled: false,
loginLabel: '', loginLabel: '',
}, },
},
publicApi: { publicApi: {
enabled: !config.getEnv('publicApi.disabled'), enabled: !config.getEnv('publicApi.disabled'),
latestVersion: 1, latestVersion: 1,
@ -325,12 +331,19 @@ class Server extends AbstractServer {
}); });
if (isLdapEnabled()) { if (isLdapEnabled()) {
Object.assign(this.frontendSettings.ldap, { Object.assign(this.frontendSettings.sso.ldap, {
loginLabel: getLdapLoginLabel(), loginLabel: getLdapLoginLabel(),
loginEnabled: isLdapLoginEnabled(), loginEnabled: isLdapLoginEnabled(),
}); });
} }
if (isSamlLicensed()) {
Object.assign(this.frontendSettings.sso.saml, {
loginLabel: getSamlLoginLabel(),
loginEnabled: isSamlLoginEnabled(),
});
}
if (config.get('nodes.packagesMissing').length > 0) { if (config.get('nodes.packagesMissing').length > 0) {
this.frontendSettings.missingPackages = true; this.frontendSettings.missingPackages = true;
} }

View file

@ -1023,15 +1023,16 @@ export const schema = {
doc: 'Whether to automatically redirect users from login dialog to initialize SSO flow.', doc: 'Whether to automatically redirect users from login dialog to initialize SSO flow.',
}, },
saml: { saml: {
enabled: { loginEnabled: {
format: Boolean, format: Boolean,
default: false, default: false,
doc: 'Whether to enable SAML SSO.', doc: 'Whether to enable SAML SSO.',
}, },
loginLabel: {
format: String,
default: '',
}, },
}, },
// TODO: move into sso settings
ldap: { ldap: {
loginEnabled: { loginEnabled: {
format: Boolean, format: Boolean,
@ -1042,6 +1043,7 @@ export const schema = {
default: '', default: '',
}, },
}, },
},
hiringBanner: { hiringBanner: {
enabled: { enabled: {

View file

@ -23,3 +23,9 @@ export class SamlUrls {
} }
export const SAML_PREFERENCES_DB_KEY = 'features.saml'; 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';

View file

@ -1,7 +1,7 @@
import type { RequestHandler } from 'express'; import type { RequestHandler } from 'express';
import type { AuthenticatedRequest } from '../../../requests'; import type { AuthenticatedRequest } from '../../../requests';
import { isSamlCurrentAuthenticationMethod } from '../../ssoHelpers'; import { isSamlCurrentAuthenticationMethod } from '../../ssoHelpers';
import { isSamlEnabled, isSamlLicensed } from '../samlHelpers'; import { isSamlLoginEnabled, isSamlLicensed } from '../samlHelpers';
export const samlLicensedOwnerMiddleware: RequestHandler = ( export const samlLicensedOwnerMiddleware: RequestHandler = (
req: AuthenticatedRequest, req: AuthenticatedRequest,
@ -16,7 +16,7 @@ export const samlLicensedOwnerMiddleware: RequestHandler = (
}; };
export const samlLicensedAndEnabledMiddleware: RequestHandler = (req, res, next) => { export const samlLicensedAndEnabledMiddleware: RequestHandler = (req, res, next) => {
if (isSamlEnabled() && isSamlLicensed() && isSamlCurrentAuthenticationMethod()) { if (isSamlLoginEnabled() && isSamlLicensed() && isSamlCurrentAuthenticationMethod()) {
next(); next();
} else { } else {
res.status(401).json({ status: 'error', message: 'Unauthorized' }); res.status(401).json({ status: 'error', message: 'Unauthorized' });

View file

@ -6,8 +6,9 @@ import {
import { SamlService } from '../saml.service.ee'; import { SamlService } from '../saml.service.ee';
import { SamlUrls } from '../constants'; import { SamlUrls } from '../constants';
import type { SamlConfiguration } from '../types/requests'; import type { SamlConfiguration } from '../types/requests';
import { AuthError } from '../../../ResponseHelper'; import { AuthError, BadRequestError } from '@/ResponseHelper';
import { issueCookie } from '../../../auth/jwt'; import { issueCookie } from '../../../auth/jwt';
import { isSamlPreferences } from '../samlHelpers';
export const samlControllerProtected = express.Router(); export const samlControllerProtected = express.Router();
@ -18,8 +19,8 @@ export const samlControllerProtected = express.Router();
samlControllerProtected.get( samlControllerProtected.get(
SamlUrls.config, SamlUrls.config,
samlLicensedOwnerMiddleware, samlLicensedOwnerMiddleware,
async (req: SamlConfiguration.Read, res: express.Response) => { (req: SamlConfiguration.Read, res: express.Response) => {
const prefs = await SamlService.getInstance().getSamlPreferences(); const prefs = SamlService.getInstance().getSamlPreferences();
return res.send(prefs); return res.send(prefs);
}, },
); );
@ -32,11 +33,12 @@ samlControllerProtected.post(
SamlUrls.config, SamlUrls.config,
samlLicensedOwnerMiddleware, samlLicensedOwnerMiddleware,
async (req: SamlConfiguration.Update, res: express.Response) => { async (req: SamlConfiguration.Update, res: express.Response) => {
const result = await SamlService.getInstance().setSamlPreferences({ if (isSamlPreferences(req.body)) {
metadata: req.body.metadata, const result = await SamlService.getInstance().setSamlPreferences(req.body);
mapping: req.body.mapping,
});
return res.send(result); return res.send(result);
} else {
throw new BadRequestError('Body is not a SamlPreferences object');
}
}, },
); );

View file

@ -14,6 +14,10 @@ import { IdentityProvider } from 'samlify';
import { import {
createUserFromSamlAttributes, createUserFromSamlAttributes,
getMappedSamlAttributesFromFlowResult, getMappedSamlAttributesFromFlowResult,
getSamlLoginLabel,
isSamlLoginEnabled,
setSamlLoginEnabled,
setSamlLoginLabel,
updateUserFromSamlAttributes, updateUserFromSamlAttributes,
} from './samlHelpers'; } from './samlHelpers';
@ -142,16 +146,20 @@ export class SamlService {
return undefined; return undefined;
} }
async getSamlPreferences(): Promise<SamlPreferences> { getSamlPreferences(): SamlPreferences {
return { return {
mapping: this.attributeMapping, mapping: this.attributeMapping,
metadata: this.metadata, metadata: this.metadata,
loginEnabled: isSamlLoginEnabled(),
loginLabel: getSamlLoginLabel(),
}; };
} }
async setSamlPreferences(prefs: SamlPreferences): Promise<void> { async setSamlPreferences(prefs: SamlPreferences): Promise<void> {
this.attributeMapping = prefs.mapping; this.attributeMapping = prefs.mapping;
this.metadata = prefs.metadata; this.metadata = prefs.metadata;
setSamlLoginEnabled(prefs.loginEnabled);
setSamlLoginLabel(prefs.loginLabel);
this.getIdentityProviderInstance(true); this.getIdentityProviderInstance(true);
await this.saveSamlPreferences(); await this.saveSamlPreferences();
} }
@ -163,11 +171,10 @@ export class SamlService {
if (samlPreferences) { if (samlPreferences) {
const prefs = jsonParse<SamlPreferences>(samlPreferences.value); const prefs = jsonParse<SamlPreferences>(samlPreferences.value);
if (prefs) { if (prefs) {
this.attributeMapping = prefs.mapping; await this.setSamlPreferences(prefs);
this.metadata = prefs.metadata;
}
return prefs; return prefs;
} }
}
return; return;
} }
@ -175,20 +182,14 @@ export class SamlService {
const samlPreferences = await Db.collections.Settings.findOne({ const samlPreferences = await Db.collections.Settings.findOne({
where: { key: SAML_PREFERENCES_DB_KEY }, where: { key: SAML_PREFERENCES_DB_KEY },
}); });
const settingsValue = JSON.stringify(this.getSamlPreferences());
if (samlPreferences) { if (samlPreferences) {
samlPreferences.value = JSON.stringify({ samlPreferences.value = settingsValue;
mapping: this.attributeMapping,
metadata: this.metadata,
});
samlPreferences.loadOnStartup = true;
await Db.collections.Settings.save(samlPreferences); await Db.collections.Settings.save(samlPreferences);
} else { } else {
await Db.collections.Settings.save({ await Db.collections.Settings.save({
key: SAML_PREFERENCES_DB_KEY, key: SAML_PREFERENCES_DB_KEY,
value: JSON.stringify({ value: settingsValue,
mapping: this.attributeMapping,
metadata: this.metadata,
}),
loadOnStartup: true, loadOnStartup: true,
}); });
} }

View file

@ -9,24 +9,43 @@ import type { SamlPreferences } from './types/samlPreferences';
import type { SamlUserAttributes } from './types/samlUserAttributes'; import type { SamlUserAttributes } from './types/samlUserAttributes';
import type { FlowResult } from 'samlify/types/src/flow'; import type { FlowResult } from 'samlify/types/src/flow';
import type { SamlAttributeMapping } from './types/samlAttributeMapping'; 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 * Check whether the SAML feature is licensed and enabled in the instance
*/ */
export function isSamlEnabled(): boolean { export function isSamlLoginEnabled(): boolean {
return config.getEnv('sso.saml.enabled'); 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 { export function isSamlLicensed(): boolean {
const license = getLicense(); const license = getLicense();
return ( return (
isUserManagementEnabled() && isUserManagementEnabled() &&
(license.isSamlEnabled() || config.getEnv('enterprise.features.saml')) (license.isSamlEnabled() || config.getEnv(SAML_ENTERPRISE_FEATURE_ENABLED))
); );
} }
export const isSamlPreferences = (candidate: unknown): candidate is SamlPreferences => { export const isSamlPreferences = (candidate: unknown): candidate is SamlPreferences => {
const o = candidate as 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 { export function generatePassword(): string {

View file

@ -3,5 +3,6 @@ import type { SamlAttributeMapping } from './samlAttributeMapping';
export interface SamlPreferences { export interface SamlPreferences {
mapping: SamlAttributeMapping; mapping: SamlAttributeMapping;
metadata: string; metadata: string;
//TODO:SAML: add fields for separate SAML settins to generate metadata from loginEnabled: boolean;
loginLabel: string;
} }

View file

@ -766,10 +766,16 @@ export interface IN8nUISettings {
enabled: boolean; enabled: boolean;
}; };
}; };
sso: {
saml: {
loginLabel: string;
loginEnabled: boolean;
};
ldap: { ldap: {
loginLabel: string; loginLabel: string;
loginEnabled: boolean; loginEnabled: boolean;
}; };
};
onboardingCallPromptEnabled: boolean; onboardingCallPromptEnabled: boolean;
allowedModules: { allowedModules: {
builtIn?: string[]; builtIn?: string[];
@ -1197,6 +1203,10 @@ export interface ISettingsState {
loginLabel: string; loginLabel: string;
loginEnabled: boolean; loginEnabled: boolean;
}; };
saml: {
loginLabel: string;
loginEnabled: boolean;
};
onboardingCallPromptEnabled: boolean; onboardingCallPromptEnabled: boolean;
saveDataErrorExecution: string; saveDataErrorExecution: string;
saveDataSuccessExecution: string; saveDataSuccessExecution: string;

View file

@ -24,7 +24,7 @@ import {
WorkflowCallerPolicyDefaultOption, WorkflowCallerPolicyDefaultOption,
ILdapConfig, ILdapConfig,
} from '@/Interface'; } from '@/Interface';
import { ITelemetrySettings } from 'n8n-workflow'; import { IDataObject, ITelemetrySettings } from 'n8n-workflow';
import { defineStore } from 'pinia'; import { defineStore } from 'pinia';
import Vue from 'vue'; import Vue from 'vue';
import { useRootStore } from './n8nRootStore'; import { useRootStore } from './n8nRootStore';
@ -54,6 +54,10 @@ export const useSettingsStore = defineStore(STORES.SETTINGS, {
loginLabel: '', loginLabel: '',
loginEnabled: false, loginEnabled: false,
}, },
saml: {
loginLabel: '',
loginEnabled: false,
},
onboardingCallPromptEnabled: false, onboardingCallPromptEnabled: false,
saveDataErrorExecution: 'all', saveDataErrorExecution: 'all',
saveDataSuccessExecution: 'all', saveDataSuccessExecution: 'all',
@ -87,6 +91,12 @@ export const useSettingsStore = defineStore(STORES.SETTINGS, {
ldapLoginLabel(): string { ldapLoginLabel(): string {
return this.ldap.loginLabel; return this.ldap.loginLabel;
}, },
isSamlLoginEnabled(): boolean {
return this.saml.loginEnabled;
},
samlLoginLabel(): string {
return this.saml.loginLabel;
},
showSetupPage(): boolean { showSetupPage(): boolean {
return this.userManagement.showSetupOnFirstLoad === true; return this.userManagement.showSetupOnFirstLoad === true;
}, },
@ -168,8 +178,10 @@ export const useSettingsStore = defineStore(STORES.SETTINGS, {
this.userManagement.smtpSetup = settings.userManagement.smtpSetup; this.userManagement.smtpSetup = settings.userManagement.smtpSetup;
this.api = settings.publicApi; this.api = settings.publicApi;
this.onboardingCallPromptEnabled = settings.onboardingCallPromptEnabled; this.onboardingCallPromptEnabled = settings.onboardingCallPromptEnabled;
this.ldap.loginEnabled = settings.ldap.loginEnabled; this.ldap.loginEnabled = settings.sso.ldap.loginEnabled;
this.ldap.loginLabel = settings.ldap.loginLabel; this.ldap.loginLabel = settings.sso.ldap.loginLabel;
this.saml.loginEnabled = settings.sso.saml.loginEnabled;
this.saml.loginLabel = settings.sso.saml.loginLabel;
}, },
async getSettings(): Promise<void> { async getSettings(): Promise<void> {
const rootStore = useRootStore(); const rootStore = useRootStore();