@sap/cds 8.2.3 → 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.
Files changed (42) hide show
  1. package/CHANGELOG.md +35 -3
  2. package/bin/test.js +1 -1
  3. package/lib/compile/etc/csv.js +1 -1
  4. package/lib/dbs/cds-deploy.js +8 -5
  5. package/lib/env/cds-requires.js +0 -13
  6. package/lib/log/cds-error.js +10 -7
  7. package/lib/plugins.js +8 -3
  8. package/lib/srv/middlewares/errors.js +5 -3
  9. package/lib/srv/protocols/index.js +4 -4
  10. package/lib/srv/srv-methods.js +1 -0
  11. package/lib/utils/cds-test.js +2 -1
  12. package/lib/utils/cds-utils.js +14 -1
  13. package/lib/utils/colors.js +45 -44
  14. package/libx/_runtime/common/composition/data.js +4 -2
  15. package/libx/_runtime/common/composition/index.js +1 -2
  16. package/libx/_runtime/common/composition/tree.js +1 -24
  17. package/libx/_runtime/common/generic/auth/restrict.js +29 -4
  18. package/libx/_runtime/common/generic/auth/restrictions.js +29 -36
  19. package/libx/_runtime/common/i18n/messages.properties +1 -1
  20. package/libx/_runtime/common/utils/cqn.js +0 -26
  21. package/libx/_runtime/common/utils/csn.js +0 -14
  22. package/libx/_runtime/common/utils/differ.js +1 -0
  23. package/libx/_runtime/common/utils/resolveView.js +28 -9
  24. package/libx/_runtime/common/utils/templateProcessor.js +3 -0
  25. package/libx/_runtime/fiori/lean-draft.js +30 -12
  26. package/libx/_runtime/types/api.js +1 -1
  27. package/libx/_runtime/ucl/Service.js +2 -2
  28. package/libx/common/utils/path.js +1 -4
  29. package/libx/odata/ODataAdapter.js +6 -0
  30. package/libx/odata/middleware/batch.js +7 -9
  31. package/libx/odata/middleware/create.js +4 -2
  32. package/libx/odata/middleware/delete.js +3 -1
  33. package/libx/odata/middleware/operation.js +7 -5
  34. package/libx/odata/middleware/read.js +14 -10
  35. package/libx/odata/middleware/service-document.js +1 -1
  36. package/libx/odata/middleware/stream.js +1 -0
  37. package/libx/odata/middleware/update.js +5 -3
  38. package/libx/odata/parse/afterburner.js +37 -49
  39. package/libx/odata/utils/postProcess.js +3 -8
  40. package/package.json +1 -1
  41. package/libx/_runtime/cds-services/services/utils/compareJson.js +0 -2
  42. package/libx/_runtime/messaging/event-broker.js +0 -317
@@ -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, service)
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, service, result, isMinimal)
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, service.model)) return next(Object.assign(new Error('422'), { statusCode: 422 }))
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, ignoredColumns, target, isOne, model, aliases = []) {
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, ignoredColumns, target, isOne, model)
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, ignoredColumns, element._target ?? element.items, isOne, model)
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
- const _target = element._target
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) }], ignoredColumns, element, isOne, model)
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) }], ignoredColumns, element, isOne, model)
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, _ignoredColumns, element._target, false, model)
665
+ _validateXpr(x.where, element._target, false, model)
670
666
  }
671
-
672
667
  if (x.orderBy) {
673
- _validateXpr(x.orderBy, _ignoredColumns, element._target, false, model)
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, ignoredColumns, target, isOne, model)
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
- const _ignoredColumns = Object.values(target.elements ?? {})
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, ignoredColumns, target, isOne, model) {
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 _ignoredColumns = Object.values(target.elements ?? {})
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, ignoredColumns, target, isOne, model)
696
+ const columnAliases = _validateXpr(SELECT.columns, target, isOne, model)
707
697
  aliases.push(...columnAliases)
708
- _validateXpr(SELECT.orderBy, ignoredColumns, target, isOne, model, aliases)
709
- _validateXpr(SELECT.where, ignoredColumns, target, isOne, model, aliases)
710
- _validateXpr(SELECT.groupBy, ignoredColumns, target, isOne, model, aliases)
711
- _validateXpr(SELECT.having, ignoredColumns, target, isOne, model, aliases)
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, ignoredColumns, target, one, model)
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, service, result, isMinimal) {
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, service, target, options)
55
+ const template = getTemplate(cacheKey, { model }, target, options)
61
56
 
62
57
  if (template.elements.size === 0) return
63
58
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sap/cds",
3
- "version": "8.2.3",
3
+ "version": "8.3.0",
4
4
  "description": "SAP Cloud Application Programming Model - CDS for Node.js",
5
5
  "homepage": "https://cap.cloud.sap/",
6
6
  "keywords": [
@@ -1,2 +0,0 @@
1
- // REVISIT: remove with cds^8
2
- module.exports = require('../../../common/utils/compareJson')
@@ -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