mirror of
https://github.com/n8n-io/n8n.git
synced 2025-02-21 02:56:40 -08:00
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:
parent
f61d779667
commit
31cc8de829
|
@ -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;
|
||||
|
|
|
@ -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'];
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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: '',
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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' });
|
||||
|
|
|
@ -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');
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
|
|
|
@ -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,
|
||||
});
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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();
|
||||
|
|
Loading…
Reference in a new issue