mirror of
https://github.com/n8n-io/n8n.git
synced 2025-01-12 13:27:31 -08:00
fix(core): Set the authentication methad to email
during startup if the SAML configuration in the database has been corrupted (#11600)
Co-authored-by: Iván Ovejero <ivov.src@gmail.com>
This commit is contained in:
parent
ddbb263dce
commit
6439291738
|
@ -2,22 +2,29 @@ import type express from 'express';
|
||||||
import { mock } from 'jest-mock-extended';
|
import { mock } from 'jest-mock-extended';
|
||||||
import type { IdentityProviderInstance, ServiceProviderInstance } from 'samlify';
|
import type { IdentityProviderInstance, ServiceProviderInstance } from 'samlify';
|
||||||
|
|
||||||
|
import { SettingsRepository } from '@/databases/repositories/settings.repository';
|
||||||
import { Logger } from '@/logging/logger.service';
|
import { Logger } from '@/logging/logger.service';
|
||||||
import { UrlService } from '@/services/url.service';
|
import { UrlService } from '@/services/url.service';
|
||||||
import * as samlHelpers from '@/sso/saml/saml-helpers';
|
import * as samlHelpers from '@/sso/saml/saml-helpers';
|
||||||
import { SamlService } from '@/sso/saml/saml.service.ee';
|
import { SamlService } from '@/sso/saml/saml.service.ee';
|
||||||
import { mockInstance } from '@test/mocking';
|
import { mockInstance } from '@test/mocking';
|
||||||
|
|
||||||
|
import { SAML_PREFERENCES_DB_KEY } from '../constants';
|
||||||
|
import { InvalidSamlMetadataError } from '../errors/invalid-saml-metadata.error';
|
||||||
|
|
||||||
describe('SamlService', () => {
|
describe('SamlService', () => {
|
||||||
const logger = mockInstance(Logger);
|
const logger = mockInstance(Logger);
|
||||||
const urlService = mockInstance(UrlService);
|
const urlService = mockInstance(UrlService);
|
||||||
const samlService = new SamlService(logger, urlService);
|
const samlService = new SamlService(logger, urlService);
|
||||||
|
const settingsRepository = mockInstance(SettingsRepository);
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.restoreAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
describe('getAttributesFromLoginResponse', () => {
|
describe('getAttributesFromLoginResponse', () => {
|
||||||
test('throws when any attribute is missing', async () => {
|
test('throws when any attribute is missing', async () => {
|
||||||
//
|
|
||||||
// ARRANGE
|
// ARRANGE
|
||||||
//
|
|
||||||
jest
|
jest
|
||||||
.spyOn(samlService, 'getIdentityProviderInstance')
|
.spyOn(samlService, 'getIdentityProviderInstance')
|
||||||
.mockReturnValue(mock<IdentityProviderInstance>());
|
.mockReturnValue(mock<IdentityProviderInstance>());
|
||||||
|
@ -41,9 +48,7 @@ describe('SamlService', () => {
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
|
||||||
//
|
|
||||||
// ACT & ASSERT
|
// ACT & ASSERT
|
||||||
//
|
|
||||||
await expect(
|
await expect(
|
||||||
samlService.getAttributesFromLoginResponse(mock<express.Request>(), 'post'),
|
samlService.getAttributesFromLoginResponse(mock<express.Request>(), 'post'),
|
||||||
).rejects.toThrowError(
|
).rejects.toThrowError(
|
||||||
|
@ -51,4 +56,74 @@ describe('SamlService', () => {
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('init', () => {
|
||||||
|
test('calls `reset` if an InvalidSamlMetadataError is thrown', async () => {
|
||||||
|
// ARRANGE
|
||||||
|
jest
|
||||||
|
.spyOn(samlService, 'loadFromDbAndApplySamlPreferences')
|
||||||
|
.mockRejectedValue(new InvalidSamlMetadataError());
|
||||||
|
jest.spyOn(samlService, 'reset');
|
||||||
|
|
||||||
|
// ACT
|
||||||
|
await samlService.init();
|
||||||
|
|
||||||
|
// ASSERT
|
||||||
|
expect(samlService.reset).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('calls `reset` if a SyntaxError is thrown', async () => {
|
||||||
|
// ARRANGE
|
||||||
|
jest
|
||||||
|
.spyOn(samlService, 'loadFromDbAndApplySamlPreferences')
|
||||||
|
.mockRejectedValue(new SyntaxError());
|
||||||
|
jest.spyOn(samlService, 'reset');
|
||||||
|
|
||||||
|
// ACT
|
||||||
|
await samlService.init();
|
||||||
|
|
||||||
|
// ASSERT
|
||||||
|
expect(samlService.reset).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('does not call reset and rethrows if another error is thrown', async () => {
|
||||||
|
// ARRANGE
|
||||||
|
jest
|
||||||
|
.spyOn(samlService, 'loadFromDbAndApplySamlPreferences')
|
||||||
|
.mockRejectedValue(new TypeError());
|
||||||
|
jest.spyOn(samlService, 'reset');
|
||||||
|
|
||||||
|
// ACT & ASSERT
|
||||||
|
await expect(samlService.init()).rejects.toThrowError(TypeError);
|
||||||
|
expect(samlService.reset).toHaveBeenCalledTimes(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('does not call reset if no error is trown', async () => {
|
||||||
|
// ARRANGE
|
||||||
|
jest.spyOn(samlService, 'reset');
|
||||||
|
|
||||||
|
// ACT
|
||||||
|
await samlService.init();
|
||||||
|
|
||||||
|
// ASSERT
|
||||||
|
expect(samlService.reset).toHaveBeenCalledTimes(0);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('reset', () => {
|
||||||
|
test('disables saml login and deletes the saml `features.saml` key in the db', async () => {
|
||||||
|
// ARRANGE
|
||||||
|
jest.spyOn(samlHelpers, 'setSamlLoginEnabled');
|
||||||
|
jest.spyOn(settingsRepository, 'delete');
|
||||||
|
|
||||||
|
// ACT
|
||||||
|
await samlService.reset();
|
||||||
|
|
||||||
|
// ASSERT
|
||||||
|
expect(samlHelpers.setSamlLoginEnabled).toHaveBeenCalledTimes(1);
|
||||||
|
expect(samlHelpers.setSamlLoginEnabled).toHaveBeenCalledWith(false);
|
||||||
|
expect(settingsRepository.delete).toHaveBeenCalledTimes(1);
|
||||||
|
expect(settingsRepository.delete).toHaveBeenCalledWith({ key: SAML_PREFERENCES_DB_KEY });
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -0,0 +1,7 @@
|
||||||
|
import { ApplicationError } from 'n8n-workflow';
|
||||||
|
|
||||||
|
export class InvalidSamlMetadataError extends ApplicationError {
|
||||||
|
constructor() {
|
||||||
|
super('Invalid SAML metadata', { level: 'warning' });
|
||||||
|
}
|
||||||
|
}
|
|
@ -16,6 +16,7 @@ import { Logger } from '@/logging/logger.service';
|
||||||
import { UrlService } from '@/services/url.service';
|
import { UrlService } from '@/services/url.service';
|
||||||
|
|
||||||
import { SAML_PREFERENCES_DB_KEY } from './constants';
|
import { SAML_PREFERENCES_DB_KEY } from './constants';
|
||||||
|
import { InvalidSamlMetadataError } from './errors/invalid-saml-metadata.error';
|
||||||
import {
|
import {
|
||||||
createUserFromSamlAttributes,
|
createUserFromSamlAttributes,
|
||||||
getMappedSamlAttributesFromFlowResult,
|
getMappedSamlAttributesFromFlowResult,
|
||||||
|
@ -81,12 +82,25 @@ export class SamlService {
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
async init(): Promise<void> {
|
async init(): Promise<void> {
|
||||||
|
try {
|
||||||
// load preferences first but do not apply so as to not load samlify unnecessarily
|
// load preferences first but do not apply so as to not load samlify unnecessarily
|
||||||
await this.loadFromDbAndApplySamlPreferences(false);
|
await this.loadFromDbAndApplySamlPreferences(false);
|
||||||
if (isSamlLicensedAndEnabled()) {
|
if (isSamlLicensedAndEnabled()) {
|
||||||
await this.loadSamlify();
|
await this.loadSamlify();
|
||||||
await this.loadFromDbAndApplySamlPreferences(true);
|
await this.loadFromDbAndApplySamlPreferences(true);
|
||||||
}
|
}
|
||||||
|
} catch (error) {
|
||||||
|
// If the SAML configuration has been corrupted in the database we'll
|
||||||
|
// delete the corrupted configuration and enable email logins again.
|
||||||
|
if (error instanceof InvalidSamlMetadataError || error instanceof SyntaxError) {
|
||||||
|
this.logger.warn(
|
||||||
|
`SAML initialization failed because of invalid metadata in database: ${error.message}. IMPORTANT: Disabling SAML and switching to email-based login for all users. Please review your configuration and re-enable SAML.`,
|
||||||
|
);
|
||||||
|
await this.reset();
|
||||||
|
} else {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async loadSamlify() {
|
async loadSamlify() {
|
||||||
|
@ -98,7 +112,7 @@ export class SamlService {
|
||||||
validate: async (response: string) => {
|
validate: async (response: string) => {
|
||||||
const valid = await validateResponse(response);
|
const valid = await validateResponse(response);
|
||||||
if (!valid) {
|
if (!valid) {
|
||||||
throw new ApplicationError('Invalid SAML response');
|
throw new InvalidSamlMetadataError();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
@ -230,7 +244,7 @@ export class SamlService {
|
||||||
} else if (prefs.metadata) {
|
} else if (prefs.metadata) {
|
||||||
const validationResult = await validateMetadata(prefs.metadata);
|
const validationResult = await validateMetadata(prefs.metadata);
|
||||||
if (!validationResult) {
|
if (!validationResult) {
|
||||||
throw new ApplicationError('Invalid SAML metadata');
|
throw new InvalidSamlMetadataError();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
this.getIdentityProviderInstance(true);
|
this.getIdentityProviderInstance(true);
|
||||||
|
@ -371,4 +385,13 @@ export class SamlService {
|
||||||
}
|
}
|
||||||
return attributes;
|
return attributes;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Disables SAML, switches to email based logins and deletes the SAML
|
||||||
|
* configuration from the database.
|
||||||
|
*/
|
||||||
|
async reset() {
|
||||||
|
await setSamlLoginEnabled(false);
|
||||||
|
await Container.get(SettingsRepository).delete({ key: SAML_PREFERENCES_DB_KEY });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,10 +5,10 @@ import type { AuthProviderType } from '@/databases/entities/auth-identity';
|
||||||
import { SettingsRepository } from '@/databases/repositories/settings.repository';
|
import { SettingsRepository } from '@/databases/repositories/settings.repository';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Only one authentication method can be active at a time. This function sets the current authentication method
|
* Only one authentication method can be active at a time. This function sets
|
||||||
* and saves it to the database.
|
* the current authentication method and saves it to the database.
|
||||||
* SSO methods should only switch to email and then to another method. Email can switch to any method.
|
* SSO methods should only switch to email and then to another method. Email
|
||||||
* @param authenticationMethod
|
* can switch to any method.
|
||||||
*/
|
*/
|
||||||
export async function setCurrentAuthenticationMethod(
|
export async function setCurrentAuthenticationMethod(
|
||||||
authenticationMethod: AuthProviderType,
|
authenticationMethod: AuthProviderType,
|
||||||
|
|
Loading…
Reference in a new issue