UNPKG

@sap/cds

Version:

SAP Cloud Application Programming Model - CDS for Node.js

159 lines (137 loc) 6.18 kB
const cds = require('../../cds.js') const express = require('express') const getTenantInfo = require('./getTenantInfo.js') const _isAll = a => a && a.includes('all') const _xsuaa_fallback = () => { let xsuaa = cds.env.requires.messaging.xsuaa xsuaa = typeof xsuaa === 'string' ? xsuaa : 'xsuaa' const xsuaa_config = cds.env.requires[xsuaa] if (!xsuaa_config) throw new Error(`Fallback XSUAA instance '${xsuaa}' not found!`) if (!xsuaa_config.credentials) throw new Error(`Fallback XSUAA instance '${xsuaa}' has no credentials!`) const jwt_auth = require('../../../../lib/srv/middlewares/auth/jwt-auth.js') return jwt_auth(xsuaa_config) } class EndpointRegistry { constructor(basePath, LOG) { const deployPath = basePath + '/deploy' this.webhookCallbacks = new Map() this.deployCallbacks = new Map() const _IS_SECURED = !!( cds.env.requires.auth && (cds.env.requires.auth.impl || !(cds.env.requires.auth.kind in { mocked: 1, 'mocked-auth': 1 })) ) const _IS_PRODUCTION = process.env.NODE_ENV === 'production' cds.app.use(basePath, cds.middlewares.context()) if (_IS_SECURED) { // unofficial XSUAA fallback if (cds.env.requires.messaging?.xsuaa) { cds.app.use(basePath, _xsuaa_fallback()) } else { cds.app.use(basePath, cds.middlewares.auth()) } // ensure that anonymous users get 401 on messaging endpoints cds.app.use(basePath, (_req, _res, next) => { next(cds.context.user._is_anonymous ? 401 : undefined) }) } else if (_IS_PRODUCTION) LOG.warn('Messaging endpoints not secured') cds.app.use(basePath, express.json({ type: 'application/*+json' })) cds.app.use(basePath, express.json()) cds.app.use(basePath, express.urlencoded({ extended: true })) LOG._debug && LOG.debug('Register inbound endpoint', { basePath, method: 'OPTIONS' }) cds.app.options(basePath, (req, res) => { try { if (_IS_SECURED && !cds.context.user.is('emcallback')) return res.sendStatus(403) res.set('webhook-allowed-origin', req.headers['webhook-request-origin']) res.sendStatus(200) } catch { res.sendStatus(500) } }) LOG._debug && LOG.debug('Register inbound endpoint', { basePath, method: 'POST' }) cds.app.post(basePath, (req, res) => { try { if (_IS_SECURED && !cds.context.user.is('emcallback')) return res.sendStatus(403) const queueName = req.query.q if (!queueName) { LOG.error('Query parameter `q` not found.') return res.sendStatus(400) } const xAddress = req.headers['x-address'] const topic = xAddress && xAddress.match(/^topic:(.*)/)?.[1] if (!topic) { LOG.error('Incoming message does not contain a topic in header `x-address`: ' + xAddress) return res.sendStatus(400) } const payload = req.body const cb = this.webhookCallbacks.get(queueName) if (!cb) return res.sendStatus(200) const { tenant } = cds.context const other = tenant ? { _: { req, res }, // For `cds.context.http` tenant } : {} if (!cb) return res.sendStatus(200) cb(topic, payload, other, { done: () => { res.sendStatus(200) }, failed: () => { res.sendStatus(500) } }) } catch (error) { LOG.error(error) return res.sendStatus(500) } }) cds.app.post(deployPath, async (req, res) => { LOG.debug('Handling deploy request') try { if (_IS_SECURED && !cds.context.user.is('emmanagement')) return res.sendStatus(403) const tenants = req.body && !_isAll(req.body.tenants) && req.body.tenants const queues = req.body && !_isAll(req.body.queues) && req.body.queues const options = { wipeData: req.body && req.body.wipeData } if (tenants && !Array.isArray(tenants)) res.status(400).send('Request parameter `tenants` must be an array.') if (queues && !Array.isArray(queues)) res.status(400).send('Request parameter `queues` must be an array.') const tenantInfo = tenants ? await Promise.all(tenants.map(t => getTenantInfo(t))) : await getTenantInfo() const callbacks = queues ? queues.map(q => this.deployCallbacks.get(q)) : [...this.deployCallbacks.values()] const results = await Promise.all(callbacks.map(c => c(tenantInfo, options))) // [{ queue: '...', failed: [...], succeeded: [...] }, ...] const hasError = results.some(r => r.failed.length) if (hasError) return res.status(500).send(results) return res.status(201).send(results) } catch (e) { LOG.error('Error while handling deploy request: ', e) // REVISIT: Still needed with cds-mtxs? // If an unknown tenant id is provided, cds-mtx will crash ("Cannot read property 'hanaClient' of undefined") return res.sendStatus(500) } }) cds.app.use(basePath, cds.middlewares.errors()) } registerWebhookCallback(queueName, cb) { this.webhookCallbacks.set(queueName, cb) } registerDeployCallback(queueName, cb) { this.deployCallbacks.set(queueName, cb) } } // Singleton registries per basePath const registries = new Map() // REVISIT: Use cds mechanism instead of express? -> Need option method and handler for specifica const registerWebhookEndpoints = (basePath, queueName, LOG, cb) => { const registry = registries.get(basePath) || (registries.set(basePath, new EndpointRegistry(basePath, LOG)) && registries.get(basePath)) registry.registerWebhookCallback(queueName, cb) } const registerDeployEndpoints = (basePath, queueName, cb) => { const registry = registries.get(basePath) || (registries.set(basePath, new EndpointRegistry(basePath)) && registries.get(basePath)) registry.registerDeployCallback(queueName, cb) } // Only needed for testing, not used in productive code const __clearRegistries = () => registries.clear() module.exports = { registerWebhookEndpoints, registerDeployEndpoints, __clearRegistries }