@sap/cds 8.2.3 → 8.3.1

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 (48) hide show
  1. package/CHANGELOG.md +44 -3
  2. package/bin/test.js +1 -1
  3. package/lib/compile/etc/_localized.js +1 -0
  4. package/lib/compile/etc/csv.js +1 -1
  5. package/lib/compile/for/lean_drafts.js +5 -0
  6. package/lib/dbs/cds-deploy.js +8 -5
  7. package/lib/env/cds-requires.js +0 -13
  8. package/lib/linked/validate.js +11 -9
  9. package/lib/log/cds-error.js +10 -7
  10. package/lib/plugins.js +8 -3
  11. package/lib/srv/middlewares/cds-context.js +1 -1
  12. package/lib/srv/middlewares/errors.js +5 -3
  13. package/lib/srv/protocols/index.js +4 -4
  14. package/lib/srv/srv-methods.js +1 -0
  15. package/lib/utils/cds-test.js +2 -1
  16. package/lib/utils/cds-utils.js +14 -1
  17. package/lib/utils/colors.js +45 -44
  18. package/libx/_runtime/common/composition/data.js +4 -2
  19. package/libx/_runtime/common/composition/index.js +1 -2
  20. package/libx/_runtime/common/composition/tree.js +1 -24
  21. package/libx/_runtime/common/error/frontend.js +18 -4
  22. package/libx/_runtime/common/generic/auth/restrict.js +29 -4
  23. package/libx/_runtime/common/generic/auth/restrictions.js +29 -36
  24. package/libx/_runtime/common/i18n/messages.properties +1 -1
  25. package/libx/_runtime/common/utils/cqn.js +0 -26
  26. package/libx/_runtime/common/utils/csn.js +0 -14
  27. package/libx/_runtime/common/utils/differ.js +1 -0
  28. package/libx/_runtime/common/utils/resolveView.js +28 -9
  29. package/libx/_runtime/common/utils/templateProcessor.js +3 -0
  30. package/libx/_runtime/fiori/lean-draft.js +30 -12
  31. package/libx/_runtime/types/api.js +1 -1
  32. package/libx/_runtime/ucl/Service.js +2 -2
  33. package/libx/common/utils/path.js +1 -4
  34. package/libx/odata/ODataAdapter.js +6 -0
  35. package/libx/odata/middleware/batch.js +7 -9
  36. package/libx/odata/middleware/create.js +4 -2
  37. package/libx/odata/middleware/delete.js +3 -1
  38. package/libx/odata/middleware/operation.js +7 -5
  39. package/libx/odata/middleware/read.js +14 -10
  40. package/libx/odata/middleware/service-document.js +1 -1
  41. package/libx/odata/middleware/stream.js +1 -0
  42. package/libx/odata/middleware/update.js +5 -3
  43. package/libx/odata/parse/afterburner.js +37 -49
  44. package/libx/odata/utils/index.js +3 -2
  45. package/libx/odata/utils/postProcess.js +3 -8
  46. package/package.json +1 -1
  47. package/libx/_runtime/cds-services/services/utils/compareJson.js +0 -2
  48. package/libx/_runtime/messaging/event-broker.js +0 -317
@@ -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