refactor(core)!: Remove basic-auth, external-jwt-auth, and no-auth options (#6362)
Co-authored-by: कारतोफ्फेलस्क्रिप्ट™ <aditya@netroy.in>
This commit is contained in:
committed by
कारतोफ्फेलस्क्रिप्ट™
parent
a45a2c8c41
commit
8c008f5d22
@@ -306,7 +306,6 @@ export interface IDiagnosticInfo {
|
||||
databaseType: DatabaseType;
|
||||
notificationsEnabled: boolean;
|
||||
disableProductionWebhooksOnMainProcess: boolean;
|
||||
basicAuthActive: boolean;
|
||||
systemInfo: {
|
||||
os: {
|
||||
type?: string;
|
||||
@@ -324,7 +323,6 @@ export interface IDiagnosticInfo {
|
||||
};
|
||||
deploymentType: string;
|
||||
binaryDataMode: string;
|
||||
n8n_multi_user_allowed: boolean;
|
||||
smtp_set_up: boolean;
|
||||
ldap_allowed: boolean;
|
||||
saml_enabled: boolean;
|
||||
|
||||
@@ -75,12 +75,10 @@ export class InternalHooks implements IInternalHooksClass {
|
||||
db_type: diagnosticInfo.databaseType,
|
||||
n8n_version_notifications_enabled: diagnosticInfo.notificationsEnabled,
|
||||
n8n_disable_production_main_process: diagnosticInfo.disableProductionWebhooksOnMainProcess,
|
||||
n8n_basic_auth_active: diagnosticInfo.basicAuthActive,
|
||||
system_info: diagnosticInfo.systemInfo,
|
||||
execution_variables: diagnosticInfo.executionVariables,
|
||||
n8n_deployment_type: diagnosticInfo.deploymentType,
|
||||
n8n_binary_data_mode: diagnosticInfo.binaryDataMode,
|
||||
n8n_multi_user_allowed: diagnosticInfo.n8n_multi_user_allowed,
|
||||
smtp_set_up: diagnosticInfo.smtp_set_up,
|
||||
ldap_allowed: diagnosticInfo.ldap_allowed,
|
||||
saml_enabled: diagnosticInfo.saml_enabled,
|
||||
|
||||
@@ -12,7 +12,6 @@ import { User } from '@db/entities/User';
|
||||
import { AuthIdentity } from '@db/entities/AuthIdentity';
|
||||
import { RoleRepository } from '@db/repositories';
|
||||
import type { AuthProviderSyncHistory } from '@db/entities/AuthProviderSyncHistory';
|
||||
import { isUserManagementEnabled } from '@/UserManagement/UserManagementHelper';
|
||||
import { LdapManager } from './LdapManager.ee';
|
||||
|
||||
import {
|
||||
@@ -37,10 +36,7 @@ import { InternalServerError } from '../ResponseHelper';
|
||||
/**
|
||||
* Check whether the LDAP feature is disabled in the instance
|
||||
*/
|
||||
export const isLdapEnabled = (): boolean => {
|
||||
const license = Container.get(License);
|
||||
return isUserManagementEnabled() && license.isLdapEnabled();
|
||||
};
|
||||
export const isLdapEnabled = () => Container.get(License).isLdapEnabled();
|
||||
|
||||
/**
|
||||
* Check whether the LDAP feature is enabled in the instance
|
||||
|
||||
@@ -87,17 +87,6 @@ export class ServiceUnavailableError extends ResponseError {
|
||||
}
|
||||
}
|
||||
|
||||
export function basicAuthAuthorizationError(resp: Response, realm: string, message?: string) {
|
||||
resp.statusCode = 401;
|
||||
resp.setHeader('WWW-Authenticate', `Basic realm="${realm}"`);
|
||||
resp.json({ code: resp.statusCode, message });
|
||||
}
|
||||
|
||||
export function jwtAuthAuthorizationError(resp: Response, message?: string) {
|
||||
resp.statusCode = 403;
|
||||
resp.json({ code: resp.statusCode, message });
|
||||
}
|
||||
|
||||
export function sendSuccessResponse(
|
||||
res: Response,
|
||||
data: any,
|
||||
|
||||
@@ -105,7 +105,6 @@ import {
|
||||
getInstanceBaseUrl,
|
||||
isEmailSetUp,
|
||||
isSharingEnabled,
|
||||
isUserManagementEnabled,
|
||||
whereClause,
|
||||
} from '@/UserManagement/UserManagementHelper';
|
||||
import { UserManagementMailer } from '@/UserManagement/email';
|
||||
@@ -145,8 +144,6 @@ import {
|
||||
} from './Ldap/helpers';
|
||||
import { AbstractServer } from './AbstractServer';
|
||||
import { configureMetrics } from './metrics';
|
||||
import { setupBasicAuth } from './middlewares/basicAuth';
|
||||
import { setupExternalJWTAuth } from './middlewares/externalJWTAuth';
|
||||
import { PostHogClient } from './posthog';
|
||||
import { eventBus } from './eventbus';
|
||||
import { Container } from 'typedi';
|
||||
@@ -261,11 +258,7 @@ export class Server extends AbstractServer {
|
||||
config.getEnv('personalization.enabled') && config.getEnv('diagnostics.enabled'),
|
||||
defaultLocale: config.getEnv('defaultLocale'),
|
||||
userManagement: {
|
||||
enabled: isUserManagementEnabled(),
|
||||
showSetupOnFirstLoad:
|
||||
config.getEnv('userManagement.disabled') === false &&
|
||||
config.getEnv('userManagement.isInstanceOwnerSetUp') === false &&
|
||||
config.getEnv('userManagement.skipInstanceOwnerSetup') === false,
|
||||
showSetupOnFirstLoad: config.getEnv('userManagement.isInstanceOwnerSetUp') === false,
|
||||
smtpSetup: isEmailSetUp(),
|
||||
authenticationMethod: getCurrentAuthenticationMethod(),
|
||||
},
|
||||
@@ -349,7 +342,6 @@ export class Server extends AbstractServer {
|
||||
const cpus = os.cpus();
|
||||
const binaryDataConfig = config.getEnv('binaryDataManager');
|
||||
const diagnosticInfo: IDiagnosticInfo = {
|
||||
basicAuthActive: config.getEnv('security.basicAuth.active'),
|
||||
databaseType: config.getEnv('database.type'),
|
||||
disableProductionWebhooksOnMainProcess: config.getEnv(
|
||||
'endpoints.disableProductionWebhooksOnMainProcess',
|
||||
@@ -385,7 +377,6 @@ export class Server extends AbstractServer {
|
||||
},
|
||||
deploymentType: config.getEnv('deployment.type'),
|
||||
binaryDataMode: binaryDataConfig.mode,
|
||||
n8n_multi_user_allowed: isUserManagementEnabled(),
|
||||
smtp_set_up: config.getEnv('userManagement.emails.mode') === 'smtp',
|
||||
ldap_allowed: isLdapCurrentAuthenticationMethod(),
|
||||
saml_enabled: isSamlCurrentAuthenticationMethod(),
|
||||
@@ -414,12 +405,9 @@ export class Server extends AbstractServer {
|
||||
getSettingsForFrontend(): IN8nUISettings {
|
||||
// refresh user management status
|
||||
Object.assign(this.frontendSettings.userManagement, {
|
||||
enabled: isUserManagementEnabled(),
|
||||
authenticationMethod: getCurrentAuthenticationMethod(),
|
||||
showSetupOnFirstLoad:
|
||||
config.getEnv('userManagement.disabled') === false &&
|
||||
config.getEnv('userManagement.isInstanceOwnerSetUp') === false &&
|
||||
config.getEnv('userManagement.skipInstanceOwnerSetup') === false &&
|
||||
config.getEnv('deployment.type').startsWith('desktop_') === false,
|
||||
});
|
||||
|
||||
@@ -461,7 +449,7 @@ export class Server extends AbstractServer {
|
||||
private registerControllers(ignoredEndpoints: Readonly<string[]>) {
|
||||
const { app, externalHooks, activeWorkflowRunner, nodeTypes } = this;
|
||||
const repositories = Db.collections;
|
||||
setupAuthMiddlewares(app, ignoredEndpoints, this.restEndpoint, repositories.User);
|
||||
setupAuthMiddlewares(app, ignoredEndpoints, this.restEndpoint);
|
||||
|
||||
const logger = LoggerProxy;
|
||||
const internalHooks = Container.get(InternalHooks);
|
||||
@@ -552,19 +540,6 @@ export class Server extends AbstractServer {
|
||||
`REST endpoint cannot be set to any of these values: ${ignoredEndpoints.join()} `,
|
||||
);
|
||||
|
||||
// eslint-disable-next-line no-useless-escape
|
||||
const authIgnoreRegex = new RegExp(`^\/(${ignoredEndpoints.join('|')})\/?.*$`);
|
||||
|
||||
// Check for basic auth credentials if activated
|
||||
if (config.getEnv('security.basicAuth.active')) {
|
||||
setupBasicAuth(this.app, config, authIgnoreRegex);
|
||||
}
|
||||
|
||||
// Check for and validate JWT if configured
|
||||
if (config.getEnv('security.jwtAuth.active')) {
|
||||
setupExternalJWTAuth(this.app, config, authIgnoreRegex);
|
||||
}
|
||||
|
||||
// ----------------------------------------
|
||||
// Public API
|
||||
// ----------------------------------------
|
||||
@@ -578,7 +553,7 @@ export class Server extends AbstractServer {
|
||||
this.app.use(cookieParser());
|
||||
|
||||
const { restEndpoint, app } = this;
|
||||
setupPushHandler(restEndpoint, app, isUserManagementEnabled());
|
||||
setupPushHandler(restEndpoint, app);
|
||||
|
||||
// Make sure that Vue history mode works properly
|
||||
this.app.use(
|
||||
|
||||
@@ -36,26 +36,8 @@ export function isEmailSetUp(): boolean {
|
||||
return smtp && host && user && pass;
|
||||
}
|
||||
|
||||
export function isUserManagementEnabled(): boolean {
|
||||
// This can be simplified but readability is more important here
|
||||
|
||||
if (config.getEnv('userManagement.isInstanceOwnerSetUp')) {
|
||||
// Short circuit - if owner is set up, UM cannot be disabled.
|
||||
// Users must reset their instance in order to do so.
|
||||
return true;
|
||||
}
|
||||
|
||||
// UM is disabled for desktop by default
|
||||
if (config.getEnv('deployment.type').startsWith('desktop_')) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return config.getEnv('userManagement.disabled') ? false : true;
|
||||
}
|
||||
|
||||
export function isSharingEnabled(): boolean {
|
||||
const license = Container.get(License);
|
||||
return isUserManagementEnabled() && license.isSharingEnabled();
|
||||
return Container.get(License).isSharingEnabled();
|
||||
}
|
||||
|
||||
export async function getRoleId(scope: Role['scope'], name: Role['name']): Promise<Role['id']> {
|
||||
|
||||
@@ -95,7 +95,7 @@ const setupUserManagement = async () => {
|
||||
`INSERT INTO user (id, globalRoleId) values ("${uuid()}", ${instanceOwnerRole[0].insertId})`,
|
||||
);
|
||||
await connection.query(
|
||||
"INSERT INTO \"settings\" (key, value, loadOnStartup) values ('userManagement.isInstanceOwnerSetUp', 'false', true), ('userManagement.skipInstanceOwnerSetup', 'false', true)",
|
||||
"INSERT INTO \"settings\" (key, value, loadOnStartup) values ('userManagement.isInstanceOwnerSetUp', 'false', true)",
|
||||
);
|
||||
|
||||
config.set('userManagement.isInstanceOwnerSetUp', false);
|
||||
|
||||
@@ -18,31 +18,17 @@ import { isApiEnabled } from '@/PublicApi';
|
||||
function getSecuritySettings() {
|
||||
if (config.getEnv('deployment.type') === 'cloud') return null;
|
||||
|
||||
const userManagementEnabled = !config.getEnv('userManagement.disabled');
|
||||
const basicAuthActive = config.getEnv('security.basicAuth.active');
|
||||
const jwtAuthActive = config.getEnv('security.jwtAuth.active');
|
||||
|
||||
const isInstancePubliclyAccessible = !userManagementEnabled && !basicAuthActive && !jwtAuthActive;
|
||||
|
||||
const settings: Record<string, unknown> = {};
|
||||
|
||||
if (isInstancePubliclyAccessible) {
|
||||
settings.publiclyAccessibleInstance =
|
||||
'Important! Your n8n instance is publicly accessible. Any third party who knows your instance URL can access your data.'.toUpperCase();
|
||||
}
|
||||
|
||||
settings.features = {
|
||||
communityPackagesEnabled: config.getEnv('nodes.communityPackages.enabled'),
|
||||
versionNotificationsEnabled: config.getEnv('versionNotifications.enabled'),
|
||||
templatesEnabled: config.getEnv('templates.enabled'),
|
||||
publicApiEnabled: isApiEnabled(),
|
||||
userManagementEnabled,
|
||||
};
|
||||
|
||||
settings.auth = {
|
||||
authExcludeEndpoints: config.getEnv('security.excludeEndpoints') || 'none',
|
||||
basicAuthActive,
|
||||
jwtAuthActive,
|
||||
};
|
||||
|
||||
settings.nodes = {
|
||||
|
||||
@@ -9,7 +9,7 @@ import { getLogger } from '@/Logger';
|
||||
import config from '@/config';
|
||||
import * as Db from '@/Db';
|
||||
import * as CrashJournal from '@/CrashJournal';
|
||||
import { USER_MANAGEMENT_DOCS_URL, inTest } from '@/constants';
|
||||
import { inTest } from '@/constants';
|
||||
import { CredentialTypes } from '@/CredentialTypes';
|
||||
import { CredentialsOverwrites } from '@/CredentialsOverwrites';
|
||||
import { initErrorHandling } from '@/ErrorReporting';
|
||||
@@ -78,24 +78,6 @@ export abstract class BaseCommand extends Command {
|
||||
);
|
||||
}
|
||||
|
||||
if (process.env.N8N_BASIC_AUTH_ACTIVE === 'true') {
|
||||
LoggerProxy.warn(
|
||||
`Basic auth has been deprecated and will be removed in a future version of n8n. For authentication, please consider User Management. To learn more: ${USER_MANAGEMENT_DOCS_URL}`,
|
||||
);
|
||||
}
|
||||
|
||||
if (process.env.N8N_JWT_AUTH_ACTIVE === 'true') {
|
||||
LoggerProxy.warn(
|
||||
`JWT auth has been deprecated and will be removed in a future version of n8n. For authentication, please consider User Management. To learn more: ${USER_MANAGEMENT_DOCS_URL}`,
|
||||
);
|
||||
}
|
||||
|
||||
if (process.env.N8N_USER_MANAGEMENT_DISABLED === 'true') {
|
||||
LoggerProxy.warn(
|
||||
`User Management will be mandatory in a future version of n8n. Please set up the instance owner. To learn more: ${USER_MANAGEMENT_DOCS_URL}`,
|
||||
);
|
||||
}
|
||||
|
||||
this.instanceId = this.userSettings.instanceId ?? '';
|
||||
await Container.get(PostHogClient).init(this.instanceId);
|
||||
await Container.get(InternalHooks).init(this.instanceId);
|
||||
|
||||
@@ -56,10 +56,6 @@ export class Reset extends BaseCommand {
|
||||
{ key: 'userManagement.isInstanceOwnerSetUp' },
|
||||
{ value: 'false' },
|
||||
);
|
||||
await Db.collections.Settings.update(
|
||||
{ key: 'userManagement.skipInstanceOwnerSetup' },
|
||||
{ value: 'false' },
|
||||
);
|
||||
|
||||
this.logger.info('Successfully reset the database to default user state.');
|
||||
}
|
||||
|
||||
@@ -492,82 +492,6 @@ export const schema = {
|
||||
default: '',
|
||||
env: 'N8N_AUTH_EXCLUDE_ENDPOINTS',
|
||||
},
|
||||
basicAuth: {
|
||||
active: {
|
||||
format: 'Boolean',
|
||||
default: false,
|
||||
env: 'N8N_BASIC_AUTH_ACTIVE',
|
||||
doc: '[DEPRECATED] If basic auth should be activated for editor and REST-API',
|
||||
},
|
||||
user: {
|
||||
format: String,
|
||||
default: '',
|
||||
env: 'N8N_BASIC_AUTH_USER',
|
||||
doc: '[DEPRECATED] The name of the basic auth user',
|
||||
},
|
||||
password: {
|
||||
format: String,
|
||||
default: '',
|
||||
env: 'N8N_BASIC_AUTH_PASSWORD',
|
||||
doc: '[DEPRECATED] The password of the basic auth user',
|
||||
},
|
||||
hash: {
|
||||
format: 'Boolean',
|
||||
default: false,
|
||||
env: 'N8N_BASIC_AUTH_HASH',
|
||||
doc: '[DEPRECATED] If password for basic auth is hashed',
|
||||
},
|
||||
},
|
||||
jwtAuth: {
|
||||
active: {
|
||||
format: 'Boolean',
|
||||
default: false,
|
||||
env: 'N8N_JWT_AUTH_ACTIVE',
|
||||
doc: '[DEPRECATED] If JWT auth should be activated for editor and REST-API',
|
||||
},
|
||||
jwtHeader: {
|
||||
format: String,
|
||||
default: '',
|
||||
env: 'N8N_JWT_AUTH_HEADER',
|
||||
doc: '[DEPRECATED] The request header containing a signed JWT',
|
||||
},
|
||||
jwtHeaderValuePrefix: {
|
||||
format: String,
|
||||
default: '',
|
||||
env: 'N8N_JWT_AUTH_HEADER_VALUE_PREFIX',
|
||||
doc: '[DEPRECATED] The request header value prefix to strip (optional)',
|
||||
},
|
||||
jwksUri: {
|
||||
format: String,
|
||||
default: '',
|
||||
env: 'N8N_JWKS_URI',
|
||||
doc: '[DEPRECATED] The URI to fetch JWK Set for JWT authentication',
|
||||
},
|
||||
jwtIssuer: {
|
||||
format: String,
|
||||
default: '',
|
||||
env: 'N8N_JWT_ISSUER',
|
||||
doc: '[DEPRECATED] JWT issuer to expect (optional)',
|
||||
},
|
||||
jwtNamespace: {
|
||||
format: String,
|
||||
default: '',
|
||||
env: 'N8N_JWT_NAMESPACE',
|
||||
doc: '[DEPRECATED] JWT namespace to expect (optional)',
|
||||
},
|
||||
jwtAllowedTenantKey: {
|
||||
format: String,
|
||||
default: '',
|
||||
env: 'N8N_JWT_ALLOWED_TENANT_KEY',
|
||||
doc: '[DEPRECATED] JWT tenant key name to inspect within JWT namespace (optional)',
|
||||
},
|
||||
jwtAllowedTenant: {
|
||||
format: String,
|
||||
default: '',
|
||||
env: 'N8N_JWT_ALLOWED_TENANT',
|
||||
doc: '[DEPRECATED] JWT tenant to allow (optional)',
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
endpoints: {
|
||||
@@ -726,12 +650,6 @@ export const schema = {
|
||||
},
|
||||
|
||||
userManagement: {
|
||||
disabled: {
|
||||
doc: '[DEPRECATED] Disable user management and hide it completely.',
|
||||
format: Boolean,
|
||||
default: false,
|
||||
env: 'N8N_USER_MANAGEMENT_DISABLED',
|
||||
},
|
||||
jwtSecret: {
|
||||
doc: 'Set a specific JWT secret (optional - n8n can generate one)', // Generated @ start.ts
|
||||
format: String,
|
||||
@@ -744,12 +662,6 @@ export const schema = {
|
||||
format: Boolean,
|
||||
default: false,
|
||||
},
|
||||
skipInstanceOwnerSetup: {
|
||||
// n8n loads this setting from DB on startup
|
||||
doc: 'Whether to hide the prompt the first time n8n starts with UM enabled',
|
||||
format: Boolean,
|
||||
default: false,
|
||||
},
|
||||
emails: {
|
||||
mode: {
|
||||
doc: 'How to send emails',
|
||||
|
||||
1
packages/cli/src/config/types.d.ts
vendored
1
packages/cli/src/config/types.d.ts
vendored
@@ -80,7 +80,6 @@ type ExceptionPaths = {
|
||||
'nodes.exclude': string[] | undefined;
|
||||
'nodes.include': string[] | undefined;
|
||||
'userManagement.isInstanceOwnerSetUp': boolean;
|
||||
'userManagement.skipInstanceOwnerSetup': boolean;
|
||||
};
|
||||
|
||||
// -----------------------------------
|
||||
|
||||
@@ -86,6 +86,3 @@ export const enum LICENSE_QUOTAS {
|
||||
}
|
||||
|
||||
export const CREDENTIAL_BLANKING_VALUE = '__n8n_BLANK_VALUE_e5362baf-c777-4d57-a609-6eaf1f9e87f6';
|
||||
|
||||
export const USER_MANAGEMENT_DOCS_URL =
|
||||
'https://docs.n8n.io/hosting/authentication/user-management-self-hosted';
|
||||
|
||||
@@ -148,19 +148,4 @@ export class OwnerController {
|
||||
|
||||
return sanitizeUser(owner);
|
||||
}
|
||||
|
||||
/**
|
||||
* Persist that the instance owner setup has been skipped
|
||||
*/
|
||||
@Post('/skip-setup')
|
||||
async skipSetup() {
|
||||
await this.settingsRepository.update(
|
||||
{ key: 'userManagement.skipInstanceOwnerSetup' },
|
||||
{ value: JSON.stringify(true) },
|
||||
);
|
||||
|
||||
this.config.set('userManagement.skipInstanceOwnerSetup', true);
|
||||
|
||||
return { success: true };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,7 +32,6 @@ import type {
|
||||
import type { ActiveWorkflowRunner } from '@/ActiveWorkflowRunner';
|
||||
import { AuthIdentity } from '@db/entities/AuthIdentity';
|
||||
import type { PostHogClient } from '@/posthog';
|
||||
import { userManagementEnabledMiddleware } from '../middlewares/userManagementEnabled';
|
||||
import { isSamlLicensedAndEnabled } from '../sso/saml/samlHelpers';
|
||||
import type {
|
||||
RoleRepository,
|
||||
@@ -106,7 +105,7 @@ export class UsersController {
|
||||
/**
|
||||
* Send email invite(s) to one or multiple users and create user shell(s).
|
||||
*/
|
||||
@Post('/', { middlewares: [userManagementEnabledMiddleware] })
|
||||
@Post('/')
|
||||
async sendEmailInvites(req: UserRequest.Invite) {
|
||||
if (isSamlLicensedAndEnabled()) {
|
||||
this.logger.debug(
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
import type { IrreversibleMigration, MigrationContext } from '@db/types';
|
||||
|
||||
export class RemoveSkipOwnerSetup1681134145997 implements IrreversibleMigration {
|
||||
async up({ queryRunner, tablePrefix }: MigrationContext) {
|
||||
await queryRunner.query(
|
||||
`DELETE FROM ${tablePrefix}settings WHERE \`key\` = 'userManagement.skipInstanceOwnerSetup';`,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -40,6 +40,7 @@ import { CreateVariables1677501636753 } from './1677501636753-CreateVariables';
|
||||
import { AddUserActivatedProperty1681134145996 } from './1681134145996-AddUserActivatedProperty';
|
||||
import { MigrateIntegerKeysToString1690000000001 } from './1690000000001-MigrateIntegerKeysToString';
|
||||
import { SeparateExecutionData1690000000030 } from './1690000000030-SeparateExecutionData';
|
||||
import { RemoveSkipOwnerSetup1681134145997 } from './1681134145997-RemoveSkipOwnerSetup';
|
||||
|
||||
export const mysqlMigrations: Migration[] = [
|
||||
InitialMigration1588157391238,
|
||||
@@ -83,4 +84,5 @@ export const mysqlMigrations: Migration[] = [
|
||||
AddUserActivatedProperty1681134145996,
|
||||
MigrateIntegerKeysToString1690000000001,
|
||||
SeparateExecutionData1690000000030,
|
||||
RemoveSkipOwnerSetup1681134145997,
|
||||
];
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
import type { IrreversibleMigration, MigrationContext } from '@db/types';
|
||||
|
||||
export class RemoveSkipOwnerSetup1681134145997 implements IrreversibleMigration {
|
||||
async up({ queryRunner, tablePrefix }: MigrationContext) {
|
||||
await queryRunner.query(
|
||||
`DELETE FROM ${tablePrefix}settings WHERE key = 'userManagement.skipInstanceOwnerSetup';`,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -38,6 +38,7 @@ import { CreateVariables1677501636754 } from './1677501636754-CreateVariables';
|
||||
import { AddUserActivatedProperty1681134145996 } from './1681134145996-AddUserActivatedProperty';
|
||||
import { MigrateIntegerKeysToString1690000000000 } from './1690000000000-MigrateIntegerKeysToString';
|
||||
import { SeparateExecutionData1690000000020 } from './1690000000020-SeparateExecutionData';
|
||||
import { RemoveSkipOwnerSetup1681134145997 } from './1681134145997-RemoveSkipOwnerSetup';
|
||||
|
||||
export const postgresMigrations: Migration[] = [
|
||||
InitialMigration1587669153312,
|
||||
@@ -79,4 +80,5 @@ export const postgresMigrations: Migration[] = [
|
||||
AddUserActivatedProperty1681134145996,
|
||||
MigrateIntegerKeysToString1690000000000,
|
||||
SeparateExecutionData1690000000020,
|
||||
RemoveSkipOwnerSetup1681134145997,
|
||||
];
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
import type { IrreversibleMigration, MigrationContext } from '@db/types';
|
||||
|
||||
export class RemoveSkipOwnerSetup1681134145997 implements IrreversibleMigration {
|
||||
async up({ queryRunner, tablePrefix }: MigrationContext) {
|
||||
await queryRunner.query(
|
||||
`DELETE FROM "${tablePrefix}settings" WHERE key = 'userManagement.skipInstanceOwnerSetup';`,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -37,6 +37,7 @@ import { CreateVariables1677501636752 } from './1677501636752-CreateVariables';
|
||||
import { AddUserActivatedProperty1681134145996 } from './1681134145996-AddUserActivatedProperty';
|
||||
import { MigrateIntegerKeysToString1690000000002 } from './1690000000002-MigrateIntegerKeysToString';
|
||||
import { SeparateExecutionData1690000000010 } from './1690000000010-SeparateExecutionData';
|
||||
import { RemoveSkipOwnerSetup1681134145997 } from './1681134145997-RemoveSkipOwnerSetup';
|
||||
|
||||
const sqliteMigrations: Migration[] = [
|
||||
InitialMigration1588102412422,
|
||||
@@ -77,6 +78,7 @@ const sqliteMigrations: Migration[] = [
|
||||
AddUserActivatedProperty1681134145996,
|
||||
MigrateIntegerKeysToString1690000000002,
|
||||
SeparateExecutionData1690000000010,
|
||||
RemoveSkipOwnerSetup1681134145997,
|
||||
];
|
||||
|
||||
export { sqliteMigrations };
|
||||
|
||||
@@ -10,7 +10,6 @@ import type { AuthenticatedRequest } from '@/requests';
|
||||
import config from '@/config';
|
||||
import { AUTH_COOKIE_NAME, EDITOR_UI_DIST_DIR } from '@/constants';
|
||||
import { issueCookie, resolveJwtContent } from '@/auth/jwt';
|
||||
import { isUserManagementEnabled } from '@/UserManagement/UserManagementHelper';
|
||||
import type { UserRepository } from '@db/repositories';
|
||||
import { canSkipAuth } from '@/decorators/registerController';
|
||||
|
||||
@@ -19,7 +18,7 @@ const jwtFromRequest = (req: Request) => {
|
||||
return (req.cookies?.[AUTH_COOKIE_NAME] as string | undefined) ?? null;
|
||||
};
|
||||
|
||||
const jwtAuth = (): RequestHandler => {
|
||||
const userManagementJwtAuth = (): RequestHandler => {
|
||||
const jwtStrategy = new Strategy(
|
||||
{
|
||||
jwtFromRequest,
|
||||
@@ -79,11 +78,10 @@ export const setupAuthMiddlewares = (
|
||||
app: Application,
|
||||
ignoredEndpoints: Readonly<string[]>,
|
||||
restEndpoint: string,
|
||||
userRepository: UserRepository,
|
||||
) => {
|
||||
// needed for testing; not adding overhead since it directly returns if req.cookies exists
|
||||
app.use(cookieParser());
|
||||
app.use(jwtAuth());
|
||||
app.use(userManagementJwtAuth());
|
||||
|
||||
app.use(async (req: Request, res: Response, next: NextFunction) => {
|
||||
if (
|
||||
@@ -101,15 +99,6 @@ export const setupAuthMiddlewares = (
|
||||
return next();
|
||||
}
|
||||
|
||||
// skip authentication if user management is disabled
|
||||
if (!isUserManagementEnabled()) {
|
||||
req.user = await userRepository.findOneOrFail({
|
||||
relations: ['globalRole'],
|
||||
where: {},
|
||||
});
|
||||
return next();
|
||||
}
|
||||
|
||||
return passportMiddleware(req, res, next);
|
||||
});
|
||||
|
||||
|
||||
@@ -1,57 +0,0 @@
|
||||
import type { Application } from 'express';
|
||||
import basicAuth from 'basic-auth';
|
||||
// IMPORTANT! Do not switch to anther bcrypt library unless really necessary and
|
||||
// tested with all possible systems like Windows, Alpine on ARM, FreeBSD, ...
|
||||
import { compare } from 'bcryptjs';
|
||||
import type { Config } from '@/config';
|
||||
import { basicAuthAuthorizationError } from '@/ResponseHelper';
|
||||
|
||||
export const setupBasicAuth = (app: Application, config: Config, authIgnoreRegex: RegExp) => {
|
||||
const basicAuthUser = config.getEnv('security.basicAuth.user');
|
||||
if (basicAuthUser === '') {
|
||||
throw new Error('Basic auth is activated but no user got defined. Please set one!');
|
||||
}
|
||||
|
||||
const basicAuthPassword = config.getEnv('security.basicAuth.password');
|
||||
if (basicAuthPassword === '') {
|
||||
throw new Error('Basic auth is activated but no password got defined. Please set one!');
|
||||
}
|
||||
|
||||
const basicAuthHashEnabled = config.getEnv('security.basicAuth.hash');
|
||||
|
||||
let validPassword: null | string = null;
|
||||
|
||||
app.use(async (req, res, next) => {
|
||||
// Skip basic auth for a few listed endpoints or when instance owner has been setup
|
||||
if (authIgnoreRegex.exec(req.url) || config.getEnv('userManagement.isInstanceOwnerSetUp')) {
|
||||
return next();
|
||||
}
|
||||
const realm = 'n8n - Editor UI';
|
||||
const basicAuthData = basicAuth(req);
|
||||
|
||||
if (basicAuthData === undefined) {
|
||||
// Authorization data is missing
|
||||
return basicAuthAuthorizationError(res, realm, 'Authorization is required!');
|
||||
}
|
||||
|
||||
if (basicAuthData.name === basicAuthUser) {
|
||||
if (basicAuthHashEnabled) {
|
||||
if (validPassword === null && (await compare(basicAuthData.pass, basicAuthPassword))) {
|
||||
// Password is valid so save for future requests
|
||||
validPassword = basicAuthData.pass;
|
||||
}
|
||||
|
||||
if (validPassword === basicAuthData.pass && validPassword !== null) {
|
||||
// Provided hash is correct
|
||||
return next();
|
||||
}
|
||||
} else if (basicAuthData.pass === basicAuthPassword) {
|
||||
// Provided password is correct
|
||||
return next();
|
||||
}
|
||||
}
|
||||
|
||||
// Provided authentication data is wrong
|
||||
return basicAuthAuthorizationError(res, realm, 'Authorization data is wrong!');
|
||||
});
|
||||
};
|
||||
@@ -1,85 +0,0 @@
|
||||
import type { Application } from 'express';
|
||||
import jwt from 'jsonwebtoken';
|
||||
import jwks from 'jwks-rsa';
|
||||
import type { Config } from '@/config';
|
||||
import { jwtAuthAuthorizationError } from '@/ResponseHelper';
|
||||
|
||||
export const setupExternalJWTAuth = (app: Application, config: Config, authIgnoreRegex: RegExp) => {
|
||||
const jwtAuthHeader = config.getEnv('security.jwtAuth.jwtHeader');
|
||||
if (jwtAuthHeader === '') {
|
||||
throw new Error('JWT auth is activated but no request header was defined. Please set one!');
|
||||
}
|
||||
|
||||
const jwksUri = config.getEnv('security.jwtAuth.jwksUri');
|
||||
if (jwksUri === '') {
|
||||
throw new Error('JWT auth is activated but no JWK Set URI was defined. Please set one!');
|
||||
}
|
||||
|
||||
const jwtHeaderValuePrefix = config.getEnv('security.jwtAuth.jwtHeaderValuePrefix');
|
||||
const jwtIssuer = config.getEnv('security.jwtAuth.jwtIssuer');
|
||||
const jwtNamespace = config.getEnv('security.jwtAuth.jwtNamespace');
|
||||
const jwtAllowedTenantKey = config.getEnv('security.jwtAuth.jwtAllowedTenantKey');
|
||||
const jwtAllowedTenant = config.getEnv('security.jwtAuth.jwtAllowedTenant');
|
||||
|
||||
// eslint-disable-next-line no-inner-declarations
|
||||
function isTenantAllowed(decodedToken: object): boolean {
|
||||
if (jwtNamespace === '' || jwtAllowedTenantKey === '' || jwtAllowedTenant === '') {
|
||||
return true;
|
||||
}
|
||||
|
||||
for (const [k, v] of Object.entries(decodedToken)) {
|
||||
if (k === jwtNamespace) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
|
||||
for (const [kn, kv] of Object.entries(v)) {
|
||||
if (kn === jwtAllowedTenantKey && kv === jwtAllowedTenant) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line consistent-return
|
||||
app.use((req, res, next) => {
|
||||
if (authIgnoreRegex.exec(req.url)) {
|
||||
return next();
|
||||
}
|
||||
|
||||
let token = req.header(jwtAuthHeader) as string;
|
||||
if (token === undefined || token === '') {
|
||||
return jwtAuthAuthorizationError(res, 'Missing token');
|
||||
}
|
||||
|
||||
if (jwtHeaderValuePrefix !== '' && token.startsWith(jwtHeaderValuePrefix)) {
|
||||
token = token.replace(`${jwtHeaderValuePrefix} `, '').trimStart();
|
||||
}
|
||||
|
||||
const jwkClient = jwks({ cache: true, jwksUri });
|
||||
const getKey: jwt.GetPublicKeyOrSecret = (header, callbackFn) => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-throw-literal
|
||||
if (!header.kid) throw jwtAuthAuthorizationError(res, 'No JWT key found');
|
||||
jwkClient.getSigningKey(header.kid, (error, key) => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-throw-literal
|
||||
if (error) throw jwtAuthAuthorizationError(res, error.message);
|
||||
callbackFn(null, key?.getPublicKey());
|
||||
});
|
||||
};
|
||||
|
||||
const jwtVerifyOptions: jwt.VerifyOptions = {
|
||||
issuer: jwtIssuer !== '' ? jwtIssuer : undefined,
|
||||
ignoreExpiration: false,
|
||||
};
|
||||
|
||||
jwt.verify(token, getKey, jwtVerifyOptions, (error: jwt.VerifyErrors, decoded: object) => {
|
||||
if (error) {
|
||||
jwtAuthAuthorizationError(res, 'Invalid token');
|
||||
} else if (!isTenantAllowed(decoded)) {
|
||||
jwtAuthAuthorizationError(res, 'Tenant not allowed');
|
||||
} else {
|
||||
next();
|
||||
}
|
||||
});
|
||||
});
|
||||
};
|
||||
@@ -1,12 +0,0 @@
|
||||
import type { RequestHandler } from 'express';
|
||||
import { LoggerProxy } from 'n8n-workflow';
|
||||
import { isUserManagementEnabled } from '../UserManagement/UserManagementHelper';
|
||||
|
||||
export const userManagementEnabledMiddleware: RequestHandler = (req, res, next) => {
|
||||
if (isUserManagementEnabled()) {
|
||||
next();
|
||||
} else {
|
||||
LoggerProxy.debug('Request failed because user management is disabled');
|
||||
res.status(400).json({ status: 'error', message: 'User management is disabled' });
|
||||
}
|
||||
};
|
||||
@@ -57,11 +57,7 @@ export const setupPushServer = (restEndpoint: string, server: Server, app: Appli
|
||||
}
|
||||
};
|
||||
|
||||
export const setupPushHandler = (
|
||||
restEndpoint: string,
|
||||
app: Application,
|
||||
isUserManagementEnabled: boolean,
|
||||
) => {
|
||||
export const setupPushHandler = (restEndpoint: string, app: Application) => {
|
||||
const endpoint = `/${restEndpoint}/push`;
|
||||
|
||||
const pushValidationMiddleware: RequestHandler = async (
|
||||
@@ -83,20 +79,19 @@ export const setupPushHandler = (
|
||||
}
|
||||
|
||||
// Handle authentication
|
||||
if (isUserManagementEnabled) {
|
||||
try {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access
|
||||
const authCookie: string = req.cookies?.[AUTH_COOKIE_NAME] ?? '';
|
||||
await resolveJwt(authCookie);
|
||||
} catch (error) {
|
||||
if (ws) {
|
||||
ws.send(`Unauthorized: ${(error as Error).message}`);
|
||||
ws.close(401);
|
||||
} else {
|
||||
res.status(401).send('Unauthorized');
|
||||
}
|
||||
return;
|
||||
|
||||
try {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access
|
||||
const authCookie: string = req.cookies?.[AUTH_COOKIE_NAME] ?? '';
|
||||
await resolveJwt(authCookie);
|
||||
} catch (error) {
|
||||
if (ws) {
|
||||
ws.send(`Unauthorized: ${(error as Error).message}`);
|
||||
ws.close(401);
|
||||
} else {
|
||||
res.status(401).send('Unauthorized');
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
next();
|
||||
|
||||
@@ -6,7 +6,7 @@ import { User } from '@db/entities/User';
|
||||
import { RoleRepository } from '@db/repositories';
|
||||
import { License } from '@/License';
|
||||
import { AuthError, InternalServerError } from '@/ResponseHelper';
|
||||
import { hashPassword, isUserManagementEnabled } from '@/UserManagement/UserManagementHelper';
|
||||
import { hashPassword } from '@/UserManagement/UserManagementHelper';
|
||||
import type { SamlPreferences } from './types/samlPreferences';
|
||||
import type { SamlUserAttributes } from './types/samlUserAttributes';
|
||||
import type { FlowResult } from 'samlify/types/src/flow';
|
||||
@@ -52,10 +52,7 @@ export function setSamlLoginLabel(label: string): void {
|
||||
config.set(SAML_LOGIN_LABEL, label);
|
||||
}
|
||||
|
||||
export function isSamlLicensed(): boolean {
|
||||
const license = Container.get(License);
|
||||
return isUserManagementEnabled() && license.isSamlEnabled();
|
||||
}
|
||||
export const isSamlLicensed = () => Container.get(License).isSamlEnabled();
|
||||
|
||||
export function isSamlLicensedAndEnabled(): boolean {
|
||||
return isSamlLoginEnabled() && isSamlLicensed() && isSamlCurrentAuthenticationMethod();
|
||||
|
||||
Reference in New Issue
Block a user