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
12 changed files with 128 additions and 56 deletions

View File

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

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

View File

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

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

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,10 +171,9 @@ 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 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;
}