diff --git a/packages/backend/src/controllers/api/v1/admin/saml-auth-providers/get-saml-auth-providers.ee.js b/packages/backend/src/controllers/api/v1/admin/saml-auth-providers/get-saml-auth-providers.ee.js new file mode 100644 index 00000000..2d39cc55 --- /dev/null +++ b/packages/backend/src/controllers/api/v1/admin/saml-auth-providers/get-saml-auth-providers.ee.js @@ -0,0 +1,11 @@ +import { renderObject } from '../../../../../helpers/renderer.js'; +import SamlAuthProvider from '../../../../../models/saml-auth-provider.ee.js'; + +export default async (request, response) => { + const samlAuthProviders = await SamlAuthProvider.query().orderBy( + 'created_at', + 'desc' + ); + + renderObject(response, samlAuthProviders); +}; diff --git a/packages/backend/src/controllers/api/v1/admin/saml-auth-providers/get-saml-auth-providers.ee.test.js b/packages/backend/src/controllers/api/v1/admin/saml-auth-providers/get-saml-auth-providers.ee.test.js new file mode 100644 index 00000000..7a589821 --- /dev/null +++ b/packages/backend/src/controllers/api/v1/admin/saml-auth-providers/get-saml-auth-providers.ee.test.js @@ -0,0 +1,39 @@ +import { vi, describe, it, expect, beforeEach } from 'vitest'; +import request from 'supertest'; +import app from '../../../../../app.js'; +import createAuthTokenByUserId from '../../../../../helpers/create-auth-token-by-user-id.js'; +import { createRole } from '../../../../../../test/factories/role.js'; +import { createUser } from '../../../../../../test/factories/user.js'; +import { createSamlAuthProvider } from '../../../../../../test/factories/saml-auth-provider.ee.js'; +import getSamlAuthProvidersMock from '../../../../../../test/mocks/rest/api/v1/saml-auth-providers/get-saml-auth-providers.ee.js'; +import * as license from '../../../../../helpers/license.ee.js'; + +describe('GET /api/v1/admin/saml-auth-providers', () => { + let samlAuthProviderOne, samlAuthProviderTwo, currentUser, token; + + beforeEach(async () => { + const role = await createRole({ key: 'admin' }); + currentUser = await createUser({ roleId: role.id }); + + samlAuthProviderOne = await createSamlAuthProvider(); + samlAuthProviderTwo = await createSamlAuthProvider(); + + token = createAuthTokenByUserId(currentUser.id); + }); + + it('should return saml auth providers', async () => { + vi.spyOn(license, 'hasValidLicense').mockResolvedValue(true); + + const response = await request(app) + .get('/api/v1/admin/saml-auth-providers') + .set('Authorization', token) + .expect(200); + + const expectedPayload = await getSamlAuthProvidersMock([ + samlAuthProviderTwo, + samlAuthProviderOne, + ]); + + expect(response.body).toEqual(expectedPayload); + }); +}); diff --git a/packages/backend/src/helpers/authorization.js b/packages/backend/src/helpers/authorization.js index 28cefc59..b8d40136 100644 --- a/packages/backend/src/helpers/authorization.js +++ b/packages/backend/src/helpers/authorization.js @@ -20,3 +20,13 @@ export const authorizeUser = async (request, response, next) => { return response.status(403).end(); } }; + +export const authorizeAdmin = async (request, response, next) => { + const role = await request.currentUser.$relatedQuery('role'); + + if (role?.isAdmin) { + next(); + } else { + return response.status(403).end(); + } +}; diff --git a/packages/backend/src/helpers/check-is-enterprise.js b/packages/backend/src/helpers/check-is-enterprise.js new file mode 100644 index 00000000..0180eea2 --- /dev/null +++ b/packages/backend/src/helpers/check-is-enterprise.js @@ -0,0 +1,9 @@ +import { hasValidLicense } from './license.ee.js'; + +export const checkIsEnterprise = async (request, response, next) => { + if (await hasValidLicense()) { + next(); + } else { + return response.status(404).end(); + } +}; diff --git a/packages/backend/src/helpers/logger.js b/packages/backend/src/helpers/logger.js index c929e5ac..d202869c 100644 --- a/packages/backend/src/helpers/logger.js +++ b/packages/backend/src/helpers/logger.js @@ -4,8 +4,8 @@ import appConfig from '../config/app.js'; const levels = { error: 0, warn: 1, - info: 2, - http: 3, + http: 2, + info: 3, debug: 4, }; diff --git a/packages/backend/src/helpers/renderer.js b/packages/backend/src/helpers/renderer.js index a465e73d..ac3fa46d 100644 --- a/packages/backend/src/helpers/renderer.js +++ b/packages/backend/src/helpers/renderer.js @@ -15,6 +15,8 @@ const renderObject = (response, object) => { let data = isPaginated(object) ? object.records : object; const type = isPaginated(object) ? object.records[0].constructor.name + : Array.isArray(object) + ? object[0].constructor.name : object.constructor.name; const serializer = serializers[type]; diff --git a/packages/backend/src/routes/api/v1/saml-auth-providers.ee.js b/packages/backend/src/routes/api/v1/saml-auth-providers.ee.js new file mode 100644 index 00000000..b8028b08 --- /dev/null +++ b/packages/backend/src/routes/api/v1/saml-auth-providers.ee.js @@ -0,0 +1,17 @@ +import { Router } from 'express'; +import { authenticateUser } from '../../../helpers/authentication.js'; +import { authorizeAdmin } from '../../../helpers/authorization.js'; +import { checkIsEnterprise } from '../../../helpers/check-is-enterprise.js'; +import getSamlAuthProvidersAction from '../../../controllers/api/v1/admin/saml-auth-providers/get-saml-auth-providers.ee.js'; + +const router = Router(); + +router.get( + '/', + authenticateUser, + authorizeAdmin, + checkIsEnterprise, + getSamlAuthProvidersAction +); + +export default router; diff --git a/packages/backend/src/routes/index.js b/packages/backend/src/routes/index.js index 215a7326..0aec5e23 100644 --- a/packages/backend/src/routes/index.js +++ b/packages/backend/src/routes/index.js @@ -5,6 +5,7 @@ import paddleRouter from './paddle.ee.js'; import healthcheckRouter from './healthcheck.js'; import automatischRouter from './api/v1/automatisch.js'; import usersRouter from './api/v1/users.js'; +import samlAuthProvidersRouter from './api/v1/saml-auth-providers.ee.js'; const router = Router(); @@ -14,5 +15,6 @@ router.use('/paddle', paddleRouter); router.use('/healthcheck', healthcheckRouter); router.use('/api/v1/automatisch', automatischRouter); router.use('/api/v1/users', usersRouter); +router.use('/api/v1/admin/saml-auth-providers', samlAuthProvidersRouter); export default router; diff --git a/packages/backend/src/serializers/index.js b/packages/backend/src/serializers/index.js index fc4f257e..b8f30924 100644 --- a/packages/backend/src/serializers/index.js +++ b/packages/backend/src/serializers/index.js @@ -1,11 +1,13 @@ import userSerializer from './user.js'; import roleSerializer from './role.js'; import permissionSerializer from './permission.js'; +import samlAuthProviderSerializer from './saml-auth-provider.ee.js'; const serializers = { User: userSerializer, Role: roleSerializer, Permission: permissionSerializer, + SamlAuthProvider: samlAuthProviderSerializer, }; export default serializers; diff --git a/packages/backend/src/serializers/saml-auth-provider.ee.js b/packages/backend/src/serializers/saml-auth-provider.ee.js new file mode 100644 index 00000000..43422704 --- /dev/null +++ b/packages/backend/src/serializers/saml-auth-provider.ee.js @@ -0,0 +1,18 @@ +const samlAuthProviderSerializer = (samlAuthProvider) => { + return { + id: samlAuthProvider.id, + name: samlAuthProvider.name, + certificate: samlAuthProvider.certificate, + signatureAlgorithm: samlAuthProvider.signatureAlgorithm, + issuer: samlAuthProvider.issuer, + entryPoint: samlAuthProvider.entryPoint, + firstnameAttributeName: samlAuthProvider.firstnameAttributeName, + surnameAttributeName: samlAuthProvider.surnameAttributeName, + emailAttributeName: samlAuthProvider.emailAttributeName, + roleAttributeName: samlAuthProvider.roleAttributeName, + active: samlAuthProvider.active, + defaultRoleId: samlAuthProvider.defaultRoleId, + }; +}; + +export default samlAuthProviderSerializer; diff --git a/packages/backend/src/serializers/saml-auth-provider.ee.test.js b/packages/backend/src/serializers/saml-auth-provider.ee.test.js new file mode 100644 index 00000000..86250b72 --- /dev/null +++ b/packages/backend/src/serializers/saml-auth-provider.ee.test.js @@ -0,0 +1,32 @@ +import { describe, it, expect, beforeEach } from 'vitest'; +import { createSamlAuthProvider } from '../../test/factories/saml-auth-provider.ee.js'; +import samlAuthProviderSerializer from './saml-auth-provider.ee.js'; + +describe('samlAuthProviderSerializer', () => { + let samlAuthProvider; + + beforeEach(async () => { + samlAuthProvider = await createSamlAuthProvider(); + }); + + it('should return saml auth provider data', async () => { + const expectedPayload = { + id: samlAuthProvider.id, + name: samlAuthProvider.name, + certificate: samlAuthProvider.certificate, + signatureAlgorithm: samlAuthProvider.signatureAlgorithm, + issuer: samlAuthProvider.issuer, + entryPoint: samlAuthProvider.entryPoint, + firstnameAttributeName: samlAuthProvider.firstnameAttributeName, + surnameAttributeName: samlAuthProvider.surnameAttributeName, + emailAttributeName: samlAuthProvider.emailAttributeName, + roleAttributeName: samlAuthProvider.roleAttributeName, + active: samlAuthProvider.active, + defaultRoleId: samlAuthProvider.defaultRoleId, + }; + + expect(samlAuthProviderSerializer(samlAuthProvider)).toEqual( + expectedPayload + ); + }); +}); diff --git a/packages/backend/test/factories/saml-auth-provider.ee.js b/packages/backend/test/factories/saml-auth-provider.ee.js new file mode 100644 index 00000000..e18bc4c6 --- /dev/null +++ b/packages/backend/test/factories/saml-auth-provider.ee.js @@ -0,0 +1,33 @@ +import { createRole } from './role'; +import SamlAuthProvider from '../../src/models/saml-auth-provider.ee.js'; + +export const createSamlAuthProvider = async (params = {}) => { + params.name = params?.name || 'Keycloak SAML'; + params.certificate = params?.certificate || 'certificate'; + params.signatureAlgorithm = params?.signatureAlgorithm || 'sha512'; + + params.entryPoint = + params?.entryPoint || + 'https://example.com/auth/realms/automatisch/protocol/saml'; + + params.issuer = params?.issuer || 'automatisch-client'; + + params.firstnameAttributeName = + params?.firstnameAttributeName || 'urn:oid:2.1.1.42'; + + params.surnameAttributeName = + params?.surnameAttributeName || 'urn:oid:2.1.1.4'; + + params.emailAttributeName = + params?.emailAttributeName || 'urn:oid:1.1.2342.19200300.100.1.1'; + + params.roleAttributeName = params?.roleAttributeName || 'Role'; + params.defaultRoleId = params?.defaultRoleId || (await createRole()).id; + params.active = params?.active || true; + + const samlAuthProvider = await SamlAuthProvider.query() + .insert(params) + .returning('*'); + + return samlAuthProvider; +}; diff --git a/packages/backend/test/mocks/rest/api/v1/saml-auth-providers/get-saml-auth-providers.ee.js b/packages/backend/test/mocks/rest/api/v1/saml-auth-providers/get-saml-auth-providers.ee.js new file mode 100644 index 00000000..30d5bfc5 --- /dev/null +++ b/packages/backend/test/mocks/rest/api/v1/saml-auth-providers/get-saml-auth-providers.ee.js @@ -0,0 +1,31 @@ +const getSamlAuthProvidersMock = async (samlAuthProviders) => { + const data = samlAuthProviders.map((samlAuthProvider) => { + return { + active: samlAuthProvider.active, + certificate: samlAuthProvider.certificate, + defaultRoleId: samlAuthProvider.defaultRoleId, + emailAttributeName: samlAuthProvider.emailAttributeName, + entryPoint: samlAuthProvider.entryPoint, + firstnameAttributeName: samlAuthProvider.firstnameAttributeName, + id: samlAuthProvider.id, + issuer: samlAuthProvider.issuer, + name: samlAuthProvider.name, + roleAttributeName: samlAuthProvider.roleAttributeName, + signatureAlgorithm: samlAuthProvider.signatureAlgorithm, + surnameAttributeName: samlAuthProvider.surnameAttributeName, + }; + }); + + return { + data: data, + meta: { + count: data.length, + currentPage: null, + isArray: true, + totalPages: null, + type: 'SamlAuthProvider', + }, + }; +}; + +export default getSamlAuthProvidersMock;