Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor(core): Remove unnecessary indirection in SAML code (no-changelog) #9103

Merged
merged 1 commit into from
Apr 10, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 0 additions & 28 deletions packages/cli/src/sso/saml/constants.ts
Original file line number Diff line number Diff line change
@@ -1,31 +1,3 @@
export class SamlUrls {
static readonly samlRESTRoot = '/rest/sso/saml';

static readonly initSSO = '/initsso';

static readonly acs = '/acs';

static readonly restAcs = this.samlRESTRoot + this.acs;

static readonly metadata = '/metadata';

static readonly restMetadata = this.samlRESTRoot + this.metadata;

static readonly config = '/config';

static readonly configTest = '/config/test';

static readonly configTestReturn = '/config/test/return';

static readonly configToggleEnabled = '/config/toggle';

static readonly defaultRedirect = '/';

static readonly samlOnboarding = '/saml/onboarding';
}

export const SAML_PREFERENCES_DB_KEY = 'features.saml';

export const SAML_LOGIN_LABEL = 'sso.saml.loginLabel';

export const SAML_LOGIN_ENABLED = 'sso.saml.loginEnabled';
4 changes: 2 additions & 2 deletions packages/cli/src/sso/saml/middleware/samlEnabledMiddleware.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
import type { RequestHandler } from 'express';
import { isSamlLicensed, isSamlLicensedAndEnabled } from '../samlHelpers';

