@sap/cds 8.2.2 → 8.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +49 -3
- package/bin/serve.js +3 -0
- package/bin/test.js +1 -1
- package/lib/compile/etc/csv.js +1 -1
- package/lib/dbs/cds-deploy.js +10 -5
- package/lib/env/cds-requires.js +0 -13
- package/lib/linked/validate.js +5 -3
- package/lib/log/cds-error.js +10 -7
- package/lib/plugins.js +8 -3
- package/lib/srv/middlewares/errors.js +5 -3
- package/lib/srv/protocols/index.js +4 -4
- package/lib/srv/srv-methods.js +1 -0
- package/lib/utils/cds-test.js +2 -1
- package/lib/utils/cds-utils.js +14 -1
- package/lib/utils/colors.js +45 -44
- package/libx/_runtime/common/composition/data.js +4 -2
- package/libx/_runtime/common/composition/index.js +1 -2
- package/libx/_runtime/common/composition/tree.js +1 -24
- package/libx/_runtime/common/generic/auth/restrict.js +29 -4
- package/libx/_runtime/common/generic/auth/restrictions.js +29 -36
- package/libx/_runtime/common/i18n/messages.properties +1 -1
- package/libx/_runtime/common/utils/cqn.js +0 -26
- package/libx/_runtime/common/utils/csn.js +0 -14
- package/libx/_runtime/common/utils/differ.js +1 -0
- package/libx/_runtime/common/utils/resolveView.js +28 -9
- package/libx/_runtime/common/utils/streamProp.js +1 -1
- package/libx/_runtime/common/utils/templateProcessor.js +3 -0
- package/libx/_runtime/fiori/lean-draft.js +34 -13
- package/libx/_runtime/types/api.js +1 -1
- package/libx/_runtime/ucl/Service.js +2 -2
- package/libx/common/utils/path.js +1 -4
- package/libx/odata/ODataAdapter.js +6 -0
- package/libx/odata/middleware/batch.js +7 -9
- package/libx/odata/middleware/create.js +4 -2
- package/libx/odata/middleware/delete.js +3 -1
- package/libx/odata/middleware/operation.js +7 -5
- package/libx/odata/middleware/read.js +19 -10
- package/libx/odata/middleware/service-document.js +1 -1
- package/libx/odata/middleware/stream.js +1 -0
- package/libx/odata/middleware/update.js +5 -3
- package/libx/odata/parse/afterburner.js +37 -49
- package/libx/odata/utils/postProcess.js +3 -8
- package/libx/odata/utils/result.js +3 -1
- package/package.json +1 -1
- package/libx/_runtime/cds-services/services/utils/compareJson.js +0 -2
- package/libx/_runtime/messaging/event-broker.js +0 -317
|
@@ -170,6 +170,7 @@ module.exports = adapter => {
|
|
|
170
170
|
|
|
171
171
|
if (isRedirect(query)) {
|
|
172
172
|
const cdsReq = adapter.request4({ query, req, res })
|
|
173
|
+
|
|
173
174
|
service.dispatch(cdsReq).then(result => {
|
|
174
175
|
if (result[query._propertyAccess]) res.set('Location', result[query._propertyAccess])
|
|
175
176
|
return res.sendStatus(307)
|
|
@@ -69,9 +69,11 @@ module.exports = adapter => {
|
|
|
69
69
|
throw Object.assign(new Error(`Method ${req.method} is not allowed for properties`), { statusCode: 405 })
|
|
70
70
|
}
|
|
71
71
|
|
|
72
|
+
const model = cds.context.model ?? service.model
|
|
73
|
+
|
|
72
74
|
// payload & params
|
|
73
75
|
const data = _propertyAccess ? { [_propertyAccess]: req.body.value } : req.body
|
|
74
|
-
const { keys, params } = getKeysAndParamsFromPath(from,
|
|
76
|
+
const { keys, params } = getKeysAndParamsFromPath(from, { model })
|
|
75
77
|
// add keys from url into payload (overwriting if already present)
|
|
76
78
|
if (!_propertyAccess) Object.assign(data, keys)
|
|
77
79
|
|
|
@@ -111,7 +113,7 @@ module.exports = adapter => {
|
|
|
111
113
|
if (result == null) return res.sendStatus(204)
|
|
112
114
|
|
|
113
115
|
const isMinimal = getPreferReturnHeader(req) === 'minimal'
|
|
114
|
-
postProcess(cdsReq.target,
|
|
116
|
+
postProcess(cdsReq.target, model, result, isMinimal)
|
|
115
117
|
|
|
116
118
|
if (isMinimal && !target._isSingleton) {
|
|
117
119
|
// determine calculation based on result with req.data as fallback
|
|
@@ -139,7 +141,7 @@ module.exports = adapter => {
|
|
|
139
141
|
// PUT / PATCH with if-match header means "only if already exists" -> no insert if it does not
|
|
140
142
|
if (req.headers['if-match']) return next(Object.assign(new Error('412'), { statusCode: 412 }))
|
|
141
143
|
|
|
142
|
-
if (!upsertSupported(from,
|
|
144
|
+
if (!upsertSupported(from, model)) return next(Object.assign(new Error('422'), { statusCode: 422 }))
|
|
143
145
|
|
|
144
146
|
// -> forward to POST
|
|
145
147
|
req.method = 'POST'
|
|
@@ -383,6 +383,14 @@ function _processSegments(from, model, namespace, cqn, protocol) {
|
|
|
383
383
|
ref[i] = { operation: current.name }
|
|
384
384
|
if (params) ref[i].args = _getDataFromParams(params, current)
|
|
385
385
|
if (current.returns && current.returns._type) one = true
|
|
386
|
+
|
|
387
|
+
if (current.returns) {
|
|
388
|
+
if (current.returns._type) {
|
|
389
|
+
one = true
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
target = current.returns.items ?? current.returns
|
|
393
|
+
}
|
|
386
394
|
} else if (current.isAssociation) {
|
|
387
395
|
if (!current._target._service) {
|
|
388
396
|
// not exposed target
|
|
@@ -589,23 +597,26 @@ const _checkAllKeysProvided = (params, entity) => {
|
|
|
589
597
|
}
|
|
590
598
|
}
|
|
591
599
|
|
|
592
|
-
const _doesNotExistError = (isExpand, refName, targetName) => {
|
|
600
|
+
const _doesNotExistError = (isExpand, refName, targetName, targetKind) => {
|
|
593
601
|
const msg = isExpand
|
|
594
602
|
? `Navigation property "${refName}" is not defined in "${targetName}"`
|
|
595
|
-
: `Property "${refName}" does not exist in "${targetName}"`
|
|
603
|
+
: `Property "${refName}" does not exist in ${targetKind === 'type' ? 'type ' : ''}"${targetName}"`
|
|
596
604
|
throw Object.assign(new Error(msg), { statusCode: 400 })
|
|
597
605
|
}
|
|
598
606
|
|
|
599
|
-
function _validateXpr(xpr,
|
|
607
|
+
function _validateXpr(xpr, target, isOne, model, aliases = []) {
|
|
600
608
|
if (!xpr) return []
|
|
601
609
|
|
|
610
|
+
const ignoredColumns = Object.values(target.elements ?? {})
|
|
611
|
+
.filter(element => element['@cds.api.ignore'] && !element.isAssociation)
|
|
612
|
+
.map(element => element.name)
|
|
602
613
|
const _aliases = []
|
|
603
614
|
|
|
604
615
|
for (const x of xpr) {
|
|
605
616
|
if (x.as) _aliases.push(x.as)
|
|
606
617
|
|
|
607
618
|
if (x.xpr) {
|
|
608
|
-
_validateXpr(x.xpr,
|
|
619
|
+
_validateXpr(x.xpr, target, isOne, model)
|
|
609
620
|
continue
|
|
610
621
|
}
|
|
611
622
|
|
|
@@ -614,101 +625,82 @@ function _validateXpr(xpr, ignoredColumns, target, isOne, model, aliases = []) {
|
|
|
614
625
|
|
|
615
626
|
if (x.ref[0].where) {
|
|
616
627
|
const element = target.elements[refName]
|
|
617
|
-
|
|
618
628
|
if (!element) {
|
|
619
629
|
_doesNotExistError(true, refName, target.name)
|
|
620
630
|
}
|
|
621
|
-
_validateXpr(x.ref[0].where,
|
|
631
|
+
_validateXpr(x.ref[0].where, element._target ?? element.items, isOne, model)
|
|
622
632
|
}
|
|
623
633
|
|
|
624
634
|
if (!target?.elements) {
|
|
625
|
-
_doesNotExistError(false, refName, target.name)
|
|
635
|
+
_doesNotExistError(false, refName, target.name, target.kind)
|
|
626
636
|
}
|
|
627
637
|
|
|
628
638
|
if (ignoredColumns.includes(refName) || (!target.elements[refName] && !aliases.includes(refName))) {
|
|
629
639
|
_doesNotExistError(x.expand, refName, target.name)
|
|
630
640
|
} else if (x.ref.length > 1) {
|
|
631
641
|
const element = target.elements[refName]
|
|
632
|
-
|
|
633
642
|
if (element.isAssociation) {
|
|
634
643
|
// navigation
|
|
635
|
-
|
|
636
|
-
const _ignoredColumns = Object.values(_target.elements ?? {})
|
|
637
|
-
.filter(element => element['@cds.api.ignore'])
|
|
638
|
-
.map(element => element.name)
|
|
639
|
-
if (element.is2one) {
|
|
640
|
-
_validateXpr([{ ref: x.ref.slice(1) }], _ignoredColumns, _target, false, model)
|
|
641
|
-
} else {
|
|
642
|
-
_validateXpr([{ ref: x.ref.slice(1) }], _ignoredColumns, _target, false, model)
|
|
643
|
-
}
|
|
644
|
+
_validateXpr([{ ref: x.ref.slice(1) }], element._target, false, model)
|
|
644
645
|
} else if (element.kind === 'element') {
|
|
645
646
|
// structured
|
|
646
|
-
_validateXpr([{ ref: x.ref.slice(1) }],
|
|
647
|
+
_validateXpr([{ ref: x.ref.slice(1) }], element, isOne, model)
|
|
647
648
|
} else {
|
|
648
649
|
throw new Error('not yet validated')
|
|
649
650
|
}
|
|
650
651
|
}
|
|
652
|
+
|
|
651
653
|
if (x.expand) {
|
|
652
654
|
let element = target.elements[refName]
|
|
653
655
|
if (element.kind === 'element' && element.elements) {
|
|
654
656
|
// structured
|
|
655
|
-
_validateXpr([{ ref: x.ref.slice(1) }],
|
|
657
|
+
_validateXpr([{ ref: x.ref.slice(1) }], element, isOne, model)
|
|
656
658
|
element = _structProperty(x.ref.slice(1), element)
|
|
657
659
|
}
|
|
658
|
-
|
|
659
660
|
if (!element._target) {
|
|
660
661
|
_doesNotExistError(true, refName, target.name)
|
|
661
662
|
}
|
|
662
|
-
|
|
663
|
-
const _ignoredColumns = Object.values(element._target.elements ?? {})
|
|
664
|
-
.filter(element => element['@cds.api.ignore'])
|
|
665
|
-
.map(element => element.name)
|
|
666
|
-
_validateXpr(x.expand, _ignoredColumns, element._target, false, model)
|
|
667
|
-
|
|
663
|
+
_validateXpr(x.expand, element._target, false, model)
|
|
668
664
|
if (x.where) {
|
|
669
|
-
_validateXpr(x.where,
|
|
665
|
+
_validateXpr(x.where, element._target, false, model)
|
|
670
666
|
}
|
|
671
|
-
|
|
672
667
|
if (x.orderBy) {
|
|
673
|
-
_validateXpr(x.orderBy,
|
|
668
|
+
_validateXpr(x.orderBy, element._target, false, model)
|
|
674
669
|
}
|
|
675
670
|
}
|
|
676
671
|
}
|
|
677
672
|
|
|
678
673
|
if (x.func) {
|
|
679
|
-
_validateXpr(x.args,
|
|
674
|
+
_validateXpr(x.args, target, isOne, model)
|
|
680
675
|
continue
|
|
681
676
|
}
|
|
682
677
|
|
|
683
678
|
if (x.SELECT) {
|
|
684
679
|
const { target } = targetFromPath(x.SELECT.from, model)
|
|
685
|
-
|
|
686
|
-
.filter(element => element['@cds.api.ignore'])
|
|
687
|
-
.map(element => element.name)
|
|
688
|
-
_validateQuery(x.SELECT, _ignoredColumns, target, x.SELECT.one, model)
|
|
680
|
+
_validateQuery(x.SELECT, target, x.SELECT.one, model)
|
|
689
681
|
}
|
|
690
682
|
}
|
|
691
683
|
|
|
692
684
|
return _aliases
|
|
693
685
|
}
|
|
694
686
|
|
|
695
|
-
function _validateQuery(SELECT,
|
|
687
|
+
function _validateQuery(SELECT, target, isOne, model) {
|
|
696
688
|
const aliases = []
|
|
689
|
+
|
|
697
690
|
if (SELECT.from.SELECT) {
|
|
698
691
|
const { target } = targetFromPath(SELECT.from.SELECT.from, model)
|
|
699
|
-
const
|
|
700
|
-
.filter(element => element['@cds.api.ignore'])
|
|
701
|
-
.map(element => element.name)
|
|
702
|
-
const subselectAliases = _validateQuery(SELECT.from.SELECT, _ignoredColumns, target, SELECT.from.SELECT.one, model)
|
|
692
|
+
const subselectAliases = _validateQuery(SELECT.from.SELECT, target, SELECT.from.SELECT.one, model)
|
|
703
693
|
aliases.push(...subselectAliases)
|
|
704
694
|
}
|
|
705
695
|
|
|
706
|
-
const columnAliases = _validateXpr(SELECT.columns,
|
|
696
|
+
const columnAliases = _validateXpr(SELECT.columns, target, isOne, model)
|
|
707
697
|
aliases.push(...columnAliases)
|
|
708
|
-
|
|
709
|
-
_validateXpr(SELECT.
|
|
710
|
-
_validateXpr(SELECT.
|
|
711
|
-
_validateXpr(SELECT.
|
|
698
|
+
|
|
699
|
+
_validateXpr(SELECT.orderBy, target, isOne, model, aliases)
|
|
700
|
+
_validateXpr(SELECT.where, target, isOne, model, aliases)
|
|
701
|
+
_validateXpr(SELECT.groupBy, target, isOne, model, aliases)
|
|
702
|
+
_validateXpr(SELECT.having, target, isOne, model, aliases)
|
|
703
|
+
|
|
712
704
|
return aliases
|
|
713
705
|
}
|
|
714
706
|
|
|
@@ -773,12 +765,8 @@ module.exports = (cqn, model, namespace, protocol) => {
|
|
|
773
765
|
_processColumns(cqn, current, protocol)
|
|
774
766
|
|
|
775
767
|
if (target) {
|
|
776
|
-
const ignoredColumns = Object.values(target.elements ?? {})
|
|
777
|
-
.filter(element => element['@cds.api.ignore'])
|
|
778
|
-
.map(element => element.name)
|
|
779
|
-
|
|
780
768
|
// validate whether only known properties are used in query options
|
|
781
|
-
_validateQuery(cqn.SELECT,
|
|
769
|
+
_validateQuery(cqn.SELECT, target, one, model)
|
|
782
770
|
}
|
|
783
771
|
|
|
784
772
|
return cqn
|
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
const cds = require('../../_runtime/cds')
|
|
2
|
-
|
|
3
1
|
const getTemplate = require('../../_runtime/common/utils/template')
|
|
4
2
|
|
|
5
3
|
const _addEtags = (row, key) => {
|
|
@@ -38,18 +36,15 @@ const _processorFn = elementInfo => {
|
|
|
38
36
|
const _pick = element => {
|
|
39
37
|
const categories = []
|
|
40
38
|
if (element['@odata.etag']) categories.push('@odata.etag')
|
|
41
|
-
if (element['@cds.api.ignore']) categories.push('@cds.api.ignore')
|
|
39
|
+
if (element['@cds.api.ignore'] && !element.isAssociation) categories.push('@cds.api.ignore')
|
|
42
40
|
if (element._type === 'cds.Binary') categories.push('binary')
|
|
43
41
|
if (element.items) categories.push('array')
|
|
44
42
|
if (categories.length) return { categories }
|
|
45
43
|
}
|
|
46
44
|
|
|
47
|
-
module.exports = function postProcess(target,
|
|
45
|
+
module.exports = function postProcess(target, model, result, isMinimal) {
|
|
48
46
|
if (!result) return
|
|
49
47
|
|
|
50
|
-
let { model } = service
|
|
51
|
-
if (service.isExtensible) model = cds.context?.model || model
|
|
52
|
-
|
|
53
48
|
if (!model.definitions[target.name]) {
|
|
54
49
|
if (model.definitions[target.items?.type]) target = target.items
|
|
55
50
|
else return
|
|
@@ -57,7 +52,7 @@ module.exports = function postProcess(target, service, result, isMinimal) {
|
|
|
57
52
|
|
|
58
53
|
const cacheKey = isMinimal ? 'postProcessMinimal' : 'postProcess'
|
|
59
54
|
const options = { pick: _pick, ignore: isMinimal ? el => el.isAssociation : undefined }
|
|
60
|
-
const template = getTemplate(cacheKey,
|
|
55
|
+
const template = getTemplate(cacheKey, { model }, target, options)
|
|
61
56
|
|
|
62
57
|
if (template.elements.size === 0) return
|
|
63
58
|
|
|
@@ -53,13 +53,15 @@ const _rewriteMetadataDeep = result => {
|
|
|
53
53
|
* @returns {object} - the odata result
|
|
54
54
|
*/
|
|
55
55
|
module.exports = function getODataResult(result, metadata, options = {}) {
|
|
56
|
-
if (result == null) return
|
|
56
|
+
if (result == null) return
|
|
57
57
|
|
|
58
58
|
const { isCollection, property } = options
|
|
59
59
|
|
|
60
60
|
if (isCollection && !Array.isArray(result)) result = [result]
|
|
61
61
|
else if (!isCollection && Array.isArray(result)) result = result[0]
|
|
62
62
|
|
|
63
|
+
if (result === undefined) return
|
|
64
|
+
|
|
63
65
|
// make sure @odata.context is the first element (per OData spec)
|
|
64
66
|
const odataResult = {
|
|
65
67
|
[METADATA.$context]: metadata.context
|
package/package.json
CHANGED
|
@@ -1,317 +0,0 @@
|
|
|
1
|
-
const cds = require('../cds')
|
|
2
|
-
|
|
3
|
-
const normalizeIncomingMessage = require('./common-utils/normalizeIncomingMessage')
|
|
4
|
-
const express = require('express')
|
|
5
|
-
const https = require('https')
|
|
6
|
-
const crypto = require('crypto')
|
|
7
|
-
|
|
8
|
-
const usedWebhookEndpoints = new Set()
|
|
9
|
-
|
|
10
|
-
async function request(options, data) {
|
|
11
|
-
return new Promise((resolve, reject) => {
|
|
12
|
-
const req = https.request(options, res => {
|
|
13
|
-
const chunks = []
|
|
14
|
-
res.on('data', chunk => {
|
|
15
|
-
chunks.push(chunk)
|
|
16
|
-
})
|
|
17
|
-
res.on('end', () => {
|
|
18
|
-
const response = {
|
|
19
|
-
statusCode: res.statusCode,
|
|
20
|
-
headers: res.headers,
|
|
21
|
-
body: Buffer.concat(chunks).toString()
|
|
22
|
-
}
|
|
23
|
-
if (res.statusCode > 299) {
|
|
24
|
-
reject({ message: response.body })
|
|
25
|
-
} else {
|
|
26
|
-
resolve(response)
|
|
27
|
-
}
|
|
28
|
-
})
|
|
29
|
-
})
|
|
30
|
-
req.on('error', error => {
|
|
31
|
-
reject(error)
|
|
32
|
-
})
|
|
33
|
-
if (data) {
|
|
34
|
-
req.write(JSON.stringify(data))
|
|
35
|
-
}
|
|
36
|
-
req.end()
|
|
37
|
-
})
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
function _validateCertificate(req, res, next) {
|
|
41
|
-
this.LOG.debug('event broker trying to authenticate via mTLS')
|
|
42
|
-
|
|
43
|
-
if (req.headers['x-ssl-client-verify'] !== '0') {
|
|
44
|
-
this.LOG.info('cf did not validate client certificate.')
|
|
45
|
-
return res.status(401).json({ message: 'Authentication Failed' })
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
if (!req.headers['x-forwarded-client-cert']) {
|
|
49
|
-
this.LOG.info('no certificate in xfcc header.')
|
|
50
|
-
return res.status(401).json({ message: 'Authentication Failed' })
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
const clientCertObj = new crypto.X509Certificate(
|
|
54
|
-
`-----BEGIN CERTIFICATE-----\n${req.headers['x-forwarded-client-cert']}\n-----END CERTIFICATE-----`
|
|
55
|
-
)
|
|
56
|
-
const clientCert = clientCertObj.toLegacyObject()
|
|
57
|
-
|
|
58
|
-
if (!this.isMultitenancy && !clientCertObj.checkPrivateKey(this.auth.privateKey))
|
|
59
|
-
return res.status(401).josn({ message: 'Authentication Failed' })
|
|
60
|
-
|
|
61
|
-
const cfSubject = Buffer.from(req.headers['x-ssl-client-subject-cn'], 'base64').toString()
|
|
62
|
-
if (
|
|
63
|
-
this.auth.validationCert.subject.CN !== clientCert.subject.CN ||
|
|
64
|
-
this.auth.validationCert.subject.CN !== cfSubject
|
|
65
|
-
) {
|
|
66
|
-
this.LOG.info('certificate subject does not match')
|
|
67
|
-
return res.status(401).json({ message: 'Authentication Failed' })
|
|
68
|
-
}
|
|
69
|
-
this.LOG.debug('incoming Subject CN is valid.')
|
|
70
|
-
|
|
71
|
-
if (this.auth.validationCert.issuer.CN !== clientCert.issuer.CN) {
|
|
72
|
-
this.LOG.info('Certificate issuer subject does not match')
|
|
73
|
-
return res.status(401).json({ message: 'Authentication Failed' })
|
|
74
|
-
}
|
|
75
|
-
this.LOG.debug('incoming issuer subject CN is valid.')
|
|
76
|
-
|
|
77
|
-
if (this.auth.validationCert.issuer.O !== clientCert.issuer.O) {
|
|
78
|
-
this.LOG.info('Certificate issuer org does not match')
|
|
79
|
-
return res.status(401).json({ message: 'Authentication Failed' })
|
|
80
|
-
}
|
|
81
|
-
this.LOG.debug('incoming Issuer Org is valid.')
|
|
82
|
-
|
|
83
|
-
if (this.auth.validationCert.issuer.OU !== clientCert.issuer.OU) {
|
|
84
|
-
this.LOG.info('certificate issuer OU does not match')
|
|
85
|
-
return res.status(401).json({ message: 'Authentication Failed' })
|
|
86
|
-
}
|
|
87
|
-
this.LOG.debug('certificate issuer OU is valid.')
|
|
88
|
-
|
|
89
|
-
const valid_from = new Date(clientCert.valid_from)
|
|
90
|
-
const valid_to = new Date(clientCert.valid_to)
|
|
91
|
-
const now = new Date(Date.now())
|
|
92
|
-
if (valid_from <= now && valid_to >= now) {
|
|
93
|
-
this.LOG.debug('certificate validation completed')
|
|
94
|
-
next()
|
|
95
|
-
} else {
|
|
96
|
-
this.LOG.error('Certificate expired')
|
|
97
|
-
return res.status(401).json({ message: 'Authentication Failed' })
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
class EventBroker extends cds.MessagingService {
|
|
102
|
-
async init() {
|
|
103
|
-
await super.init()
|
|
104
|
-
cds.once('listening', () => {
|
|
105
|
-
this.startListening()
|
|
106
|
-
})
|
|
107
|
-
this.isMultitenancy = cds.env.requires.multitenancy || cds.env.profiles.includes('mtx-sidecar')
|
|
108
|
-
|
|
109
|
-
this.auth = {} // { kind: 'cert', validationCert?, privateKey? } or { kind: 'ias', ias }
|
|
110
|
-
|
|
111
|
-
// determine auth.kind
|
|
112
|
-
if (this.options.x509) {
|
|
113
|
-
if (!this.options.x509.cert && !this.options.x509.certPath)
|
|
114
|
-
throw new Error(`${this.name}: Event Broker with x509 option requires \`x509.cert\` or \`x509.certPath\`.`)
|
|
115
|
-
if (!this.options.x509.pkey && !this.options.x509.pkeyPath)
|
|
116
|
-
throw new Error(`${this.name}: Event Broker with x509 option requires \`x509.pkey\` or \`x509.pkeyPath\`.`)
|
|
117
|
-
this.auth.kind = 'cert' // byo cert, unofficial
|
|
118
|
-
} else {
|
|
119
|
-
let ias
|
|
120
|
-
for (const k in cds.env.requires) {
|
|
121
|
-
const r = cds.env.requires[k]
|
|
122
|
-
if (r.vcap?.label === 'identity' || r.kind === 'ias') ias = r
|
|
123
|
-
}
|
|
124
|
-
// multitenant receiver-only services don't need x509, check for ias existence
|
|
125
|
-
if (!this.isMultitenancy || ias) {
|
|
126
|
-
this.auth.kind = 'ias'
|
|
127
|
-
this.auth.ias = ias
|
|
128
|
-
} else this.auth.kind = 'cert'
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
if (!this.auth.kind || (this.auth.kind === 'ias' && !this.auth.ias))
|
|
132
|
-
throw new Error(`${this.name}: Event Broker requires your app to be bound to an IAS instance.`) // do not mention byo cert
|
|
133
|
-
|
|
134
|
-
if (this.auth.kind === 'cert') {
|
|
135
|
-
if (this.isMultitenancy && !this.options.credentials?.certificate)
|
|
136
|
-
throw new Error(
|
|
137
|
-
`${this.name}: \`certificate\` not found in Event Broker binding information. You need to bind your app to an Event Broker instance.`
|
|
138
|
-
)
|
|
139
|
-
this.auth.validationCert = new crypto.X509Certificate(
|
|
140
|
-
this.isMultitenancy ? this.options.credentials.certificate : this.agent.options.cert
|
|
141
|
-
).toLegacyObject()
|
|
142
|
-
this.auth.privateKey = !this.isMultitenancy && crypto.createPrivateKey(this.agent.options.key)
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
this.LOG._debug && this.LOG.debug('using auth: ' + this.auth.kind)
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
get agent() {
|
|
149
|
-
return (this.__agentCache ??=
|
|
150
|
-
this.auth.kind === 'ias'
|
|
151
|
-
? new https.Agent({ cert: this.auth.ias.credentials.certificate, key: this.auth.ias.credentials.key })
|
|
152
|
-
: new https.Agent({
|
|
153
|
-
cert:
|
|
154
|
-
this.options.x509.cert ??
|
|
155
|
-
cds.utils.fs.readFileSync(cds.utils.path.resolve(cds.root, this.options.x509.certPath)),
|
|
156
|
-
key:
|
|
157
|
-
this.options.x509.pkey ??
|
|
158
|
-
cds.utils.fs.readFileSync(cds.utils.path.resolve(cds.root, this.options.x509.pkeyPath))
|
|
159
|
-
}))
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
async handle(msg) {
|
|
163
|
-
if (msg.inbound) return super.handle(msg)
|
|
164
|
-
if (!this.options.credentials) throw new Error(`${this.name}: No credentials found for Event Broker service.`)
|
|
165
|
-
if (!this.options.credentials.ceSource)
|
|
166
|
-
throw new Error(`${this.name}: Emitting events is not supported by Event Broker plan \`event-connectivity\`.`)
|
|
167
|
-
const _msg = this.message4(msg)
|
|
168
|
-
await this.emitToEventBroker(_msg)
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
startListening() {
|
|
172
|
-
if (!this._listenToAll.value && !this.subscribedTopics.size) return
|
|
173
|
-
this.registerWebhookEndpoints()
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
async emitToEventBroker(msg) {
|
|
177
|
-
// TODO: CSN definition probably not needed, just in case...
|
|
178
|
-
// See if there's a CSN entry for that event
|
|
179
|
-
// const found = cds?.model.definitions[topicOrEvent]
|
|
180
|
-
// if (found) return found // case for fully-qualified event name
|
|
181
|
-
// for (const def in cds.model?.definitions) {
|
|
182
|
-
// const definition = cds.model.definitions[def]
|
|
183
|
-
// if (definition['@topic'] === topicOrEvent) return definition
|
|
184
|
-
// }
|
|
185
|
-
|
|
186
|
-
try {
|
|
187
|
-
const hostname = this.options.credentials.eventing.http.x509.url.replace(/^https?:\/\//, '')
|
|
188
|
-
|
|
189
|
-
// take over and cleanse cloudevents headers
|
|
190
|
-
const headers = { ...(msg.headers ?? {}) }
|
|
191
|
-
|
|
192
|
-
const ceId = headers.id
|
|
193
|
-
delete headers.id
|
|
194
|
-
|
|
195
|
-
const ceSource = headers.source
|
|
196
|
-
delete headers.source
|
|
197
|
-
|
|
198
|
-
const ceType = headers.type
|
|
199
|
-
delete headers.type
|
|
200
|
-
|
|
201
|
-
const ceSpecversion = headers.specversion
|
|
202
|
-
delete headers.specversion
|
|
203
|
-
|
|
204
|
-
// const ceDatacontenttype = headers.datacontenttype // not part of the HTTP API
|
|
205
|
-
delete headers.datacontenttype
|
|
206
|
-
|
|
207
|
-
// const ceTime = headers.time // not part of the HTTP API
|
|
208
|
-
delete headers.time
|
|
209
|
-
|
|
210
|
-
const options = {
|
|
211
|
-
hostname: hostname,
|
|
212
|
-
method: 'POST',
|
|
213
|
-
headers: {
|
|
214
|
-
'ce-id': ceId,
|
|
215
|
-
'ce-source': ceSource,
|
|
216
|
-
'ce-type': ceType,
|
|
217
|
-
'ce-specversion': ceSpecversion,
|
|
218
|
-
'Content-Type': 'application/json' // because of { data, ...headers } format
|
|
219
|
-
},
|
|
220
|
-
agent: this.agent
|
|
221
|
-
}
|
|
222
|
-
if (this.LOG._debug) {
|
|
223
|
-
this.LOG.debug('HTTP headers:', JSON.stringify(options.headers))
|
|
224
|
-
this.LOG.debug('HTTP body:', JSON.stringify(msg.data))
|
|
225
|
-
}
|
|
226
|
-
// what about headers?
|
|
227
|
-
// TODO: Clarify if we should send `{ data, ...headers }` vs. `data` + HTTP headers (`ce-*`)
|
|
228
|
-
// Disadvantage with `data` + HTTP headers is that they're case insensitive -> information loss, but they're 'closer' to the cloudevents standard
|
|
229
|
-
await request(options, { data: msg.data, ...headers }) // TODO: fetch does not work with mTLS as of today, requires another module. see https://github.com/nodejs/node/issues/48977
|
|
230
|
-
if (this.LOG._info) this.LOG.info('Emit', { topic: msg.event })
|
|
231
|
-
} catch (e) {
|
|
232
|
-
this.LOG.error('Emit failed:', e.message)
|
|
233
|
-
throw e
|
|
234
|
-
}
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
prepareHeaders(headers, event) {
|
|
238
|
-
if (!('source' in headers)) {
|
|
239
|
-
if (!this.options.credentials.ceSource)
|
|
240
|
-
throw new Error(`${this.name}: Cannot emit event: Parameter \`ceSource\` not found in Event Broker binding.`)
|
|
241
|
-
headers.source = `${this.options.credentials.ceSource[0]}/${cds.context.tenant}`
|
|
242
|
-
}
|
|
243
|
-
super.prepareHeaders(headers, event)
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
registerWebhookEndpoints() {
|
|
247
|
-
const webhookBasePath = this.options.webhookPath || '/-/cds/event-broker/webhook'
|
|
248
|
-
if (usedWebhookEndpoints.has(webhookBasePath))
|
|
249
|
-
throw new Error(
|
|
250
|
-
`${this.name}: Event Broker: Webhook endpoint already registered. Use a different one with \`options.webhookPath\`.`
|
|
251
|
-
)
|
|
252
|
-
usedWebhookEndpoints.add(webhookBasePath)
|
|
253
|
-
// auth
|
|
254
|
-
if (this.auth.kind === 'ias') {
|
|
255
|
-
const ias_auth = require('../../../lib/auth/ias-auth')
|
|
256
|
-
cds.app.use(webhookBasePath, cds.middlewares.context())
|
|
257
|
-
cds.app.use(webhookBasePath, ias_auth(this.auth.ias))
|
|
258
|
-
cds.app.use(webhookBasePath, (err, _req, res, next) => {
|
|
259
|
-
if (err.code === 401) return res.status(401).json({ message: 'Unauthorized' })
|
|
260
|
-
return next(err)
|
|
261
|
-
})
|
|
262
|
-
cds.app.use(webhookBasePath, (_req, res, next) => {
|
|
263
|
-
if (
|
|
264
|
-
cds.context.user.is('system-user') &&
|
|
265
|
-
cds.context.user.tokenInfo.azp === this.options.credentials.ias.clientId
|
|
266
|
-
) {
|
|
267
|
-
// the token was fetched by event broker -> OK
|
|
268
|
-
return next()
|
|
269
|
-
}
|
|
270
|
-
if (cds.context.user.is('internal-user')) {
|
|
271
|
-
// the token was fetched by own credentials -> OK (for testing)
|
|
272
|
-
return next()
|
|
273
|
-
}
|
|
274
|
-
res.status(401).json({ message: 'Unauthorized' })
|
|
275
|
-
})
|
|
276
|
-
} else {
|
|
277
|
-
cds.app.post(webhookBasePath, _validateCertificate.bind(this))
|
|
278
|
-
}
|
|
279
|
-
cds.app.post(webhookBasePath, express.json())
|
|
280
|
-
cds.app.post(webhookBasePath, this.onEventReceived.bind(this))
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
async onEventReceived(req, res) {
|
|
284
|
-
try {
|
|
285
|
-
const event = req.headers['ce-type'] // TG27: type contains namespace, so there's no collision
|
|
286
|
-
const tenant = req.headers['ce-sapconsumertenant']
|
|
287
|
-
|
|
288
|
-
// take over cloudevents headers (`ce-*`) without the prefix
|
|
289
|
-
const headers = {}
|
|
290
|
-
for (const header in req.headers) {
|
|
291
|
-
if (header.startsWith('ce-')) headers[header.slice(3)] = req.headers[header]
|
|
292
|
-
}
|
|
293
|
-
|
|
294
|
-
const msg = normalizeIncomingMessage(req.body)
|
|
295
|
-
msg.event = event
|
|
296
|
-
Object.assign(msg.headers, headers)
|
|
297
|
-
if (this.isMultitenancy) msg.tenant = tenant
|
|
298
|
-
|
|
299
|
-
// for cds.context.http
|
|
300
|
-
msg._ = {}
|
|
301
|
-
msg._.req = req
|
|
302
|
-
msg._.res = res
|
|
303
|
-
|
|
304
|
-
const context = { user: cds.User.privileged, _: msg._ }
|
|
305
|
-
if (msg.tenant) context.tenant = msg.tenant
|
|
306
|
-
|
|
307
|
-
await this.tx(context, tx => tx.emit(msg))
|
|
308
|
-
this.LOG.debug('Event processed successfully.')
|
|
309
|
-
return res.status(200).json({ message: 'OK' })
|
|
310
|
-
} catch (e) {
|
|
311
|
-
this.LOG.error('ERROR during inbound event processing:', e) // TODO: How does Event Broker do error handling?
|
|
312
|
-
res.status(500).json({ message: 'Internal Server Error!' })
|
|
313
|
-
}
|
|
314
|
-
}
|
|
315
|
-
}
|
|
316
|
-
|
|
317
|
-
module.exports = EventBroker
|