From 863f1555e34589471f40ea95e4efba7ce2d746f6 Mon Sep 17 00:00:00 2001 From: Albin Ramovic Date: Wed, 29 Jan 2025 10:22:28 +0100 Subject: [PATCH] additional changes related to ucl-mtls --- __tests__/unittest/authentication.test.js | 5 +- lib/authentication.js | 104 +++++++++++++++------- lib/constants.js | 49 +++++----- lib/defaults.js | 10 +-- lib/index.js | 1 - lib/plugin.js | 17 +++- xmpl/default-env.json | 2 +- 7 files changed, 123 insertions(+), 65 deletions(-) diff --git a/__tests__/unittest/authentication.test.js b/__tests__/unittest/authentication.test.js index dbcf678..ccae1f2 100644 --- a/__tests__/unittest/authentication.test.js +++ b/__tests__/unittest/authentication.test.js @@ -1,6 +1,6 @@ const cds = require('@sap/cds'); const { AUTHENTICATION_TYPE } = require('../../lib/constants'); -const authenticate = require('../../lib/authentication'); +const { authenticate }= require('../../lib/authentication'); describe('Authentication Middleware', () => { const mockValidUser = { admin: "secret" }; @@ -131,6 +131,9 @@ describe('Authentication Middleware', () => { describe("UCL mTLS Authentication", () => { beforeEach(async () => { + // mock cds.context + // cds.context = { + // tru // server = fastify(); // server.setErrorHandler(errorHandler); // await setupAuthentication(server, { diff --git a/lib/authentication.js b/lib/authentication.js index 2be25ca..f0732a2 100644 --- a/lib/authentication.js +++ b/lib/authentication.js @@ -1,41 +1,62 @@ const cds = require("@sap/cds"); -const { AUTHENTICATION_TYPE } = require("./constants"); +const { AUTHENTICATION_TYPE, CERT_SUBJECT_HEADER_KEY } = require("./constants"); +const { Logger } = require("./logger"); -/** - * Depending on authentication type, return the credentials in case of the basic authentication - * or mtls endpoints in case of UCL-mtls - */ -function getAuthenticationDetails() { - function getBasicAuthDetails() { - const basicAuthData = process.env.APP_USERS ? - JSON.parse(process.env.APP_USERS) : - cds.env.authentication.credentials ? - JSON.parse(cds.env.authentication.credentials) : {}; - - return Object.entries(basicAuthData).map(([username, password]) => ({ username, password }))[0]; - } +function getAuthenticationType() { + return process.env.ORD_AUTH || cds.env.authentication?.type || AUTHENTICATION_TYPE.Open; +} - switch (process.env.ORD_AUTH || cds.env.authentication?.type || AUTHENTICATION_TYPE.Open) { - case AUTHENTICATION_TYPE.Basic: +function getAuthData(type) { + switch (type) { + case AUTHENTICATION_TYPE.Basic: { + const basicAuthData = process.env.APP_USERS ? + JSON.parse(process.env.APP_USERS) : + cds.env.authentication.credentials ? + JSON.parse(cds.env.authentication.credentials) : {}; return { - type: AUTHENTICATION_TYPE.Basic, - ...getBasicAuthDetails() - }; - case AUTHENTICATION_TYPE.UclMtls: + type, + ...Object.entries(basicAuthData).map(([username, password]) => ({ username, password }))[0] + } + } + case AUTHENTICATION_TYPE.UclMtls: { + const infoEndpoints = process.env.UCL_MTLS_ENDPOINTS ? + JSON.parse(process.env.UCL_MTLS_ENDPOINTS) : + cds.env.authentication.uclMtlsEndpoints ? + cds.env.authentication.uclMtlsEndpoints : []; return { - type: AUTHENTICATION_TYPE.UclMtls, - infoEndpoints: process.env.UCL_MTLS_ENDPOINTS ?? cds.env.authentication.uclMtlsEndpoints + type, + infoEndpoints }; + } default: - return { - type: AUTHENTICATION_TYPE.Open - }; + return {}; } } +async function fetchTrustedSubject(endpoint) { + try { + console.log('TrustedSubjectService:', endpoint); + return null; + // const resp = await fetch(endpoint); + // const jsonBody = (await resp.json()); + // return jsonBody.certSubject; + } catch (error) { + Logger.error('TrustedSubjectService:', error.message); + return null; + } +} + +async function getTrustedSubjects() { + const authData = getAuthData(AUTHENTICATION_TYPE.UclMtls); + const subjects = await Promise.all(authData.infoEndpoints.map((endpoint) => fetchTrustedSubject(endpoint))); + return subjects.filter((subject) => subject !== null); +} + // Middleware for authentication -module.exports = (req, res, next) => { - const authData = getAuthenticationDetails(); +async function authenticate(req, res, next) { + const authType = getAuthenticationType(); + const authData = getAuthData(authType); + switch (authData.type) { case AUTHENTICATION_TYPE.Basic: { const authHeader = req.headers['authorization']; @@ -57,15 +78,36 @@ module.exports = (req, res, next) => { } break; } - case AUTHENTICATION_TYPE.UclMtls: + case AUTHENTICATION_TYPE.UclMtls: { + const certSubjectEncoded = req.headers[CERT_SUBJECT_HEADER_KEY] || ""; + const certSubject = Buffer.from(certSubjectEncoded, "base64").toString("ascii"); + const certSubjectTokens = certSubject.split(",").filter((token) => token); + + const matchesSomeTrustedSubject = cds.context.trustedSubjects + .map((subj) => subj.split(",").map((token) => token.trim())) + .some( + (trustedIssuerTokens) => + trustedIssuerTokens.length === certSubjectTokens.length && + trustedIssuerTokens.every((token) => certSubjectTokens.includes(token)), + ); + + if (!certSubject || !matchesSomeTrustedSubject) { + return res.status(401).send('Certificate subject header is missing or invalid'); + } + res.status(200); next(); break; - // const infoEndpoints = cds.env.infoEndpoints - // return uclMtlsAuthentication(req, res, next); + } default: // Open access res.status(200); next(); break; } -} \ No newline at end of file +} + +module.exports = { + authenticate, + getAuthenticationType, + getTrustedSubjects, +}; \ No newline at end of file diff --git a/lib/constants.js b/lib/constants.js index bae8dc7..78efd5f 100644 --- a/lib/constants.js +++ b/lib/constants.js @@ -1,3 +1,21 @@ +const AUTHENTICATION_TYPE = Object.freeze({ + Open: "open", + Basic: "basic", + UclMtls: "ucl-mtls", +}); + +const ORD_ACCESS_STRATEGY = Object.freeze({ + Open: "open", + Basic: "sap.businesshub:basic-auth:v1", + UclMtls: "sap:cmp-mtls:v1", +}); + +const AUTH_TYPE_ORD_ACCESS_STRATEGY_MAP = Object.freeze({ + [AUTHENTICATION_TYPE.Open]: ORD_ACCESS_STRATEGY.Open, + [AUTHENTICATION_TYPE.Basic]: ORD_ACCESS_STRATEGY.Basic, + [AUTHENTICATION_TYPE.UclMtls]: ORD_ACCESS_STRATEGY.UclMtls, +}); + const CDS_ELEMENT_KIND = Object.freeze({ action: "action", annotation: "annotation", @@ -9,6 +27,8 @@ const CDS_ELEMENT_KIND = Object.freeze({ type: "type", }); +const CERT_SUBJECT_HEADER_KEY = "x-ssl-client-subject-dn"; + const COMPILER_TYPES = Object.freeze({ oas3: "oas3", asyncapi2: "asyncapi2", @@ -46,43 +66,26 @@ const RESOURCE_VISIBILITY = Object.freeze({ private: "private", }); -const SEM_VERSION_REGEX = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$/; - const SHORT_DESCRIPTION_PREFIX = "Short description of "; -const AUTHENTICATION_TYPE = Object.freeze({ - Open: "open", - Basic: "basic", - UclMtls: "ucl-mtls", -}); - -const ORD_ACCESS_STRATEGY = Object.freeze({ - Open: "open", - Basic: "sap.businesshub:basic-auth:v1", - UclMtls: "sap:cmp-mtls:v1", -}); - -const AUTH_TYPE_ORD_ACCESS_STRATEGY_MAP = Object.freeze({ - [AUTHENTICATION_TYPE.Open]: ORD_ACCESS_STRATEGY.Open, - [AUTHENTICATION_TYPE.Basic]: ORD_ACCESS_STRATEGY.Basic, - [AUTHENTICATION_TYPE.UclMtls]: ORD_ACCESS_STRATEGY.UclMtls, -}); +const SEM_VERSION_REGEX = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$/; module.exports = { + AUTHENTICATION_TYPE, + AUTH_TYPE_ORD_ACCESS_STRATEGY_MAP, CDS_ELEMENT_KIND, + CERT_SUBJECT_HEADER_KEY, COMPILER_TYPES, CONTENT_MERGE_KEY, DESCRIPTION_PREFIX, ENTITY_RELATIONSHIP_ANNOTATION, LEVEL, OPEN_RESOURCE_DISCOVERY_VERSION, + ORD_ACCESS_STRATEGY, ORD_EXTENSIONS_PREFIX, ORD_ODM_ENTITY_NAME_ANNOTATION, ORD_RESOURCE_TYPE, RESOURCE_VISIBILITY, - SEM_VERSION_REGEX, SHORT_DESCRIPTION_PREFIX, - AUTHENTICATION_TYPE, - ORD_ACCESS_STRATEGY, - AUTH_TYPE_ORD_ACCESS_STRATEGY_MAP + SEM_VERSION_REGEX }; diff --git a/lib/defaults.js b/lib/defaults.js index bb9ff23..5780e82 100644 --- a/lib/defaults.js +++ b/lib/defaults.js @@ -1,11 +1,11 @@ -const cds = require("@sap/cds"); const { + AUTHENTICATION_TYPE, + AUTH_TYPE_ORD_ACCESS_STRATEGY_MAP, DESCRIPTION_PREFIX, OPEN_RESOURCE_DISCOVERY_VERSION, SHORT_DESCRIPTION_PREFIX, - AUTHENTICATION_TYPE, - AUTH_TYPE_ORD_ACCESS_STRATEGY_MAP,} - = require("./constants"); +} = require("./constants"); +const { getAuthenticationType } = require("./authentication"); const regexWithRemoval = (name) => { return name?.replace(/[^a-zA-Z0-9]/g, ""); @@ -93,7 +93,7 @@ module.exports = { url: "/open-resource-discovery/v1/documents/1", accessStrategies: [ { - type: "open"// AUTH_TYPE_ORD_ACCESS_STRATEGY_MAP[cds.env.authentication.type] || AUTHENTICATION_TYPE.Open, + type: AUTH_TYPE_ORD_ACCESS_STRATEGY_MAP[getAuthenticationType()] || AUTHENTICATION_TYPE.Open }, ], }, diff --git a/lib/index.js b/lib/index.js index 46b2165..1248b85 100644 --- a/lib/index.js +++ b/lib/index.js @@ -1,5 +1,4 @@ module.exports = { - authenticate: require("./authentication.js"), defaults: require("./defaults.js"), getMetadata: require("./metaData.js"), ord: require("./ord.js"), diff --git a/lib/plugin.js b/lib/plugin.js index 22ea4c1..7daaabe 100644 --- a/lib/plugin.js +++ b/lib/plugin.js @@ -1,8 +1,15 @@ const cds = require("@sap/cds"); -const { Logger } = require("./logger"); -const { authenticate, ord, getMetadata, defaults } = require("./"); +const { ord, getMetadata, defaults, Logger } = require("./"); +const { authenticate, getTrustedSubjects } = require("./authentication"); + +cds.on("bootstrap", async (app) => { + if (!cds.context) { + cds.context = {}; + } + + const trustedSubjects = await getTrustedSubjects(); + cds.context.trustedSubjects = trustedSubjects; -cds.on("bootstrap", (app) => { app.use("/.well-known/open-resource-discovery", async (req, res) => { if (req.url === "/") { res.status(200).send(defaults.baseTemplate); @@ -29,4 +36,8 @@ cds.on("bootstrap", (app) => { }); }); +cds.on("shutdown", () => { + delete cds.context.trustedSubjects +}); + module.exports = cds.server; diff --git a/xmpl/default-env.json b/xmpl/default-env.json index 20f0a30..051e945 100644 --- a/xmpl/default-env.json +++ b/xmpl/default-env.json @@ -4,7 +4,7 @@ "ORD_BASE_URL": "http://localhost:8080", "ORD_SOURCE_TYPE": "local", "ORD_DIRECTORY": "./example", - "ORD_AUTH": "basic", + "ORD_AUTH": "open", "APP_USERS": { "admin": "secret" },