export const samlLicensedAndEnabledMiddleware: RequestHandler = (req, res, next) => {
export const samlLicensedAndEnabledMiddleware: RequestHandler = (_, res, next) => {
if (isSamlLicensedAndEnabled()) {
next();
} else {
res.status(403).json({ status: 'error', message: 'Unauthorized' });
}
};

export const samlLicensedMiddleware: RequestHandler = (req, res, next) => {
export const samlLicensedMiddleware: RequestHandler = (_, res, next) => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe we should replace this middleware with an @Licensed decorator?

if (isSamlLicensed()) {
next();
} else {
Expand Down
32 changes: 12 additions & 20 deletions packages/cli/src/sso/saml/routes/saml.controller.ee.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ import { BadRequestError } from '@/errors/response-errors/bad-request.error';
import { AuthError } from '@/errors/response-errors/auth.error';
import { UrlService } from '@/services/url.service';

import { SamlUrls } from '../constants';
import {
getServiceProviderConfigTestReturnUrl,
getServiceProviderEntityId,
Expand All @@ -39,18 +38,17 @@ export class SamlController {
private readonly internalHooks: InternalHooks,
) {}

@Get(SamlUrls.metadata, { skipAuth: true })
@Get('/metadata', { skipAuth: true })
async getServiceProviderMetadata(_: express.Request, res: express.Response) {
return res
.header('Content-Type', 'text/xml')
.send(this.samlService.getServiceProviderInstance().getMetadata());
}

/**
* GET /sso/saml/config
* Return SAML config
*/
@Get(SamlUrls.config, { middlewares: [samlLicensedMiddleware] })
@Get('/config', { middlewares: [samlLicensedMiddleware] })
async configGet() {
const prefs = this.samlService.samlPreferences;
return {
Expand All @@ -61,10 +59,9 @@ export class SamlController {
}

/**
* POST /sso/saml/config
* Set SAML config
*/
@Post(SamlUrls.config, { middlewares: [samlLicensedMiddleware] })
@Post('/config', { middlewares: [samlLicensedMiddleware] })
@GlobalScope('saml:manage')
async configPost(req: SamlConfiguration.Update) {
const validationResult = await validate(req.body);
Expand All @@ -80,10 +77,9 @@ export class SamlController {
}

/**
* POST /sso/saml/config/toggle
* Set SAML config
* Toggle SAML status
*/
@Post(SamlUrls.configToggleEnabled, { middlewares: [samlLicensedMiddleware] })
@Post('/config/toggle', { middlewares: [samlLicensedMiddleware] })
@GlobalScope('saml:manage')
async toggleEnabledPost(req: SamlConfiguration.Toggle, res: express.Response) {
if (req.body.loginEnabled === undefined) {
Expand All @@ -94,19 +90,17 @@ export class SamlController {
}

/**
* GET /sso/saml/acs
* Assertion Consumer Service endpoint
*/
@Get(SamlUrls.acs, { middlewares: [samlLicensedMiddleware], skipAuth: true })
@Get('/acs', { middlewares: [samlLicensedMiddleware], skipAuth: true })
async acsGet(req: SamlConfiguration.AcsRequest, res: express.Response) {
return await this.acsHandler(req, res, 'redirect');
}

/**
* POST /sso/saml/acs
* Assertion Consumer Service endpoint
*/
@Post(SamlUrls.acs, { middlewares: [samlLicensedMiddleware], skipAuth: true })
@Post('/acs', { middlewares: [samlLicensedMiddleware], skipAuth: true })
async acsPost(req: SamlConfiguration.AcsRequest, res: express.Response) {
return await this.acsHandler(req, res, 'post');
}
Expand Down Expand Up @@ -140,9 +134,9 @@ export class SamlController {
if (isSamlLicensedAndEnabled()) {
this.authService.issueCookie(res, loginResult.authenticatedUser, req.browserId);
if (loginResult.onboardingRequired) {
return res.redirect(this.urlService.getInstanceBaseUrl() + SamlUrls.samlOnboarding);
return res.redirect(this.urlService.getInstanceBaseUrl() + '/saml/onboarding');
} else {
const redirectUrl = req.body?.RelayState ?? SamlUrls.defaultRedirect;
const redirectUrl = req.body?.RelayState ?? '/';
return res.redirect(this.urlService.getInstanceBaseUrl() + redirectUrl);
}
} else {
Expand All @@ -167,11 +161,10 @@ export class SamlController {
}

/**
* GET /sso/saml/initsso
* Access URL for implementing SP-init SSO
* This endpoint is available if SAML is licensed and enabled
*/
@Get(SamlUrls.initSSO, { middlewares: [samlLicensedAndEnabledMiddleware], skipAuth: true })
@Get('/initsso', { middlewares: [samlLicensedAndEnabledMiddleware], skipAuth: true })
async initSsoGet(req: express.Request, res: express.Response) {
let redirectUrl = '';
try {
Expand All @@ -192,13 +185,12 @@ export class SamlController {
}

/**
* GET /sso/saml/config/test
* Test SAML config
* This endpoint is available if SAML is licensed and the requestor is an instance owner
*/
@Get(SamlUrls.configTest, { middlewares: [samlLicensedMiddleware] })
@Get('/config/test', { middlewares: [samlLicensedMiddleware] })
@GlobalScope('saml:manage')
async configTestGet(req: AuthenticatedRequest, res: express.Response) {
async configTestGet(_: AuthenticatedRequest, res: express.Response) {
return await this.handleInitSSO(res, getServiceProviderConfigTestReturnUrl());
}

Expand Down
8 changes: 4 additions & 4 deletions packages/cli/src/sso/saml/serviceProvider.ee.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,21 +2,21 @@
import { Container } from 'typedi';
import type { ServiceProviderInstance } from 'samlify';
import { UrlService } from '@/services/url.service';
import { SamlUrls } from './constants';
import type { SamlPreferences } from './types/samlPreferences';

let serviceProviderInstance: ServiceProviderInstance | undefined;

export function getServiceProviderEntityId(): string {
return Container.get(UrlService).getInstanceBaseUrl() + SamlUrls.restMetadata;
return Container.get(UrlService).getInstanceBaseUrl() + '/rest/sso/saml/metadata';
}

export function getServiceProviderReturnUrl(): string {
return Container.get(UrlService).getInstanceBaseUrl() + SamlUrls.restAcs;
return Container.get(UrlService).getInstanceBaseUrl() + '/rest/sso/saml/acs';
}

export function getServiceProviderConfigTestReturnUrl(): string {
return Container.get(UrlService).getInstanceBaseUrl() + SamlUrls.configTestReturn;
// TODO: what is this URL?
return Container.get(UrlService).getInstanceBaseUrl() + '/config/test/return';
}

// TODO:SAML: make these configurable for the end user
Expand Down
105 changes: 52 additions & 53 deletions packages/cli/test/integration/saml/saml.api.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import type { AuthenticationMethod } from 'n8n-workflow';
import type { User } from '@db/entities/User';
import { setSamlLoginEnabled } from '@/sso/saml/samlHelpers';
import { getCurrentAuthenticationMethod, setCurrentAuthenticationMethod } from '@/sso/ssoHelpers';
import { SamlUrls } from '@/sso/saml/constants';
import { InternalHooks } from '@/InternalHooks';
import { SamlService } from '@/sso/saml/saml.service.ee';
import type { SamlUserAttributes } from '@/sso/saml/types/samlUserAttributes';
Expand Down Expand Up @@ -146,123 +145,123 @@ describe('Check endpoint permissions', () => {
beforeEach(async () => {
await enableSaml(true);
});

describe('Owner', () => {
test(`should be able to access ${SamlUrls.metadata}`, async () => {
await authOwnerAgent.get(`/sso/saml${SamlUrls.metadata}`).expect(200);
test('should be able to access GET /sso/saml/metadata', async () => {
await authOwnerAgent.get('/sso/saml/metadata').expect(200);
});

test(`should be able to access GET ${SamlUrls.config}`, async () => {
await authOwnerAgent.get(`/sso/saml${SamlUrls.config}`).expect(200);
test('should be able to access GET /sso/saml/config', async () => {
await authOwnerAgent.get('/sso/saml/config').expect(200);
});

test(`should be able to access POST ${SamlUrls.config}`, async () => {
await authOwnerAgent.post(`/sso/saml${SamlUrls.config}`).expect(200);
test('should be able to access POST /sso/saml/config', async () => {
await authOwnerAgent.post('/sso/saml/config').expect(200);
});

test(`should be able to access POST ${SamlUrls.configToggleEnabled}`, async () => {
await authOwnerAgent.post(`/sso/saml${SamlUrls.configToggleEnabled}`).expect(400);
test('should be able to access POST /sso/saml/config/toggle', async () => {
await authOwnerAgent.post('/sso/saml/config/toggle').expect(400);
});

test(`should be able to access GET ${SamlUrls.acs}`, async () => {
test('should be able to access GET /sso/saml/acs', async () => {
// Note that 401 here is coming from the missing SAML object,
// not from not being able to access the endpoint, so this is expected!
const response = await authOwnerAgent.get(`/sso/saml${SamlUrls.acs}`).expect(401);
const response = await authOwnerAgent.get('/sso/saml/acs').expect(401);
expect(response.text).toContain('SAML Authentication failed');
});

test(`should be able to access POST ${SamlUrls.acs}`, async () => {
test('should be able to access POST /sso/saml/acs', async () => {
// Note that 401 here is coming from the missing SAML object,
// not from not being able to access the endpoint, so this is expected!
const response = await authOwnerAgent.post(`/sso/saml${SamlUrls.acs}`).expect(401);
const response = await authOwnerAgent.post('/sso/saml/acs').expect(401);
expect(response.text).toContain('SAML Authentication failed');
});

test(`should be able to access GET ${SamlUrls.initSSO}`, async () => {
await authOwnerAgent.get(`/sso/saml${SamlUrls.initSSO}`).expect(200);
test('should be able to access GET /sso/saml/initsso', async () => {
await authOwnerAgent.get('/sso/saml/initsso').expect(200);
});

test(`should be able to access GET ${SamlUrls.configTest}`, async () => {
await authOwnerAgent.get(`/sso/saml${SamlUrls.configTest}`).expect(200);
test('should be able to access GET /sso/saml/config/test', async () => {
await authOwnerAgent.get('/sso/saml/config/test').expect(200);
});
});

describe('Authenticated Member', () => {
test(`should be able to access ${SamlUrls.metadata}`, async () => {
await authMemberAgent.get(`/sso/saml${SamlUrls.metadata}`).expect(200);
test('should be able to access GET /sso/saml/metadata', async () => {
await authMemberAgent.get('/sso/saml/metadata').expect(200);
});

test(`should be able to access GET ${SamlUrls.config}`, async () => {
await authMemberAgent.get(`/sso/saml${SamlUrls.config}`).expect(200);
test('should be able to access GET /sso/saml/config', async () => {
await authMemberAgent.get('/sso/saml/config').expect(200);
});

test(`should NOT be able to access POST ${SamlUrls.config}`, async () => {
await authMemberAgent.post(`/sso/saml${SamlUrls.config}`).expect(403);
test('should NOT be able to access POST /sso/saml/config', async () => {
await authMemberAgent.post('/sso/saml/config').expect(403);
});

test(`should NOT be able to access POST ${SamlUrls.configToggleEnabled}`, async () => {
await authMemberAgent.post(`/sso/saml${SamlUrls.configToggleEnabled}`).expect(403);
test('should NOT be able to access POST /sso/saml/config/toggle', async () => {
await authMemberAgent.post('/sso/saml/config/toggle').expect(403);
});

test(`should be able to access GET ${SamlUrls.acs}`, async () => {
test('should be able to access GET /sso/saml/acs', async () => {
// Note that 401 here is coming from the missing SAML object,
// not from not being able to access the endpoint, so this is expected!
const response = await authMemberAgent.get(`/sso/saml${SamlUrls.acs}`).expect(401);
const response = await authMemberAgent.get('/sso/saml/acs').expect(401);
expect(response.text).toContain('SAML Authentication failed');
});

test(`should be able to access POST ${SamlUrls.acs}`, async () => {
test('should be able to access POST /sso/saml/acs', async () => {
// Note that 401 here is coming from the missing SAML object,
// not from not being able to access the endpoint, so this is expected!
const response = await authMemberAgent.post(`/sso/saml${SamlUrls.acs}`).expect(401);
const response = await authMemberAgent.post('/sso/saml/acs').expect(401);
expect(response.text).toContain('SAML Authentication failed');
});

test(`should be able to access GET ${SamlUrls.initSSO}`, async () => {
await authMemberAgent.get(`/sso/saml${SamlUrls.initSSO}`).expect(200);
test('should be able to access GET /sso/saml/initsso', async () => {
await authMemberAgent.get('/sso/saml/initsso').expect(200);
});

test(`should NOT be able to access GET ${SamlUrls.configTest}`, async () => {
await authMemberAgent.get(`/sso/saml${SamlUrls.configTest}`).expect(403);
test('should NOT be able to access GET /sso/saml/config/test', async () => {
await authMemberAgent.get('/sso/saml/config/test').expect(403);
});
});
describe('Non-Authenticated User', () => {
test(`should be able to access ${SamlUrls.metadata}`, async () => {
await testServer.authlessAgent.get(`/sso/saml${SamlUrls.metadata}`).expect(200);
test('should be able to access /sso/saml/metadata', async () => {
await testServer.authlessAgent.get('/sso/saml/metadata').expect(200);
});

test(`should NOT be able to access GET ${SamlUrls.config}`, async () => {
await testServer.authlessAgent.get(`/sso/saml${SamlUrls.config}`).expect(401);
test('should NOT be able to access GET /sso/saml/config', async () => {
await testServer.authlessAgent.get('/sso/saml/config').expect(401);
});

test(`should NOT be able to access POST ${SamlUrls.config}`, async () => {
await testServer.authlessAgent.post(`/sso/saml${SamlUrls.config}`).expect(401);
test('should NOT be able to access POST /sso/saml/config', async () => {
await testServer.authlessAgent.post('/sso/saml/config').expect(401);
});

test(`should NOT be able to access POST ${SamlUrls.configToggleEnabled}`, async () => {
await testServer.authlessAgent.post(`/sso/saml${SamlUrls.configToggleEnabled}`).expect(401);
test('should NOT be able to access POST /sso/saml/config/toggle', async () => {
await testServer.authlessAgent.post('/sso/saml/config/toggle').expect(401);
});

test(`should be able to access GET ${SamlUrls.acs}`, async () => {
test('should be able to access GET /sso/saml/acs', async () => {
// Note that 401 here is coming from the missing SAML object,
// not from not being able to access the endpoint, so this is expected!
const response = await testServer.authlessAgent.get(`/sso/saml${SamlUrls.acs}`).expect(401);
const response = await testServer.authlessAgent.get('/sso/saml/acs').expect(401);
expect(response.text).toContain('SAML Authentication failed');
});

test(`should be able to access POST ${SamlUrls.acs}`, async () => {
test('should be able to access POST /sso/saml/acs', async () => {
// Note that 401 here is coming from the missing SAML object,
// not from not being able to access the endpoint, so this is expected!
const response = await testServer.authlessAgent.post(`/sso/saml${SamlUrls.acs}`).expect(401);
const response = await testServer.authlessAgent.post('/sso/saml/acs').expect(401);
expect(response.text).toContain('SAML Authentication failed');
});

test(`should be able to access GET ${SamlUrls.initSSO}`, async () => {
const response = await testServer.authlessAgent
.get(`/sso/saml${SamlUrls.initSSO}`)
.expect(200);
test('should be able to access GET /sso/saml/initsso', async () => {
await testServer.authlessAgent.get('/sso/saml/initsso').expect(200);
});

test(`should NOT be able to access GET ${SamlUrls.configTest}`, async () => {
await testServer.authlessAgent.get(`/sso/saml${SamlUrls.configTest}`).expect(401);
test('should NOT be able to access GET /sso/saml/config/test', async () => {
await testServer.authlessAgent.get('/sso/saml/config/test').expect(401);
});
});
});
Expand Down Expand Up @@ -304,7 +303,7 @@ describe('SAML login flow', () => {
return;
},
);
const response = await authOwnerAgent.post(`/sso/saml${SamlUrls.acs}`).expect(302);
await authOwnerAgent.post('/sso/saml/acs').expect(302);
expect(mockedHookOnUserLoginSuccess).toBeCalled();
mockedHookOnUserLoginSuccess.mockRestore();
mockedHandleSamlLogin.mockRestore();
Expand Down Expand Up @@ -346,7 +345,7 @@ describe('SAML login flow', () => {
return;
},
);
const response = await authOwnerAgent.post(`/sso/saml${SamlUrls.acs}`).expect(401);
await authOwnerAgent.post('/sso/saml/acs').expect(401);
expect(mockedHookOnUserLoginFailed).toBeCalled();
mockedHookOnUserLoginFailed.mockRestore();
mockedHandleSamlLogin.mockRestore();
Expand Down
Loading