mirror of
https://github.com/Abdulazizzn/n8n-enterprise-unlocked.git
synced 2025-12-17 18:12:04 +00:00
refactor(core): Port SSO config (#17044)
This commit is contained in:
48
packages/@n8n/config/src/configs/sso.config.ts
Normal file
48
packages/@n8n/config/src/configs/sso.config.ts
Normal file
@@ -0,0 +1,48 @@
|
||||
import { Config, Env, Nested } from '../decorators';
|
||||
|
||||
@Config
|
||||
class SamlConfig {
|
||||
/** Whether to enable SAML SSO. */
|
||||
@Env('N8N_SSO_SAML_LOGIN_ENABLED')
|
||||
loginEnabled: boolean = false;
|
||||
|
||||
@Env('N8N_SSO_SAML_LOGIN_LABEL')
|
||||
loginLabel: string = '';
|
||||
}
|
||||
|
||||
@Config
|
||||
class OidcConfig {
|
||||
/** Whether to enable OIDC SSO. */
|
||||
@Env('N8N_SSO_OIDC_LOGIN_ENABLED')
|
||||
loginEnabled: boolean = false;
|
||||
}
|
||||
|
||||
@Config
|
||||
class LdapConfig {
|
||||
/** Whether to enable LDAP SSO. */
|
||||
@Env('N8N_SSO_LDAP_LOGIN_ENABLED')
|
||||
loginEnabled: boolean = false;
|
||||
|
||||
@Env('N8N_SSO_LDAP_LOGIN_LABEL')
|
||||
loginLabel: string = '';
|
||||
}
|
||||
|
||||
@Config
|
||||
export class SsoConfig {
|
||||
/** Whether to create users when they log in via SSO. */
|
||||
@Env('N8N_SSO_JUST_IN_TIME_PROVISIONING')
|
||||
justInTimeProvisioning: boolean = true;
|
||||
|
||||
/** Whether to redirect users from the login dialog to initialize SSO flow. */
|
||||
@Env('N8N_SSO_REDIRECT_LOGIN_TO_SSO')
|
||||
redirectLoginToSso: boolean = true;
|
||||
|
||||
@Nested
|
||||
saml: SamlConfig;
|
||||
|
||||
@Nested
|
||||
oidc: OidcConfig;
|
||||
|
||||
@Nested
|
||||
ldap: LdapConfig;
|
||||
}
|
||||
@@ -25,6 +25,7 @@ import { TaskRunnersConfig } from './configs/runners.config';
|
||||
import { ScalingModeConfig } from './configs/scaling-mode.config';
|
||||
import { SecurityConfig } from './configs/security.config';
|
||||
import { SentryConfig } from './configs/sentry.config';
|
||||
import { SsoConfig } from './configs/sso.config';
|
||||
import { TagsConfig } from './configs/tags.config';
|
||||
import { TemplatesConfig } from './configs/templates.config';
|
||||
import { UserManagementConfig } from './configs/user-management.config';
|
||||
@@ -167,6 +168,9 @@ export class GlobalConfig {
|
||||
@Nested
|
||||
personalization: PersonalizationConfig;
|
||||
|
||||
@Nested
|
||||
sso: SsoConfig;
|
||||
|
||||
/** Default locale for the UI. */
|
||||
@Env('N8N_DEFAULT_LOCALE')
|
||||
defaultLocale: string = 'en';
|
||||
|
||||
@@ -331,6 +331,21 @@ describe('GlobalConfig', () => {
|
||||
enabled: true,
|
||||
pruneTime: -1,
|
||||
},
|
||||
sso: {
|
||||
justInTimeProvisioning: true,
|
||||
redirectLoginToSso: true,
|
||||
saml: {
|
||||
loginEnabled: false,
|
||||
loginLabel: '',
|
||||
},
|
||||
oidc: {
|
||||
loginEnabled: false,
|
||||
},
|
||||
ldap: {
|
||||
loginEnabled: false,
|
||||
loginLabel: '',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
it('should use all default values when no env variables are defined', () => {
|
||||
|
||||
@@ -146,47 +146,6 @@ export const schema = {
|
||||
env: 'EXTERNAL_FRONTEND_HOOKS_URLS',
|
||||
},
|
||||
|
||||
sso: {
|
||||
justInTimeProvisioning: {
|
||||
format: Boolean,
|
||||
default: true,
|
||||
doc: 'Whether to automatically create users when they login via SSO.',
|
||||
},
|
||||
redirectLoginToSso: {
|
||||
format: Boolean,
|
||||
default: true,
|
||||
doc: 'Whether to automatically redirect users from login dialog to initialize SSO flow.',
|
||||
},
|
||||
saml: {
|
||||
loginEnabled: {
|
||||
format: Boolean,
|
||||
default: false,
|
||||
doc: 'Whether to enable SAML SSO.',
|
||||
},
|
||||
loginLabel: {
|
||||
format: String,
|
||||
default: '',
|
||||
},
|
||||
},
|
||||
oidc: {
|
||||
loginEnabled: {
|
||||
format: Boolean,
|
||||
default: false,
|
||||
doc: 'Whether to enable OIDC SSO.',
|
||||
},
|
||||
},
|
||||
ldap: {
|
||||
loginEnabled: {
|
||||
format: Boolean,
|
||||
default: false,
|
||||
},
|
||||
loginLabel: {
|
||||
format: String,
|
||||
default: '',
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
redis: {
|
||||
prefix: {
|
||||
doc: 'Prefix for all n8n related keys',
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import { mockLogger } from '@n8n/backend-test-utils';
|
||||
import { mockInstance } from '@n8n/backend-test-utils';
|
||||
import { mockLogger, mockInstance } from '@n8n/backend-test-utils';
|
||||
import { GlobalConfig } from '@n8n/config';
|
||||
import { LDAP_FEATURE_NAME, type LdapConfig } from '@n8n/constants';
|
||||
import type { Settings } from '@n8n/db';
|
||||
import { AuthIdentityRepository, SettingsRepository } from '@n8n/db';
|
||||
import { Container } from '@n8n/di';
|
||||
import { QueryFailedError } from '@n8n/typeorm';
|
||||
import { mock } from 'jest-mock-extended';
|
||||
import { Client } from 'ldapts';
|
||||
@@ -12,7 +13,7 @@ import { randomString } from 'n8n-workflow';
|
||||
import config from '@/config';
|
||||
import type { EventService } from '@/events/event.service';
|
||||
|
||||
import { BINARY_AD_ATTRIBUTES, LDAP_LOGIN_ENABLED, LDAP_LOGIN_LABEL } from '../constants';
|
||||
import { BINARY_AD_ATTRIBUTES } from '../constants';
|
||||
import {
|
||||
getLdapIds,
|
||||
createFilter,
|
||||
@@ -51,6 +52,15 @@ jest.mock('n8n-workflow', () => ({
|
||||
randomString: jest.fn(),
|
||||
}));
|
||||
|
||||
mockInstance(GlobalConfig, {
|
||||
sso: {
|
||||
ldap: {
|
||||
loginEnabled: true,
|
||||
loginLabel: 'fakeLoginLabel',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
describe('LdapService', () => {
|
||||
const ldapConfig: LdapConfig = {
|
||||
loginEnabled: true,
|
||||
@@ -74,7 +84,7 @@ describe('LdapService', () => {
|
||||
searchTimeout: 6,
|
||||
};
|
||||
|
||||
let settingsRepository = mockInstance(SettingsRepository);
|
||||
const settingsRepository = mockInstance(SettingsRepository);
|
||||
|
||||
beforeAll(() => {
|
||||
// Need fake timers to avoid setInterval
|
||||
@@ -116,13 +126,12 @@ describe('LdapService', () => {
|
||||
|
||||
await ldapService.init();
|
||||
|
||||
expect(configSetSpy).toHaveBeenNthCalledWith(1, LDAP_LOGIN_ENABLED, ldapConfig.loginEnabled);
|
||||
expect(configSetSpy).toHaveBeenNthCalledWith(
|
||||
2,
|
||||
1,
|
||||
'userManagement.authenticationMethod',
|
||||
'ldap',
|
||||
);
|
||||
expect(configSetSpy).toHaveBeenNthCalledWith(3, LDAP_LOGIN_LABEL, ldapConfig.loginLabel);
|
||||
expect(Container.get(GlobalConfig).sso.ldap.loginLabel).toBe(ldapConfig.loginLabel);
|
||||
});
|
||||
|
||||
it('should set expected configuration variables from LDAP config if LDAP is disabled', async () => {
|
||||
@@ -134,13 +143,12 @@ describe('LdapService', () => {
|
||||
|
||||
await ldapService.init();
|
||||
|
||||
expect(configSetSpy).toHaveBeenNthCalledWith(1, LDAP_LOGIN_ENABLED, givenConfig.loginEnabled);
|
||||
expect(configSetSpy).toHaveBeenNthCalledWith(
|
||||
2,
|
||||
1,
|
||||
'userManagement.authenticationMethod',
|
||||
'email',
|
||||
);
|
||||
expect(configSetSpy).toHaveBeenNthCalledWith(3, LDAP_LOGIN_LABEL, givenConfig.loginLabel);
|
||||
expect(Container.get(GlobalConfig).sso.ldap.loginLabel).toBe(ldapConfig.loginLabel);
|
||||
});
|
||||
|
||||
it('should show logger warning if authentication method is not ldap or email', async () => {
|
||||
@@ -356,17 +364,14 @@ describe('LdapService', () => {
|
||||
|
||||
it('should update the LDAP login label in the config', async () => {
|
||||
mockSettingsRespositoryFindOneByOrFail(ldapConfig);
|
||||
|
||||
mockInstance(AuthIdentityRepository, {
|
||||
find: jest.fn().mockResolvedValue([{ user: { id: 'userId' } }]),
|
||||
delete: jest.fn(),
|
||||
});
|
||||
|
||||
const cipherMock = mock<Cipher>({
|
||||
encrypt: jest.fn().mockReturnValue('encryptedPassword'),
|
||||
});
|
||||
const configSetSpy = jest.spyOn(config, 'set');
|
||||
|
||||
const globalConfig = Container.get(GlobalConfig);
|
||||
config.set('userManagement.authenticationMethod', 'email');
|
||||
const ldapService = new LdapService(mockLogger(), settingsRepository, cipherMock, mock());
|
||||
|
||||
@@ -377,8 +382,7 @@ describe('LdapService', () => {
|
||||
loginLabel: 'newLoginLabel',
|
||||
};
|
||||
await ldapService.updateConfig(newConfig);
|
||||
|
||||
expect(configSetSpy).toHaveBeenNthCalledWith(4, LDAP_LOGIN_LABEL, newConfig.loginLabel);
|
||||
expect(globalConfig.sso.ldap.loginLabel).toBe(newConfig.loginLabel);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { GlobalConfig } from '@n8n/config';
|
||||
import type { LdapConfig, ConnectionSecurity } from '@n8n/constants';
|
||||
import type { AuthProviderSyncHistory } from '@n8n/db';
|
||||
import {
|
||||
@@ -13,15 +14,9 @@ import type { Entry as LdapUser } from 'ldapts';
|
||||
import { Filter } from 'ldapts/filters/Filter';
|
||||
import { randomString } from 'n8n-workflow';
|
||||
|
||||
import config from '@/config';
|
||||
import { License } from '@/license';
|
||||
|
||||
import {
|
||||
BINARY_AD_ATTRIBUTES,
|
||||
LDAP_CONFIG_SCHEMA,
|
||||
LDAP_LOGIN_ENABLED,
|
||||
LDAP_LOGIN_LABEL,
|
||||
} from './constants';
|
||||
import { BINARY_AD_ATTRIBUTES, LDAP_CONFIG_SCHEMA } from './constants';
|
||||
|
||||
/**
|
||||
* Check whether the LDAP feature is disabled in the instance
|
||||
@@ -33,12 +28,12 @@ export const isLdapEnabled = () => {
|
||||
/**
|
||||
* Retrieve the LDAP login label from the configuration object
|
||||
*/
|
||||
export const getLdapLoginLabel = (): string => config.getEnv(LDAP_LOGIN_LABEL);
|
||||
export const getLdapLoginLabel = (): string => Container.get(GlobalConfig).sso.ldap.loginLabel;
|
||||
|
||||
/**
|
||||
* Retrieve the LDAP login enabled from the configuration object
|
||||
*/
|
||||
export const isLdapLoginEnabled = (): boolean => config.getEnv(LDAP_LOGIN_ENABLED);
|
||||
export const isLdapLoginEnabled = (): boolean => Container.get(GlobalConfig).sso.ldap.loginEnabled;
|
||||
|
||||
/**
|
||||
* Validate the structure of the LDAP configuration schema
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { Logger } from '@n8n/backend-common';
|
||||
import { GlobalConfig } from '@n8n/config';
|
||||
import type { LdapConfig } from '@n8n/constants';
|
||||
import { LDAP_FEATURE_NAME } from '@n8n/constants';
|
||||
import { SettingsRepository } from '@n8n/db';
|
||||
@@ -12,7 +13,6 @@ import { Cipher } from 'n8n-core';
|
||||
import { jsonParse, UnexpectedError } from 'n8n-workflow';
|
||||
import type { ConnectionOptions } from 'tls';
|
||||
|
||||
import config from '@/config';
|
||||
import { BadRequestError } from '@/errors/response-errors/bad-request.error';
|
||||
import { InternalServerError } from '@/errors/response-errors/internal-server.error';
|
||||
import { EventService } from '@/events/event.service';
|
||||
@@ -45,7 +45,7 @@ import {
|
||||
setCurrentAuthenticationMethod,
|
||||
} from '@/sso.ee/sso-helpers';
|
||||
|
||||
import { BINARY_AD_ATTRIBUTES, LDAP_LOGIN_ENABLED, LDAP_LOGIN_LABEL } from './constants';
|
||||
import { BINARY_AD_ATTRIBUTES } from './constants';
|
||||
|
||||
@Service()
|
||||
export class LdapService {
|
||||
@@ -141,7 +141,7 @@ export class LdapService {
|
||||
/** Take the LDAP configuration and set login enabled and login label to the config object */
|
||||
private async setGlobalLdapConfigVariables(ldapConfig: LdapConfig): Promise<void> {
|
||||
await this.setLdapLoginEnabled(ldapConfig.loginEnabled);
|
||||
config.set(LDAP_LOGIN_LABEL, ldapConfig.loginLabel);
|
||||
Container.get(GlobalConfig).sso.ldap.loginLabel = ldapConfig.loginLabel;
|
||||
}
|
||||
|
||||
/** Set the LDAP login enabled to the configuration object */
|
||||
@@ -153,7 +153,7 @@ export class LdapService {
|
||||
);
|
||||
}
|
||||
|
||||
config.set(LDAP_LOGIN_ENABLED, enabled);
|
||||
Container.get(GlobalConfig).sso.ldap.loginEnabled = enabled;
|
||||
|
||||
const targetAuthenticationMethod =
|
||||
!enabled && currentAuthenticationMethod === 'ldap' ? 'email' : currentAuthenticationMethod;
|
||||
|
||||
@@ -343,20 +343,20 @@ export class FrontendService {
|
||||
if (this.license.isLdapEnabled()) {
|
||||
Object.assign(this.settings.sso.ldap, {
|
||||
loginLabel: getLdapLoginLabel(),
|
||||
loginEnabled: config.getEnv('sso.ldap.loginEnabled'),
|
||||
loginEnabled: this.globalConfig.sso.ldap.loginEnabled,
|
||||
});
|
||||
}
|
||||
|
||||
if (this.license.isSamlEnabled()) {
|
||||
Object.assign(this.settings.sso.saml, {
|
||||
loginLabel: getSamlLoginLabel(),
|
||||
loginEnabled: config.getEnv('sso.saml.loginEnabled'),
|
||||
loginEnabled: this.globalConfig.sso.saml.loginEnabled,
|
||||
});
|
||||
}
|
||||
|
||||
if (this.licenseState.isOidcLicensed()) {
|
||||
Object.assign(this.settings.sso.oidc, {
|
||||
loginEnabled: config.getEnv('sso.oidc.loginEnabled'),
|
||||
loginEnabled: this.globalConfig.sso.oidc.loginEnabled,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -8,22 +8,17 @@ import {
|
||||
type User,
|
||||
UserRepository,
|
||||
} from '@n8n/db';
|
||||
import { Service } from '@n8n/di';
|
||||
import { Container, Service } from '@n8n/di';
|
||||
import { Cipher } from 'n8n-core';
|
||||
import { jsonParse } from 'n8n-workflow';
|
||||
import * as client from 'openid-client';
|
||||
|
||||
import config from '@/config';
|
||||
import { BadRequestError } from '@/errors/response-errors/bad-request.error';
|
||||
import { ForbiddenError } from '@/errors/response-errors/forbidden.error';
|
||||
import { InternalServerError } from '@/errors/response-errors/internal-server.error';
|
||||
import { UrlService } from '@/services/url.service';
|
||||
|
||||
import {
|
||||
OIDC_CLIENT_SECRET_REDACTED_VALUE,
|
||||
OIDC_LOGIN_ENABLED,
|
||||
OIDC_PREFERENCES_DB_KEY,
|
||||
} from './constants';
|
||||
import { OIDC_CLIENT_SECRET_REDACTED_VALUE, OIDC_PREFERENCES_DB_KEY } from './constants';
|
||||
import {
|
||||
getCurrentAuthenticationMethod,
|
||||
isEmailCurrentAuthenticationMethod,
|
||||
@@ -235,7 +230,7 @@ export class OidcService {
|
||||
const targetAuthenticationMethod =
|
||||
!enabled && currentAuthenticationMethod === 'oidc' ? 'email' : currentAuthenticationMethod;
|
||||
|
||||
config.set(OIDC_LOGIN_ENABLED, enabled);
|
||||
Container.get(GlobalConfig).sso.oidc.loginEnabled = enabled;
|
||||
await setCurrentAuthenticationMethod(enabled ? 'oidc' : targetAuthenticationMethod);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,17 +1,16 @@
|
||||
import type { SamlAcsDto, SamlPreferences } from '@n8n/api-types';
|
||||
import { GlobalConfig } from '@n8n/config';
|
||||
import type { User } from '@n8n/db';
|
||||
import { AuthIdentity, AuthIdentityRepository, UserRepository } from '@n8n/db';
|
||||
import { Container } from '@n8n/di';
|
||||
import { randomString } from 'n8n-workflow';
|
||||
import type { FlowResult } from 'samlify/types/src/flow';
|
||||
|
||||
import config from '@/config';
|
||||
import { AuthError } from '@/errors/response-errors/auth.error';
|
||||
import { InternalServerError } from '@/errors/response-errors/internal-server.error';
|
||||
import { License } from '@/license';
|
||||
import { PasswordUtility } from '@/services/password.utility';
|
||||
|
||||
import { SAML_LOGIN_ENABLED, SAML_LOGIN_LABEL } from './constants';
|
||||
import { getServiceProviderConfigTestReturnUrl } from './service-provider.ee';
|
||||
import type { SamlAttributeMapping, SamlUserAttributes } from './types';
|
||||
import {
|
||||
@@ -25,11 +24,11 @@ import {
|
||||
* Check whether the SAML feature is licensed and enabled in the instance
|
||||
*/
|
||||
export function isSamlLoginEnabled(): boolean {
|
||||
return config.getEnv(SAML_LOGIN_ENABLED);
|
||||
return Container.get(GlobalConfig).sso.saml.loginEnabled;
|
||||
}
|
||||
|
||||
export function getSamlLoginLabel(): string {
|
||||
return config.getEnv(SAML_LOGIN_LABEL);
|
||||
return Container.get(GlobalConfig).sso.saml.loginLabel;
|
||||
}
|
||||
|
||||
// can only toggle between email and saml, not directly to e.g. ldap
|
||||
@@ -44,12 +43,12 @@ export async function setSamlLoginEnabled(enabled: boolean): Promise<void> {
|
||||
const targetAuthenticationMethod =
|
||||
!enabled && currentAuthenticationMethod === 'saml' ? 'email' : currentAuthenticationMethod;
|
||||
|
||||
config.set(SAML_LOGIN_ENABLED, enabled);
|
||||
Container.get(GlobalConfig).sso.saml.loginEnabled = enabled;
|
||||
await setCurrentAuthenticationMethod(enabled ? 'saml' : targetAuthenticationMethod);
|
||||
}
|
||||
|
||||
export function setSamlLoginLabel(label: string): void {
|
||||
config.set(SAML_LOGIN_LABEL, label);
|
||||
Container.get(GlobalConfig).sso.saml.loginLabel = label;
|
||||
}
|
||||
|
||||
export function isSamlLicensed(): boolean {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { GlobalConfig } from '@n8n/config';
|
||||
import { SettingsRepository, type AuthProviderType } from '@n8n/db';
|
||||
import { Container } from '@n8n/di';
|
||||
|
||||
@@ -44,9 +45,9 @@ export function isEmailCurrentAuthenticationMethod(): boolean {
|
||||
}
|
||||
|
||||
export function isSsoJustInTimeProvisioningEnabled(): boolean {
|
||||
return config.getEnv('sso.justInTimeProvisioning');
|
||||
return Container.get(GlobalConfig).sso.justInTimeProvisioning;
|
||||
}
|
||||
|
||||
export function doRedirectUsersFromLoginToSsoFlow(): boolean {
|
||||
return config.getEnv('sso.redirectLoginToSso');
|
||||
return Container.get(GlobalConfig).sso.redirectLoginToSso;
|
||||
}
|
||||
|
||||
@@ -18,7 +18,10 @@ const testServer = utils.setupTestServer({ endpointGroups: ['me'] });
|
||||
|
||||
beforeEach(async () => {
|
||||
await testDb.truncate(['User']);
|
||||
mockInstance(GlobalConfig, { publicApi: { disabled: false } });
|
||||
mockInstance(GlobalConfig, {
|
||||
publicApi: { disabled: false },
|
||||
sso: { saml: { loginEnabled: true } },
|
||||
});
|
||||
});
|
||||
|
||||
describe('Owner shell', () => {
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import { randomEmail, randomName, randomValidPassword } from '@n8n/backend-test-utils';
|
||||
import { GlobalConfig } from '@n8n/config';
|
||||
import type { User } from '@n8n/db';
|
||||
import { Container } from '@n8n/di';
|
||||
|
||||
import { setSamlLoginEnabled } from '@/sso.ee/saml/saml-helpers';
|
||||
import {
|
||||
@@ -33,6 +35,7 @@ beforeAll(async () => {
|
||||
someUser = await createUser({ password: memberPassword });
|
||||
authOwnerAgent = testServer.authAgentFor(owner);
|
||||
authMemberAgent = testServer.authAgentFor(someUser);
|
||||
Container.get(GlobalConfig).sso.saml.loginEnabled = true;
|
||||
});
|
||||
|
||||
beforeEach(async () => await enableSaml(false));
|
||||
|
||||
Reference in New Issue
Block a user