@sap/cds 7.1.1 → 7.2.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 +68 -4
- package/apis/cds.d.ts +10 -6
- package/apis/connect.d.ts +0 -1
- package/apis/core.d.ts +54 -5
- package/apis/log.d.ts +19 -6
- package/apis/models.d.ts +0 -18
- package/apis/ql.d.ts +23 -23
- package/apis/serve.d.ts +17 -14
- package/apis/services.d.ts +40 -29
- package/apis/test.d.ts +1 -2
- package/bin/serve.js +4 -4
- package/lib/auth/basic-auth.js +1 -1
- package/lib/auth/dummy-auth.js +2 -1
- package/lib/auth/ias-auth.js +68 -2
- package/lib/auth/index.js +5 -5
- package/lib/auth/jwt-auth.js +40 -24
- package/lib/auth/mocked-users.js +0 -13
- package/lib/auth/passport-basic.js +2 -0
- package/lib/auth/passport-digest.js +2 -0
- package/lib/compile/etc/_localized.js +0 -1
- package/lib/compile/extend.js +16 -0
- package/lib/compile/for/lean_drafts.js +38 -6
- package/lib/compile/resolve.js +7 -5
- package/lib/compile/to/json.js +6 -2
- package/lib/dbs/cds-deploy.js +4 -4
- package/lib/env/cds-env.js +3 -3
- package/lib/env/cds-requires.js +1 -0
- package/lib/env/defaults.js +8 -1
- package/lib/env/schemas/cds-rc.json +27 -3
- package/lib/i18n/localize.js +3 -3
- package/lib/index.js +4 -0
- package/lib/log/cds-log.js +10 -1
- package/lib/ql/Whereable.js +7 -3
- package/lib/req/user.js +18 -16
- package/lib/srv/middlewares/sap-statistics.js +3 -3
- package/lib/srv/middlewares/trace.js +5 -4
- package/lib/srv/srv-dispatch.js +10 -9
- package/lib/utils/axios.js +3 -0
- package/lib/utils/cds-test.js +3 -0
- package/lib/utils/cds-utils.js +2 -0
- package/libx/_runtime/auth/index.js +8 -32
- package/libx/_runtime/auth/strategies/ias-auth.js +1 -77
- package/libx/_runtime/auth/strategies/mock.js +1 -12
- package/libx/_runtime/auth/strategies/xssecUtils.js +2 -2
- package/libx/_runtime/cds-services/adapter/odata-v4/ODataRequest.js +3 -1
- package/libx/_runtime/cds-services/adapter/odata-v4/handlers/request.js +11 -9
- package/libx/_runtime/cds-services/adapter/odata-v4/odata-to-cqn/ExpressionToCQN.js +5 -0
- package/libx/_runtime/cds-services/adapter/odata-v4/odata-to-cqn/applyToCQN.js +5 -2
- package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-commons/uri/UriParser.js +4 -0
- package/libx/_runtime/cds-services/services/utils/compareJson.js +0 -9
- package/libx/_runtime/cds-services/services/utils/differ.js +8 -10
- package/libx/_runtime/common/composition/data.js +10 -7
- package/libx/_runtime/common/composition/insert.js +9 -5
- package/libx/_runtime/common/composition/update.js +18 -12
- package/libx/_runtime/common/error/constants.js +6 -1
- package/libx/_runtime/common/generic/auth/requires.js +11 -3
- package/libx/_runtime/common/generic/auth/restrict.js +22 -16
- package/libx/_runtime/common/generic/auth/restrictions.js +5 -2
- package/libx/_runtime/common/generic/crud.js +6 -0
- package/libx/_runtime/common/generic/paging.js +2 -1
- package/libx/_runtime/common/utils/cqn2cqn4sql.js +3 -5
- package/libx/_runtime/common/utils/resolveView.js +3 -1
- package/libx/_runtime/common/utils/restrictions.js +47 -0
- package/libx/_runtime/db/data-conversion/post-processing.js +3 -3
- package/libx/_runtime/db/generic/input.js +1 -1
- package/libx/_runtime/db/sql-builder/ExpressionBuilder.js +0 -17
- package/libx/_runtime/db/utils/coloredTxCommands.js +5 -3
- package/libx/_runtime/fiori/lean-draft.js +24 -19
- package/libx/_runtime/hana/driver.js +2 -4
- package/libx/_runtime/hana/pool.js +53 -57
- package/libx/_runtime/messaging/enterprise-messaging-utils/registerEndpoints.js +2 -2
- package/libx/_runtime/messaging/outbox/utils.js +1 -2
- package/libx/_runtime/remote/utils/client.js +1 -1
- package/libx/_runtime/sqlite/Service.js +0 -4
- package/libx/_runtime/sqlite/customBuilder/CustomFunctionBuilder.js +2 -1
- package/libx/odata/afterburner.js +6 -4
- package/libx/odata/cqn2odata.js +7 -7
- package/libx/odata/utils.js +4 -1
- package/libx/rest/RestAdapter.js +15 -16
- package/package.json +1 -1
- package/lib/auth/xsuaa-auth.js +0 -2
- package/libx/_runtime/auth/utils.js +0 -32
- package/libx/audit-log/client.cds +0 -0
- package/libx/audit-log/client.js +0 -0
|
@@ -157,23 +157,6 @@ class ExpressionBuilder extends BaseBuilder {
|
|
|
157
157
|
objects[i + 1].func = `not ${objects[i + 1].func}`
|
|
158
158
|
return 1
|
|
159
159
|
}
|
|
160
|
-
if (objects[i].func || (objects[i + 2] && objects[i + 2].func)) {
|
|
161
|
-
// sqlite requires leading 0 for numbers in datetime functions
|
|
162
|
-
const f = objects[i].func ? i : OPERATORS.has(objects[i + 1]) ? i + 2 : i - 2
|
|
163
|
-
const v = objects[i].val ? i : OPERATORS.has(objects[i + 1]) ? i + 2 : i - 2
|
|
164
|
-
if (
|
|
165
|
-
objects[f] &&
|
|
166
|
-
cds.db &&
|
|
167
|
-
((SQLITE_DATETIME_FUNCTIONS.has(objects[f].func) && cds.db.kind === 'sqlite') ||
|
|
168
|
-
(HANA_DATETIME_FUNCTIONS.has(objects[f].func) && cds.db.kind === 'hana'))
|
|
169
|
-
) {
|
|
170
|
-
if (objects[v] && objects[v].val !== undefined && typeof objects[v].val === 'number') {
|
|
171
|
-
objects[v] = { val: `${objects[v].val < 10 ? 0 : ''}${objects[v].val}` }
|
|
172
|
-
if (objects[f].func === 'second') objects[v].val = _fillAfterDot(objects[v].val)
|
|
173
|
-
}
|
|
174
|
-
}
|
|
175
|
-
return 0
|
|
176
|
-
}
|
|
177
160
|
|
|
178
161
|
if ((objects[i + 1] === '=' || objects[i + 1] in NOT_EQUAL) && objects[i + 2] && objects[i + 2].val === null) {
|
|
179
162
|
this._addNullOrNotNull(objects[i], objects[i + 1])
|
|
@@ -1,5 +1,7 @@
|
|
|
1
|
+
const COLORS = !!process.stdout.isTTY && !!process.stderr.isTTY && !process.env.NO_COLOR
|
|
2
|
+
|
|
1
3
|
module.exports = {
|
|
2
|
-
BEGIN: '\x1b[1m\x1b[33mBEGIN\x1b[0m',
|
|
3
|
-
COMMIT: '\x1b[1m\x1b[32mCOMMIT\x1b[0m',
|
|
4
|
-
ROLLBACK: '\x1b[1m\x1b[91mROLLBACK\x1b[0m'
|
|
4
|
+
BEGIN: COLORS ? '\x1b[1m\x1b[33mBEGIN\x1b[0m' : 'BEGIN',
|
|
5
|
+
COMMIT: COLORS ? '\x1b[1m\x1b[32mCOMMIT\x1b[0m' : 'COMMIT',
|
|
6
|
+
ROLLBACK: COLORS ? '\x1b[1m\x1b[91mROLLBACK\x1b[0m' : 'ROLLBACK'
|
|
5
7
|
}
|
|
@@ -87,6 +87,7 @@ const _redirectRefToActives = (ref, model) => {
|
|
|
87
87
|
}
|
|
88
88
|
|
|
89
89
|
const h = cds.ApplicationService.prototype.handle
|
|
90
|
+
|
|
90
91
|
/* eslint-disable complexity */
|
|
91
92
|
cds.ApplicationService.prototype.handle = async function (req) {
|
|
92
93
|
const handle = h.bind(this)
|
|
@@ -286,9 +287,13 @@ cds.ApplicationService.prototype.handle = async function (req) {
|
|
|
286
287
|
if (req.target.actions?.[req.event] && draftParams.IsActiveEntity === false) {
|
|
287
288
|
if (query.SELECT?.from?.ref) query.SELECT.from.ref = _redirectRefToDrafts(query.SELECT.from.ref, this.model)
|
|
288
289
|
const rootQuery = query.clone()
|
|
289
|
-
|
|
290
|
+
const columns = Object_keys(query._target.keys)
|
|
291
|
+
.filter(k => k !== 'IsActiveEntity')
|
|
292
|
+
.map(k => ({ ref: [k] }))
|
|
293
|
+
columns.push({ ref: ['DraftAdministrativeData'], expand: [{ ref: ['InProcessByUser'] }] })
|
|
294
|
+
rootQuery.SELECT.columns = columns
|
|
290
295
|
rootQuery.SELECT.one = true
|
|
291
|
-
const root = await rootQuery
|
|
296
|
+
const root = await cds.run(rootQuery)
|
|
292
297
|
if (!root) req.reject(404)
|
|
293
298
|
if (root.DraftAdministrativeData?.InProcessByUser !== cds.context.user.id) req.reject(403)
|
|
294
299
|
const _req = _newReq(req, query, draftParams, { event: req.event })
|
|
@@ -334,8 +339,8 @@ cds.ApplicationService.prototype.handle = async function (req) {
|
|
|
334
339
|
}
|
|
335
340
|
|
|
336
341
|
req.query = query
|
|
337
|
-
|
|
338
|
-
return
|
|
342
|
+
|
|
343
|
+
return handle(req)
|
|
339
344
|
}
|
|
340
345
|
|
|
341
346
|
// REVISIT: It's not optimal to first calculate the whole result array and only later
|
|
@@ -410,7 +415,7 @@ const Read = {
|
|
|
410
415
|
draftsQuery.SELECT.orderBy = undefined
|
|
411
416
|
draftsQuery.SELECT.columns = keys.map(k => ({ ref: [k] }))
|
|
412
417
|
|
|
413
|
-
const drafts = await draftsQuery
|
|
418
|
+
const drafts = await draftsQuery.where({ HasActiveEntity: true })
|
|
414
419
|
const res = await Read.onlyActives(run, query.where(Read.whereNotIn(query._target, drafts)), {
|
|
415
420
|
ignoreDrafts: true
|
|
416
421
|
})
|
|
@@ -450,6 +455,7 @@ const Read = {
|
|
|
450
455
|
},
|
|
451
456
|
all: async function (run, query) {
|
|
452
457
|
LOG.debug('List Editing Status: All')
|
|
458
|
+
if (!query._drafts) return []
|
|
453
459
|
query._drafts.SELECT.count = false
|
|
454
460
|
query._drafts.SELECT.limit = undefined // We need all entries for the keys to properly select actives (count)
|
|
455
461
|
const isCount = _isCount(query._drafts)
|
|
@@ -567,21 +573,17 @@ const Read = {
|
|
|
567
573
|
if (!actives.length) return []
|
|
568
574
|
const drafts = cds.ql.clone(query._drafts)
|
|
569
575
|
drafts.SELECT.where = Read.whereIn(query._target, actives)
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
const relevantColumns = ['DraftAdministrativeData', 'DraftAdministrativeData_DraftUUID']
|
|
577
|
-
drafts.SELECT.columns = (
|
|
578
|
-
drafts.SELECT.columns?.filter(c => c.ref && relevantColumns.includes(c.ref[0])) ||
|
|
579
|
-
relevantColumns.map(k => ({ ref: [k] }))
|
|
580
|
-
).concat(
|
|
581
|
-
Object_keys(query._target.keys)
|
|
582
|
-
.filter(k => k !== 'IsActiveEntity')
|
|
583
|
-
.map(k => ({ ref: [k] }))
|
|
576
|
+
const newColumns = Object_keys(query._target.keys)
|
|
577
|
+
.filter(k => k !== 'IsActiveEntity')
|
|
578
|
+
.map(k => ({ ref: [k] }))
|
|
579
|
+
if (
|
|
580
|
+
!drafts.SELECT.columns ||
|
|
581
|
+
drafts.SELECT.columns.some(c => c === '*' || c.ref?.[0] === 'DraftAdministrativeData_DraftUUID')
|
|
584
582
|
)
|
|
583
|
+
newColumns.push({ ref: ['DraftAdministrativeData_DraftUUID'] })
|
|
584
|
+
const draftAdmin = drafts.SELECT.columns?.find(c => c.ref?.[0] === 'DraftAdministrativeData')
|
|
585
|
+
if (draftAdmin) newColumns.push(draftAdmin)
|
|
586
|
+
drafts.SELECT.columns = newColumns
|
|
585
587
|
drafts.SELECT.count = undefined
|
|
586
588
|
drafts.SELECT.search = undefined
|
|
587
589
|
drafts.SELECT.one = undefined
|
|
@@ -662,6 +664,7 @@ function _cleansed(query, model) {
|
|
|
662
664
|
draftsQuery._target = undefined
|
|
663
665
|
const [root, ...tail] = draftsQuery.SELECT.from.ref
|
|
664
666
|
const draft = model.definitions[root.id || root].drafts
|
|
667
|
+
if (!draft) return
|
|
665
668
|
draftsQuery.SELECT.from = {
|
|
666
669
|
ref: [root.id ? { ...root, id: draft.name } : draft.name, ...tail]
|
|
667
670
|
}
|
|
@@ -1090,6 +1093,7 @@ async function onPrepare(req) {
|
|
|
1090
1093
|
module.exports = {
|
|
1091
1094
|
impl() {
|
|
1092
1095
|
if (!this._datasource) this._datasource = cds.db
|
|
1096
|
+
|
|
1093
1097
|
function _wrapped(handler, isActiveEntity) {
|
|
1094
1098
|
const fn = function (req, next) {
|
|
1095
1099
|
if (!req.target?.drafts || (isActiveEntity && req.target.isDraft) || (!isActiveEntity && !req.target.isDraft))
|
|
@@ -1098,6 +1102,7 @@ module.exports = {
|
|
|
1098
1102
|
}
|
|
1099
1103
|
return fn
|
|
1100
1104
|
}
|
|
1105
|
+
|
|
1101
1106
|
// Also runs those handlers if they're annotated with @odata.draft.enabled through extensibility
|
|
1102
1107
|
this.on('NEW', '*', _wrapped(onNew, false))
|
|
1103
1108
|
this.on('EDIT', '*', _wrapped(onEdit, true))
|
|
@@ -195,11 +195,9 @@ const _getHanaDriver = name => {
|
|
|
195
195
|
LOG._debug && LOG.debug(`Failed to require "hdb" with error "${e.message}". Trying "@sap/hana-client" next.`)
|
|
196
196
|
return _getHanaDriver('@sap/hana-client')
|
|
197
197
|
} else if (isConfigured) {
|
|
198
|
-
throw new Error(`"${name}" could not be
|
|
198
|
+
throw new Error(`"${name}" could not be found. Please make sure it is installed.`)
|
|
199
199
|
} else {
|
|
200
|
-
throw new Error(
|
|
201
|
-
'Neither "hdb" nor "@sap/hana-client" could be required. Please make sure one of them is installed.'
|
|
202
|
-
)
|
|
200
|
+
throw new Error('Neither "hdb" nor "@sap/hana-client" could be found. Please make sure one of them is installed.')
|
|
203
201
|
}
|
|
204
202
|
}
|
|
205
203
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
const cds = require('../cds')
|
|
2
2
|
const LOG = cds.log('pool|db')
|
|
3
3
|
|
|
4
|
-
const { pool } =
|
|
4
|
+
const { pool } = cds.utils
|
|
5
5
|
const hana = require('./driver')
|
|
6
6
|
const getError = require('../common/error')
|
|
7
7
|
|
|
@@ -15,18 +15,20 @@ const _getMassagedCreds = function (creds) {
|
|
|
15
15
|
return creds
|
|
16
16
|
}
|
|
17
17
|
|
|
18
|
-
//
|
|
19
|
-
async function credentials4(tenant,
|
|
20
|
-
const {
|
|
18
|
+
// `disableCache: true` means to force fetch credentials from service manager
|
|
19
|
+
async function credentials4(tenant, db) {
|
|
20
|
+
const { disableCache = false, options } = db
|
|
21
|
+
const credentials = options?.credentials ?? cds.env.requires.db?.credentials
|
|
21
22
|
if (!credentials) throw new Error('No database credentials provided')
|
|
23
|
+
|
|
22
24
|
if (cds.requires.multitenancy) {
|
|
23
25
|
// eslint-disable-next-line cds/no-missing-dependencies
|
|
24
26
|
const res = await require('@sap/cds-mtxs/lib').xt.serviceManager.get(tenant, { disableCache })
|
|
25
27
|
return _getMassagedCreds(res.credentials)
|
|
26
|
-
} else {
|
|
27
|
-
if (typeof credentials !== 'object' || !credentials.host) throw new Error('Malformed database credentials provided')
|
|
28
|
-
return _getMassagedCreds(credentials)
|
|
29
28
|
}
|
|
29
|
+
|
|
30
|
+
if (typeof credentials !== 'object' || !credentials.host) throw new Error('Malformed database credentials provided')
|
|
31
|
+
return _getMassagedCreds(credentials)
|
|
30
32
|
}
|
|
31
33
|
|
|
32
34
|
function factory4(creds, tenant) {
|
|
@@ -89,55 +91,48 @@ const pools = new Map()
|
|
|
89
91
|
|
|
90
92
|
async function pool4(tenant, db) {
|
|
91
93
|
if (!pools.get(tenant)) {
|
|
92
|
-
|
|
93
|
-
tenant,
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
.
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
const INVALID_CREDENTIALS_ERROR = new Error(
|
|
103
|
-
`Create is blocked for tenant "${tenant}" due to invalid credentials.`
|
|
104
|
-
)
|
|
105
|
-
|
|
106
|
-
/*
|
|
107
|
-
* The error listener for "factoryCreateError" is registered to find out failed connection attempts.
|
|
108
|
-
* If it fails due to invalid credentials, we delete the current pool from the pools map and overwrite the
|
|
109
|
-
* pool factory create function.
|
|
110
|
-
* Background is that generic-pool will continue to try to open a connection by calling the factory create
|
|
111
|
-
* function until the "acquireTimeoutMillis" is reached.
|
|
112
|
-
* This ends up in many connection attempts for one request even though the credentials are invalid.
|
|
113
|
-
* Because of the deletion in the map, subsequent requests will fetch the credentials again.
|
|
114
|
-
*/
|
|
115
|
-
p.on('factoryCreateError', async function (err) {
|
|
116
|
-
if (err._connectError) {
|
|
117
|
-
LOG._warn && LOG.warn(INVALID_CREDENTIALS_WARNING)
|
|
118
|
-
pools.delete(tenant)
|
|
119
|
-
if (p._factory && p._factory.create) {
|
|
120
|
-
// reject after 100 ms to not block CPU completely
|
|
121
|
-
p._factory.create = () =>
|
|
122
|
-
new Promise((resolve, reject) => setTimeout(() => reject(INVALID_CREDENTIALS_ERROR), 100))
|
|
123
|
-
}
|
|
124
|
-
await p.drain()
|
|
125
|
-
await p.clear()
|
|
126
|
-
}
|
|
127
|
-
})
|
|
94
|
+
const poolPromise = new Promise((resolve, reject) => {
|
|
95
|
+
credentials4(tenant, db)
|
|
96
|
+
.then(creds => {
|
|
97
|
+
const config = _getPoolConfig()
|
|
98
|
+
LOG._info && LOG.info('effective pool configuration:', config)
|
|
99
|
+
const p = pool.createPool(factory4(creds, tenant), config)
|
|
100
|
+
const INVALID_CREDENTIALS_WARNING = `Could not establish connection for tenant "${tenant}". Existing pool will be drained.`
|
|
101
|
+
const INVALID_CREDENTIALS_ERROR = new Error(
|
|
102
|
+
`Create is blocked for tenant "${tenant}" due to invalid credentials.`
|
|
103
|
+
)
|
|
128
104
|
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
105
|
+
// The error listener for `factoryCreateError` is registered to detect failed connection attempts.
|
|
106
|
+
// If it fails due to invalid credentials, we delete the current pool from the pools map and overwrite the
|
|
107
|
+
// pool factory create function.
|
|
108
|
+
// The background is that the generic pool will keep trying to establish a connection by invoking the factory
|
|
109
|
+
// create function until the `acquireTimeoutMillis` is reached.
|
|
110
|
+
// This leads to numerous connection attempts for a single request, even when the credentials are invalid.
|
|
111
|
+
// Due to the deletion in the map, subsequent requests will retrieve the credentials again.
|
|
112
|
+
p.on('factoryCreateError', async function (err) {
|
|
113
|
+
if (err._connectError) {
|
|
114
|
+
LOG._warn && LOG.warn(INVALID_CREDENTIALS_WARNING)
|
|
115
|
+
pools.delete(tenant)
|
|
116
|
+
if (p._factory && p._factory.create) {
|
|
117
|
+
// reject after 100 ms to not block CPU completely
|
|
118
|
+
p._factory.create = () =>
|
|
119
|
+
new Promise((resolve, reject) => setTimeout(() => reject(INVALID_CREDENTIALS_ERROR), 100))
|
|
120
|
+
}
|
|
121
|
+
await p.drain()
|
|
122
|
+
await p.clear()
|
|
123
|
+
}
|
|
135
124
|
})
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
125
|
+
|
|
126
|
+
resolve(p)
|
|
127
|
+
})
|
|
128
|
+
.catch(e => {
|
|
129
|
+
// delete pools entry if fetching credentials failed
|
|
130
|
+
pools.delete(tenant)
|
|
131
|
+
reject(e)
|
|
132
|
+
})
|
|
133
|
+
})
|
|
134
|
+
|
|
135
|
+
pools.set(tenant, poolPromise)
|
|
141
136
|
}
|
|
142
137
|
|
|
143
138
|
if ('then' in pools.get(tenant)) {
|
|
@@ -150,9 +145,10 @@ async function pool4(tenant, db) {
|
|
|
150
145
|
async function resilientAcquire(pool, attempts = 1) {
|
|
151
146
|
// max 3 attempts
|
|
152
147
|
attempts = Math.min(attempts, 3)
|
|
153
|
-
let client
|
|
154
|
-
|
|
155
|
-
|
|
148
|
+
let client,
|
|
149
|
+
err,
|
|
150
|
+
attempt = 0
|
|
151
|
+
|
|
156
152
|
while (!client && attempt < attempts) {
|
|
157
153
|
try {
|
|
158
154
|
client = await pool.acquire()
|
|
@@ -4,7 +4,7 @@ const express = require('express')
|
|
|
4
4
|
const getTenantInfo = require('./getTenantInfo.js')
|
|
5
5
|
const isSecured = () => cds.requires.auth && (cds.requires.auth.impl || cds.requires.auth.credentials)
|
|
6
6
|
const _require = require('../../common/utils/require')
|
|
7
|
-
const {
|
|
7
|
+
const { ODATA_UNAUTHORIZED } = require('../../common/error/constants')
|
|
8
8
|
|
|
9
9
|
const _isAll = a => a && a.includes('all')
|
|
10
10
|
|
|
@@ -34,7 +34,7 @@ class EndpointRegistry {
|
|
|
34
34
|
paths.forEach(path => {
|
|
35
35
|
cds.app.use(path, (req, res, next) => {
|
|
36
36
|
// REVISIT: we should probably pass an error into next so that a (custom) error middleware can handle it
|
|
37
|
-
if (!req.user) res.status(401).json({ error:
|
|
37
|
+
if (!req.user) res.status(401).json({ error: ODATA_UNAUTHORIZED })
|
|
38
38
|
next()
|
|
39
39
|
})
|
|
40
40
|
})
|
|
@@ -135,8 +135,7 @@ const processMessages = async (service, tenant, _opts = {}) => {
|
|
|
135
135
|
if (!msg) continue
|
|
136
136
|
const res = {
|
|
137
137
|
process: () =>
|
|
138
|
-
|
|
139
|
-
if (userId) cds.context = { user }
|
|
138
|
+
cds._context.run({ user, tenant }, async () => {
|
|
140
139
|
try {
|
|
141
140
|
return service._emitImmediate && (await service._emitImmediate(msg))
|
|
142
141
|
} catch (e) {
|
|
@@ -416,7 +416,7 @@ const _stringToReqOptions = (query, data, target) => {
|
|
|
416
416
|
const cleanQuery = query.trim()
|
|
417
417
|
const blankIndex = cleanQuery.substring(0, 8).indexOf(' ')
|
|
418
418
|
const reqOptions = {
|
|
419
|
-
method: cleanQuery.substring(0, blankIndex).toUpperCase(),
|
|
419
|
+
method: cleanQuery.substring(0, blankIndex).toUpperCase() || 'GET',
|
|
420
420
|
url: encodeURI(formatPath(cleanQuery.substring(blankIndex, cleanQuery.length).trim()))
|
|
421
421
|
}
|
|
422
422
|
|
|
@@ -98,10 +98,6 @@ module.exports = class SQLiteDatabase extends DatabaseService {
|
|
|
98
98
|
}
|
|
99
99
|
}
|
|
100
100
|
|
|
101
|
-
getDbUrl(tenant) {
|
|
102
|
-
return this.url4(tenant)
|
|
103
|
-
}
|
|
104
|
-
|
|
105
101
|
url4(tenant) {
|
|
106
102
|
const credentials = this.options.credentials || this.options || {}
|
|
107
103
|
let dbUrl = credentials.database || credentials.url || credentials.host || ':memory:'
|
|
@@ -87,7 +87,7 @@ class CustomFunctionBuilder extends FunctionBuilder {
|
|
|
87
87
|
}
|
|
88
88
|
|
|
89
89
|
_timeFunction(functionName, args) {
|
|
90
|
-
this._outputObj.sql.push('strftime(')
|
|
90
|
+
this._outputObj.sql.push('CAST(strftime(')
|
|
91
91
|
this._outputObj.sql.push(dateTimeFunctions.get(functionName), ',')
|
|
92
92
|
if (typeof args === 'string') {
|
|
93
93
|
this._outputObj.sql.push(args, ')')
|
|
@@ -95,6 +95,7 @@ class CustomFunctionBuilder extends FunctionBuilder {
|
|
|
95
95
|
this._addFunctionArgs(args)
|
|
96
96
|
this._outputObj.sql.push(')')
|
|
97
97
|
}
|
|
98
|
+
this._outputObj.sql.push(' as decimal)')
|
|
98
99
|
}
|
|
99
100
|
}
|
|
100
101
|
|
|
@@ -28,8 +28,9 @@ const _addKeysDeep = (keys, keysCollector, ignoreManagedBacklinks) => {
|
|
|
28
28
|
}
|
|
29
29
|
|
|
30
30
|
function _keysOf(entity, ignoreManagedBacklinks) {
|
|
31
|
-
if (!entity || !entity.keys) return
|
|
32
31
|
const keysCollector = []
|
|
32
|
+
if (!entity || !entity.keys) return keysCollector
|
|
33
|
+
|
|
33
34
|
_addKeysDeep(entity.keys, keysCollector, ignoreManagedBacklinks)
|
|
34
35
|
return keysCollector
|
|
35
36
|
}
|
|
@@ -174,15 +175,16 @@ function _convertVal(element, value) {
|
|
|
174
175
|
throw new Error('Not a valid integer') // TODO
|
|
175
176
|
|
|
176
177
|
case 'cds.String':
|
|
177
|
-
case 'cds.LargeString':
|
|
178
|
+
case 'cds.LargeString':
|
|
179
|
+
return String(value)
|
|
180
|
+
case 'cds.Double':
|
|
181
|
+
return parseFloat(value)
|
|
178
182
|
case 'cds.Decimal':
|
|
179
183
|
case 'cds.DecimalFloat':
|
|
180
|
-
case 'cds.Double':
|
|
181
184
|
case 'cds.Int64':
|
|
182
185
|
case 'cds.Integer64':
|
|
183
186
|
if (typeof value === 'string') return value
|
|
184
187
|
return String(value)
|
|
185
|
-
|
|
186
188
|
case 'cds.Boolean':
|
|
187
189
|
return typeof value === 'string' ? value === 'true' : value
|
|
188
190
|
|
package/libx/odata/cqn2odata.js
CHANGED
|
@@ -73,7 +73,7 @@ function hasValidProps(obj, ...names) {
|
|
|
73
73
|
return true
|
|
74
74
|
}
|
|
75
75
|
|
|
76
|
-
function _args(args) {
|
|
76
|
+
function _args(args, func) {
|
|
77
77
|
const res = []
|
|
78
78
|
|
|
79
79
|
for (const cur of args) {
|
|
@@ -83,11 +83,11 @@ function _args(args) {
|
|
|
83
83
|
}
|
|
84
84
|
|
|
85
85
|
if (hasValidProps(cur, 'func', 'args')) {
|
|
86
|
-
res.push(`${cur.func}(${_args(cur.args)})`)
|
|
86
|
+
res.push(`${cur.func}(${_args(cur.args, cur.func)})`)
|
|
87
87
|
} else if (hasValidProps(cur, 'ref')) {
|
|
88
88
|
res.push(_format(cur))
|
|
89
89
|
} else if (hasValidProps(cur, 'val')) {
|
|
90
|
-
res.push(_format(cur))
|
|
90
|
+
res.push(_format(cur, null, null, null, null, func))
|
|
91
91
|
}
|
|
92
92
|
}
|
|
93
93
|
|
|
@@ -111,20 +111,20 @@ const _odataV2Func = (func, args) => {
|
|
|
111
111
|
// this doesn't support the contains signature with two collections as args, introduced in odata v4.01
|
|
112
112
|
return `substringof(${_args([args[1], args[0]])})`
|
|
113
113
|
default:
|
|
114
|
-
return `${func}(${_args(args)})`
|
|
114
|
+
return `${func}(${_args(args, func)})`
|
|
115
115
|
}
|
|
116
116
|
}
|
|
117
117
|
|
|
118
|
-
const _format = (cur, elementName, target, kind, isLambda) => {
|
|
118
|
+
const _format = (cur, elementName, target, kind, isLambda, func) => {
|
|
119
119
|
if (typeof cur !== 'object') return encodeURIComponent(formatVal(cur, elementName, target, kind))
|
|
120
120
|
if (hasValidProps(cur, 'ref'))
|
|
121
121
|
return encodeURIComponent(isLambda ? [LAMBDA_VARIABLE, ...cur.ref].join('/') : cur.ref[0].id || cur.ref.join('/'))
|
|
122
|
-
if (hasValidProps(cur, 'val')) return encodeURIComponent(formatVal(cur.val, elementName, target, kind))
|
|
122
|
+
if (hasValidProps(cur, 'val')) return encodeURIComponent(formatVal(cur.val, elementName, target, kind, func))
|
|
123
123
|
if (hasValidProps(cur, 'xpr')) return `(${_xpr(cur.xpr, target, kind, isLambda)})`
|
|
124
124
|
// REVISIT: How to detect the types for all functions?
|
|
125
125
|
if (hasValidProps(cur, 'func')) {
|
|
126
126
|
if (cur.args?.length) {
|
|
127
|
-
return kind === 'odata-v2' ? _odataV2Func(cur.func, cur.args) : `${cur.func}(${_args(cur.args)})`
|
|
127
|
+
return kind === 'odata-v2' ? _odataV2Func(cur.func, cur.args) : `${cur.func}(${_args(cur.args, cur.func)})`
|
|
128
128
|
}
|
|
129
129
|
return `${cur.func}()`
|
|
130
130
|
}
|
package/libx/odata/utils.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
const { toBase64url } = require('../_runtime/common/utils/binary')
|
|
2
2
|
const cds = require('../_runtime/cds')
|
|
3
3
|
|
|
4
|
+
const MATH_FUNC = {'round': 1, 'floor': 1, 'ceiling': 1}
|
|
5
|
+
|
|
4
6
|
const getSafeNumber = str => {
|
|
5
7
|
const n = Number(str)
|
|
6
8
|
return Number.isSafeInteger(n) || String(n) === str ? n : str
|
|
@@ -48,11 +50,12 @@ const _getElement = (csnTarget, key) => {
|
|
|
48
50
|
}
|
|
49
51
|
}
|
|
50
52
|
|
|
51
|
-
const formatVal = (val, elementName, csnTarget, kind) => {
|
|
53
|
+
const formatVal = (val, elementName, csnTarget, kind, func) => {
|
|
52
54
|
if (val === null || val === 'null') return 'null'
|
|
53
55
|
if (typeof val === 'boolean') return val
|
|
54
56
|
if (typeof val === 'number') return getSafeNumber(val)
|
|
55
57
|
if (!csnTarget && typeof val === 'string' && UUID.test(val)) return kind === 'odata-v2' ? `guid'${val}'` : val
|
|
58
|
+
if (typeof val === 'string' && func in MATH_FUNC) return val
|
|
56
59
|
const element = _getElement(csnTarget, elementName)
|
|
57
60
|
if (kind === 'odata-v2') {
|
|
58
61
|
switch (element.type) {
|
package/libx/rest/RestAdapter.js
CHANGED
|
@@ -17,9 +17,13 @@ const error = require('./middleware/error')
|
|
|
17
17
|
const { alias2ref } = require('../_runtime/common/utils/csn')
|
|
18
18
|
const { bufferToBase64 } = require('../_runtime/common/utils/binary')
|
|
19
19
|
|
|
20
|
+
const { getAccessRestrictions } = require('../_runtime/common/utils/restrictions')
|
|
21
|
+
|
|
20
22
|
const RestAdapter = function (srv) {
|
|
21
23
|
alias2ref(srv) // REVISIT: that's an anti pattern in new prototocol adapter setups
|
|
22
24
|
|
|
25
|
+
const accessRestrictions = getAccessRestrictions(srv)
|
|
26
|
+
|
|
23
27
|
const router = express.Router()
|
|
24
28
|
|
|
25
29
|
// pass srv-related stuff to middlewares via req
|
|
@@ -64,24 +68,19 @@ const RestAdapter = function (srv) {
|
|
|
64
68
|
// REVISIT: ensure there always is a user (should be the case with new middlewares -> remove with old middlewares)
|
|
65
69
|
if (!req.user) req.user = new cds.User.default
|
|
66
70
|
|
|
67
|
-
//
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
// > unauthorized or forbidden?
|
|
77
|
-
if (req.user._is_anonymous) {
|
|
78
|
-
// NOTE: "return req._login()" would not invoke custom error handlers
|
|
79
|
-
if (req._login) res.set('WWW-Authenticate', `Basic realm="Users"`)
|
|
80
|
-
else if (req.user._challenges) res.set('WWW-Authenticate', req.user._challenges.join(';'))
|
|
81
|
-
throw cds.error('Unauthorized', { statusCode: 401, code: '401' })
|
|
82
|
-
} else {
|
|
71
|
+
// check @restrict and @requires as soon as possible (DoS)
|
|
72
|
+
if (!accessRestrictions.some(r => req.user.is(r))) {
|
|
73
|
+
// > unauthorized or forbidden?
|
|
74
|
+
if (req.user._is_anonymous) {
|
|
75
|
+
// NOTE: "return req._login()" would not invoke custom error handlers
|
|
76
|
+
if (req._login) res.set('WWW-Authenticate', `Basic realm="Users"`)
|
|
77
|
+
else if (req.user._challenges) res.set('WWW-Authenticate', req.user._challenges.join(';'))
|
|
78
|
+
throw cds.error('Unauthorized', { statusCode: 401, code: '401' })
|
|
79
|
+
}
|
|
83
80
|
throw cds.error('Forbidden', { statusCode: 403, code: '403' })
|
|
84
81
|
}
|
|
82
|
+
|
|
83
|
+
next()
|
|
85
84
|
})
|
|
86
85
|
|
|
87
86
|
// -----------------------------------------------------------------------------------------
|
package/package.json
CHANGED
package/lib/auth/xsuaa-auth.js
DELETED
|
@@ -1,32 +0,0 @@
|
|
|
1
|
-
const UNAUTHORIZED = { statusCode: 401, code: '401', message: 'Unauthorized' }
|
|
2
|
-
const FORBIDDEN = { statusCode: 403, code: '403', message: 'Forbidden' }
|
|
3
|
-
|
|
4
|
-
const getRequiresAsArray = definition => {
|
|
5
|
-
const requires = definition['@requires']
|
|
6
|
-
if (requires) return Array.isArray(requires) ? requires : [requires]
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
const isRestricted = srv => {
|
|
10
|
-
if (srv.definition['@requires']) return true
|
|
11
|
-
|
|
12
|
-
const entities = srv.entities
|
|
13
|
-
const entitiesKeys = Object.keys(entities)
|
|
14
|
-
|
|
15
|
-
return !!(
|
|
16
|
-
entitiesKeys.some(entity => entities[entity]['@requires'] || entities[entity]['@restrict']) ||
|
|
17
|
-
entitiesKeys.some(entity => {
|
|
18
|
-
const actions = entities[entity].actions
|
|
19
|
-
actions && Object.keys(actions).some(action => actions[action]['@requires'] || actions[action]['@restrict'])
|
|
20
|
-
}) ||
|
|
21
|
-
Object.keys(srv.operations).some(
|
|
22
|
-
operation => srv.operations[operation]['@requires'] || srv.operations[operation]['@restrict']
|
|
23
|
-
)
|
|
24
|
-
)
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
module.exports = {
|
|
28
|
-
UNAUTHORIZED,
|
|
29
|
-
FORBIDDEN,
|
|
30
|
-
getRequiresAsArray,
|
|
31
|
-
isRestricted
|
|
32
|
-
}
|
|
File without changes
|
package/libx/audit-log/client.js
DELETED
|
File without changes
|