@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.
- package/CHANGELOG.md +44 -3
- package/bin/test.js +1 -1
- package/lib/compile/etc/_localized.js +1 -0
- package/lib/compile/etc/csv.js +1 -1
- package/lib/compile/for/lean_drafts.js +5 -0
- package/lib/dbs/cds-deploy.js +8 -5
- package/lib/env/cds-requires.js +0 -13
- package/lib/linked/validate.js +11 -9
- package/lib/log/cds-error.js +10 -7
- package/lib/plugins.js +8 -3
- package/lib/srv/middlewares/cds-context.js +1 -1
- 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/error/frontend.js +18 -4
- 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/templateProcessor.js +3 -0
- package/libx/_runtime/fiori/lean-draft.js +30 -12
- 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 +14 -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/index.js +3 -2
- package/libx/odata/utils/postProcess.js +3 -8
- 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
|
@@ -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
|