@sap/cds-mtxs
Version:
SAP Cloud Application Programming Model - Multitenancy library
116 lines (96 loc) • 4.92 kB
JavaScript
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')
}
}