UNPKG

@sap/cds-mtxs

Version:

SAP Cloud Application Programming Model - Multitenancy library

116 lines (96 loc) 4.92 kB
const { X509Certificate } = require('node:crypto') const cds = require('@sap/cds/lib') const LOG = cds.log('mtx'), DEBUG = cds.debug('mtx') module.exports = class SmsProvisioningService extends require('./abstract-provisioning-service') { async init() { this.on('UPDATE', 'tenant', this._create) this.on('READ', 'tenant', this._read) this.on('DELETE', 'tenant', this._delete) this.on('READ', 'dependencies', this._dependencies) this.on('upgrade', super._upgrade) this.on('getAppUrl', super._getAppUrl) super.init() } async _create(context) { this._validateCertificate(context) const { subscriber = {} } = context.data const { app_tid: subscribedTenantId, subaccountSubdomain: subscribedSubdomain } = subscriber DEBUG?.('Subscription Manager service subscription payload:', context.data) const internalSubscriptionPayload = { subscribedTenantId, subscribedSubdomain, ...context.data // technical data that remains the same + data that we do not care of } const result = await super._create(context, internalSubscriptionPayload) cds.context.http.res.set('content-type', 'application/json') // workaround for return value -> skip original key that is added by runtime, otherwise it is not accepted by subscription-manager if (!result.message) cds.context.http.res.send({ applicationURL: result }) else return { applicationURL: result } } async _delete(context) { this._validateCertificate(context) return super._delete(context) } async _dependencies(context) { this._validateCertificate(context) return super._dependencies(context) } async _read(context) { this._validateCertificate(context) return super._read(context) } _parseHeaders(headers) { const { prefer, status_callback } = headers ?? {} const { subscription_manager_url } = this._getCredentials() const callbackUrl = (status_callback && subscription_manager_url && new URL(status_callback, subscription_manager_url).toString()) return { callbackUrl, isSync: !(prefer?.includes('respond-async') || callbackUrl) } } async _sendCallback(status, message, applicationUrl) { const originalRequest = cds.context?.http?.req const { callbackUrl } = this._parseHeaders(originalRequest?.headers) if (callbackUrl) { DEBUG?.('sending callback to', callbackUrl) const payload = { status, message, applicationUrl } try { await this.sendResult(callbackUrl, payload, null, `Bearer ${await this._token()}`) } catch (error) { LOG.error(error) } } } _validateCertificate(req) { const CERTIFICATE_HEADER = '-----BEGIN CERTIFICATE-----' const header = cds.env.requires['cds.xt.SmsProvisioningService']?.clientCertificateHeader const certHeader = (header && req.headers?.[header]) ?? req.headers?.['X-Forwarded-Client-Cert'] ?? req.headers?.['x-forwarded-client-cert'] if (!certHeader) return req.reject(401, `Missing certificate header: ${certHeader}`) // check for kyma header, see https://www.envoyproxy.io/docs/envoy/latest/configuration/http/http_conn_man/headers#x-forwarded-client-cert const matchEnvoy = certHeader.match(/Cert="([^"]*)/); let certString = matchEnvoy ? decodeURIComponent(matchEnvoy[1]) : certHeader; let cert try { const encoding = certString.startsWith(CERTIFICATE_HEADER) ? 'utf-8' : 'base64' if(encoding == 'utf-8'){ certString = certString.replaceAll("\\n", "\n"); } const buffer = Buffer.from(certString, encoding) cert = new X509Certificate(buffer) } catch { return req.reject(401, 'Invalid certificate') } const { callback_certificate_issuer, callback_certificate_subject } = cds.env.requires['cds.xt.SmsProvisioningService'].credentials ?? {} if (!callback_certificate_issuer || !callback_certificate_subject) return req.reject(401, 'No subscription-manager binding') const isIssuerValid = isValid(callback_certificate_issuer, cert.issuer) const isSubjectValid = isValid(callback_certificate_subject, cert.subject) function isValid(expected, fromCert) { return Object.entries(JSON.parse(expected)).every(([k, v]) => { if (v === '*') return true if (Array.isArray(v)) return v.some(v => fromCert.includes(`${k}=${v}`)) else return fromCert.includes(`${k}=${v}`) }) } if (!isIssuerValid || !isSubjectValid) return req.reject(403, 'Certificate check failed') } }