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;
defaultLocale: string;
userManagement: IUserManagementSettings;
sso: {
saml: {
loginLabel: string;
loginEnabled: boolean;
};
ldap: {
loginLabel: string;
loginEnabled: boolean;
};
};
publicApi: IPublicApiSettings;
workflowTagsDisabled: boolean;
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_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'];

View file

@ -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,10 +258,16 @@ class Server extends AbstractServer {
config.getEnv('userManagement.skipInstanceOwnerSetup') === false,
smtpSetup: isEmailSetUp(),
},
sso: {
saml: {
loginEnabled: false,
loginLabel: '',
},
ldap: {
loginEnabled: false,
loginLabel: '',
},
},
publicApi: {
enabled: !config.getEnv('publicApi.disabled'),
latestVersion: 1,
@ -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;
}

View file

@ -1023,15 +1023,16 @@ 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,
@ -1042,6 +1043,7 @@ export const schema = {
default: '',
},
},
},
hiringBanner: {
enabled: {

View file

@ -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';

View file

@ -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' });

View file

@ -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,
});
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');
}
},
);

View file

@ -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<SamlPreferences> {
getSamlPreferences(): SamlPreferences {
return {
mapping: this.attributeMapping,
metadata: this.metadata,
loginEnabled: isSamlLoginEnabled(),
loginLabel: getSamlLoginLabel(),
};
}
async setSamlPreferences(prefs: SamlPreferences): Promise<void> {
this.attributeMapping = prefs.mapping;
this.metadata = prefs.metadata;
setSamlLoginEnabled(prefs.loginEnabled);
setSamlLoginLabel(prefs.loginLabel);
this.getIdentityProviderInstance(true);
await this.saveSamlPreferences();
}
@ -163,11 +171,10 @@ export class SamlService {
if (samlPreferences) {
const prefs = jsonParse<SamlPreferences>(samlPreferences.value);
if (prefs) {
this.attributeMapping = prefs.mapping;
this.metadata = prefs.metadata;
}
await this.setSamlPreferences(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,
});
}

View file

@ -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 {

View file

@ -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;
}

View file

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

View file

@ -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<void> {
const rootStore = useRootStore();