@sap/cds 7.1.2 → 7.2.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 +68 -4
- package/apis/cds.d.ts +10 -6
- package/apis/connect.d.ts +1 -2
- 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 +18 -15
- package/apis/services.d.ts +67 -56
- package/apis/test.d.ts +1 -2
- package/bin/serve.js +4 -4
- package/common.cds +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 +3 -3
- 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/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/common/composition/data.js +5 -3
- package/libx/_runtime/common/composition/insert.js +6 -3
- package/libx/_runtime/common/composition/update.js +12 -8
- 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 +21 -15
- 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 +3 -1
- package/libx/_runtime/common/i18n/messages.properties +1 -0
- 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/fiori/lean-draft.js +27 -24
- package/libx/_runtime/hana/driver.js +2 -4
- package/libx/_runtime/hana/pool.js +1 -1
- package/libx/_runtime/messaging/enterprise-messaging-utils/registerEndpoints.js +2 -2
- package/libx/_runtime/messaging/outbox/utils.js +1 -2
- package/libx/_runtime/remote/Service.js +10 -9
- package/libx/_runtime/remote/utils/client.js +4 -3
- package/libx/_runtime/sqlite/Service.js +0 -4
- package/libx/_runtime/sqlite/customBuilder/CustomFunctionBuilder.js +2 -1
- package/libx/odata/afterburner.js +5 -3
- 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
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
const cds = require('../../cds')
|
|
2
|
+
|
|
3
|
+
const containsAnyRestrictions = srv => {
|
|
4
|
+
const accessRestrictions = getAccessRestrictions(srv)
|
|
5
|
+
if (accessRestrictions.length > 1 || accessRestrictions[0] !== 'any') return true
|
|
6
|
+
|
|
7
|
+
const entities = srv.entities
|
|
8
|
+
const entitiesKeys = Object.keys(entities)
|
|
9
|
+
|
|
10
|
+
return !!(
|
|
11
|
+
entitiesKeys.some(entity => entities[entity]['@requires'] || entities[entity]['@restrict']) ||
|
|
12
|
+
entitiesKeys.some(entity => {
|
|
13
|
+
const actions = entities[entity].actions
|
|
14
|
+
actions && Object.keys(actions).some(action => actions[action]['@requires'] || actions[action]['@restrict'])
|
|
15
|
+
}) ||
|
|
16
|
+
Object.keys(srv.operations).some(
|
|
17
|
+
operation => srv.operations[operation]['@requires'] || srv.operations[operation]['@restrict']
|
|
18
|
+
)
|
|
19
|
+
)
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const getAccessRestrictions = srv => {
|
|
23
|
+
let restrictions = srv.definition['@restrict'] || srv.definition['@requires']
|
|
24
|
+
if (restrictions) {
|
|
25
|
+
if (typeof restrictions === 'string') restrictions = [restrictions]
|
|
26
|
+
else
|
|
27
|
+
restrictions = restrictions
|
|
28
|
+
.map(r => (typeof r === 'string' ? r : r.to))
|
|
29
|
+
.reduce((acc, cur) => {
|
|
30
|
+
Array.isArray(cur) ? acc.push(...cur) : acc.push(cur)
|
|
31
|
+
return acc
|
|
32
|
+
}, [])
|
|
33
|
+
} else {
|
|
34
|
+
const { restrict_all_services } = cds.env.requires.auth
|
|
35
|
+
const in_prod = process.env.NODE_ENV === 'production'
|
|
36
|
+
// REVISIT: cleanup during streamlined auth
|
|
37
|
+
const is_mocked_auth = cds.env.requires.auth._kind === 'mocked'
|
|
38
|
+
if (restrict_all_services === false || !in_prod || is_mocked_auth) restrictions = ['any']
|
|
39
|
+
else restrictions = ['authenticated-user']
|
|
40
|
+
}
|
|
41
|
+
return restrictions
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
module.exports = {
|
|
45
|
+
containsAnyRestrictions,
|
|
46
|
+
getAccessRestrictions
|
|
47
|
+
}
|
|
@@ -132,9 +132,9 @@ const _getMapperForListedElements = (conversionMap, csn, cqn) => {
|
|
|
132
132
|
* @returns {Map<any, any>}
|
|
133
133
|
* @private
|
|
134
134
|
*/
|
|
135
|
-
const getPostProcessMapper = (conversionMap, csn
|
|
136
|
-
// No mapper defined or irrelevant as no READ request
|
|
137
|
-
if (!Object.prototype.hasOwnProperty.call(cqn, 'SELECT')) {
|
|
135
|
+
const getPostProcessMapper = (conversionMap, csn, cqn) => {
|
|
136
|
+
// No mapper defined or irrelevant as no CSN, CQN or READ request
|
|
137
|
+
if (!csn || !cqn || !Object.prototype.hasOwnProperty.call(cqn, 'SELECT')) {
|
|
138
138
|
return new Map()
|
|
139
139
|
}
|
|
140
140
|
|
|
@@ -138,7 +138,7 @@ const _pickCRUD = element => {
|
|
|
138
138
|
categories.push('!default')
|
|
139
139
|
}
|
|
140
140
|
|
|
141
|
-
if (element.default && !DRAFT_COLUMNS_MAP[element.name]) {
|
|
141
|
+
if (element.default && !DRAFT_COLUMNS_MAP[element.name] && !element.isAssociation) {
|
|
142
142
|
categories.push({ category: 'default', args: element })
|
|
143
143
|
}
|
|
144
144
|
|
|
@@ -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])
|
|
@@ -48,6 +48,10 @@ const _promiseAll = async array => {
|
|
|
48
48
|
|
|
49
49
|
const _isCount = query => query.SELECT.columns?.length === 1 && query.SELECT.columns[0].func === 'count'
|
|
50
50
|
|
|
51
|
+
const entity_keys = e => {
|
|
52
|
+
return Object_keys(e.keys).filter(k => k !== 'IsActiveEntity' && !e.keys[k].isAssociation)
|
|
53
|
+
}
|
|
54
|
+
|
|
51
55
|
const _inProcessByUserXpr = lockShiftedNow => ({
|
|
52
56
|
xpr: [
|
|
53
57
|
'case',
|
|
@@ -87,6 +91,7 @@ const _redirectRefToActives = (ref, model) => {
|
|
|
87
91
|
}
|
|
88
92
|
|
|
89
93
|
const h = cds.ApplicationService.prototype.handle
|
|
94
|
+
|
|
90
95
|
/* eslint-disable complexity */
|
|
91
96
|
cds.ApplicationService.prototype.handle = async function (req) {
|
|
92
97
|
const handle = h.bind(this)
|
|
@@ -256,7 +261,7 @@ cds.ApplicationService.prototype.handle = async function (req) {
|
|
|
256
261
|
{ ref: ['DraftAdministrativeData'], expand: [{ ref: ['InProcessByUser'] }] }
|
|
257
262
|
])
|
|
258
263
|
)
|
|
259
|
-
if (!res) req.reject(_etagValidationType ? 412 : 404)
|
|
264
|
+
if (!res) req.reject(_etagValidationType ? 412 : { code: 'DRAFT_NOT_EXISTING', status: 404 })
|
|
260
265
|
if (res.DraftAdministrativeData?.InProcessByUser !== cds.context.user.id)
|
|
261
266
|
req.reject(403, 'DRAFT_LOCKED_BY_ANOTHER_USER', [res.DraftAdministrativeData?.InProcessByUser])
|
|
262
267
|
const DraftAdministrativeData_DraftUUID = res.DraftAdministrativeData_DraftUUID
|
|
@@ -286,9 +291,7 @@ cds.ApplicationService.prototype.handle = async function (req) {
|
|
|
286
291
|
if (req.target.actions?.[req.event] && draftParams.IsActiveEntity === false) {
|
|
287
292
|
if (query.SELECT?.from?.ref) query.SELECT.from.ref = _redirectRefToDrafts(query.SELECT.from.ref, this.model)
|
|
288
293
|
const rootQuery = query.clone()
|
|
289
|
-
const columns =
|
|
290
|
-
.filter(k => k !== 'IsActiveEntity')
|
|
291
|
-
.map(k => ({ ref: [k] }))
|
|
294
|
+
const columns = entity_keys(query._target).map(k => ({ ref: [k] }))
|
|
292
295
|
columns.push({ ref: ['DraftAdministrativeData'], expand: [{ ref: ['InProcessByUser'] }] })
|
|
293
296
|
rootQuery.SELECT.columns = columns
|
|
294
297
|
rootQuery.SELECT.one = true
|
|
@@ -338,8 +341,8 @@ cds.ApplicationService.prototype.handle = async function (req) {
|
|
|
338
341
|
}
|
|
339
342
|
|
|
340
343
|
req.query = query
|
|
341
|
-
|
|
342
|
-
return
|
|
344
|
+
|
|
345
|
+
return handle(req)
|
|
343
346
|
}
|
|
344
347
|
|
|
345
348
|
// REVISIT: It's not optimal to first calculate the whole result array and only later
|
|
@@ -376,7 +379,7 @@ const Read = {
|
|
|
376
379
|
if (query._target.name.endsWith('.DraftAdministrativeData')) return run(query._drafts)
|
|
377
380
|
if (!query._target._isDraftEnabled) return run(query)
|
|
378
381
|
if (!query.SELECT.groupBy && query.SELECT.columns && !query.SELECT.columns.some(c => c === '*')) {
|
|
379
|
-
const keys =
|
|
382
|
+
const keys = entity_keys(query._target)
|
|
380
383
|
for (const key of keys) {
|
|
381
384
|
if (!query.SELECT.columns.some(c => c.ref?.[0] === key)) query.SELECT.columns.push({ ref: [key] })
|
|
382
385
|
}
|
|
@@ -408,13 +411,12 @@ const Read = {
|
|
|
408
411
|
unchanged: async function (run, query) {
|
|
409
412
|
LOG.debug('List Editing Status: Unchanged')
|
|
410
413
|
const draftsQuery = query._drafts
|
|
411
|
-
const keys = Object_keys(query._target.keys).filter(k => k !== 'IsActiveEntity')
|
|
412
414
|
draftsQuery.SELECT.count = undefined
|
|
413
|
-
draftsQuery.SELECT.limit = undefined
|
|
414
415
|
draftsQuery.SELECT.orderBy = undefined
|
|
415
|
-
draftsQuery.SELECT.
|
|
416
|
+
draftsQuery.SELECT.limit = false
|
|
417
|
+
draftsQuery.SELECT.columns = entity_keys(query._target).map(k => ({ ref: [k] }))
|
|
416
418
|
|
|
417
|
-
const drafts = await draftsQuery
|
|
419
|
+
const drafts = await draftsQuery.where({ HasActiveEntity: true })
|
|
418
420
|
const res = await Read.onlyActives(run, query.where(Read.whereNotIn(query._target, drafts)), {
|
|
419
421
|
ignoreDrafts: true
|
|
420
422
|
})
|
|
@@ -456,11 +458,10 @@ const Read = {
|
|
|
456
458
|
LOG.debug('List Editing Status: All')
|
|
457
459
|
if (!query._drafts) return []
|
|
458
460
|
query._drafts.SELECT.count = false
|
|
459
|
-
query._drafts.SELECT.limit =
|
|
461
|
+
query._drafts.SELECT.limit = false // We need all entries for the keys to properly select actives (count)
|
|
460
462
|
const isCount = _isCount(query._drafts)
|
|
461
463
|
if (isCount) {
|
|
462
|
-
|
|
463
|
-
query._drafts.SELECT.columns = keys.map(k => ({ ref: [k] }))
|
|
464
|
+
query._drafts.SELECT.columns = entity_keys(query._target).map(k => ({ ref: [k] }))
|
|
464
465
|
}
|
|
465
466
|
if (!query._drafts.SELECT.columns) query._drafts.SELECT.columns = ['*']
|
|
466
467
|
if (!query._drafts.SELECT.columns.some(c => c.ref?.[0] === 'HasActiveEntity'))
|
|
@@ -524,13 +525,14 @@ const Read = {
|
|
|
524
525
|
},
|
|
525
526
|
activesFromDrafts: async function (run, query, { isLocked = true }) {
|
|
526
527
|
const draftsQuery = query._drafts
|
|
527
|
-
const keys = Object_keys(query._target.keys).filter(k => k !== 'IsActiveEntity')
|
|
528
528
|
const additionalCols = draftsQuery.SELECT.columns
|
|
529
529
|
? draftsQuery.SELECT.columns.filter(
|
|
530
530
|
c => c.ref && ['DraftAdministrativeData', 'DraftAdministrativeData_DraftUUID'].includes(c.ref[0])
|
|
531
531
|
)
|
|
532
532
|
: [{ ref: ['DraftAdministrativeData_DraftUUID'] }]
|
|
533
|
-
draftsQuery.SELECT.columns =
|
|
533
|
+
draftsQuery.SELECT.columns = entity_keys(query._target)
|
|
534
|
+
.map(k => ({ ref: [k] }))
|
|
535
|
+
.concat(additionalCols)
|
|
534
536
|
draftsQuery.where({
|
|
535
537
|
HasActiveEntity: true,
|
|
536
538
|
'DraftAdministrativeData.InProcessByUser': { '!=': cds.context.user.id },
|
|
@@ -559,7 +561,7 @@ const Read = {
|
|
|
559
561
|
},
|
|
560
562
|
whereNotIn: (target, data) => Read.whereIn(target, data, true),
|
|
561
563
|
whereIn: (target, data, not = false) => {
|
|
562
|
-
const keys =
|
|
564
|
+
const keys = entity_keys(target)
|
|
563
565
|
const dataArray = data ? (Array.isArray(data) ? data : [data]) : []
|
|
564
566
|
if (not && !dataArray.length) return []
|
|
565
567
|
const left = { list: keys.map(k => ({ ref: [k] })) }
|
|
@@ -572,9 +574,7 @@ const Read = {
|
|
|
572
574
|
if (!actives.length) return []
|
|
573
575
|
const drafts = cds.ql.clone(query._drafts)
|
|
574
576
|
drafts.SELECT.where = Read.whereIn(query._target, actives)
|
|
575
|
-
const newColumns =
|
|
576
|
-
.filter(k => k !== 'IsActiveEntity')
|
|
577
|
-
.map(k => ({ ref: [k] }))
|
|
577
|
+
const newColumns = entity_keys(query._target).map(k => ({ ref: [k] }))
|
|
578
578
|
if (
|
|
579
579
|
!drafts.SELECT.columns ||
|
|
580
580
|
drafts.SELECT.columns.some(c => c === '*' || c.ref?.[0] === 'DraftAdministrativeData_DraftUUID')
|
|
@@ -593,8 +593,10 @@ const Read = {
|
|
|
593
593
|
// Indexes the data for fast key access
|
|
594
594
|
const dataArray = Read._makeArray(data)
|
|
595
595
|
if (!dataArray.length) return
|
|
596
|
-
const
|
|
597
|
-
|
|
596
|
+
const hash = row =>
|
|
597
|
+
entity_keys(target)
|
|
598
|
+
.map(k => row[k])
|
|
599
|
+
.reduce((res, curr) => res + '|$|' + curr, '')
|
|
598
600
|
const hashMap = new Map()
|
|
599
601
|
for (const row of dataArray) hashMap.set(hash(row), row)
|
|
600
602
|
return { hashMap, hash }
|
|
@@ -1073,12 +1075,11 @@ async function onPrepare(req) {
|
|
|
1073
1075
|
}
|
|
1074
1076
|
const where = req.query.SELECT.from.ref[0].where
|
|
1075
1077
|
|
|
1076
|
-
const keys = Object_keys(req.target.keys).filter(k => k !== 'IsActiveEntity')
|
|
1077
1078
|
const draftQuery = SELECT.one
|
|
1078
1079
|
.from(req.target, d => {
|
|
1079
1080
|
d.DraftAdministrativeData(a => a.InProcessByUser)
|
|
1080
1081
|
})
|
|
1081
|
-
.columns(
|
|
1082
|
+
.columns(entity_keys(req.target))
|
|
1082
1083
|
.where(where)
|
|
1083
1084
|
draftQuery[DRAFT_PARAMS] = draftParams
|
|
1084
1085
|
const data = await draftQuery
|
|
@@ -1092,6 +1093,7 @@ async function onPrepare(req) {
|
|
|
1092
1093
|
module.exports = {
|
|
1093
1094
|
impl() {
|
|
1094
1095
|
if (!this._datasource) this._datasource = cds.db
|
|
1096
|
+
|
|
1095
1097
|
function _wrapped(handler, isActiveEntity) {
|
|
1096
1098
|
const fn = function (req, next) {
|
|
1097
1099
|
if (!req.target?.drafts || (isActiveEntity && req.target.isDraft) || (!isActiveEntity && !req.target.isDraft))
|
|
@@ -1100,6 +1102,7 @@ module.exports = {
|
|
|
1100
1102
|
}
|
|
1101
1103
|
return fn
|
|
1102
1104
|
}
|
|
1105
|
+
|
|
1103
1106
|
// Also runs those handlers if they're annotated with @odata.draft.enabled through extensibility
|
|
1104
1107
|
this.on('NEW', '*', _wrapped(onNew, false))
|
|
1105
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
|
}
|
|
@@ -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) {
|
|
@@ -109,8 +109,9 @@ const _handleUnboundActionFunction = (srv, def, req, event) => {
|
|
|
109
109
|
def &&
|
|
110
110
|
def.returns &&
|
|
111
111
|
(def.returns.type === 'cds.LargeBinary' || def.returns.type === 'cds.Binary')
|
|
112
|
+
const { headers, data } = req
|
|
112
113
|
|
|
113
|
-
return srv.send({ method: 'POST', path: `/${event}`,
|
|
114
|
+
return srv.send({ method: 'POST', path: `/${event}`, headers, data, _binary: isBinary })
|
|
114
115
|
}
|
|
115
116
|
|
|
116
117
|
const url =
|
|
@@ -118,16 +119,17 @@ const _handleUnboundActionFunction = (srv, def, req, event) => {
|
|
|
118
119
|
return srv.get(url)
|
|
119
120
|
}
|
|
120
121
|
|
|
121
|
-
const _sendV2RequestActionFunction = (srv, def, url) => {
|
|
122
|
+
const _sendV2RequestActionFunction = (srv, def, req, url) => {
|
|
123
|
+
const { headers } = req
|
|
122
124
|
return def.kind === 'function'
|
|
123
|
-
? srv.send({ method: 'GET', path: url, _returnType: def.returns })
|
|
124
|
-
: srv.send({ method: 'POST', path: url, data: {}, _returnType: def.returns })
|
|
125
|
+
? srv.send({ method: 'GET', path: url, headers, _returnType: def.returns })
|
|
126
|
+
: srv.send({ method: 'POST', path: url, headers, data: {}, _returnType: def.returns })
|
|
125
127
|
}
|
|
126
128
|
|
|
127
129
|
const _handleV2ActionFunction = (srv, def, req, event, kind) => {
|
|
128
130
|
const url =
|
|
129
131
|
Object.keys(req.data).length > 0 ? _buildPartialUrlFunctions(`/${event}`, req.data, def.params, kind) : `/${event}`
|
|
130
|
-
return _sendV2RequestActionFunction(srv, def, url)
|
|
132
|
+
return _sendV2RequestActionFunction(srv, def, req, url)
|
|
131
133
|
}
|
|
132
134
|
|
|
133
135
|
const _handleV2BoundActionFunction = (srv, def, req, event, kind) => {
|
|
@@ -147,7 +149,7 @@ const _handleV2BoundActionFunction = (srv, def, req, event, kind) => {
|
|
|
147
149
|
}
|
|
148
150
|
|
|
149
151
|
const url = `${`/${event}`}?${params.join('&')}`
|
|
150
|
-
return _sendV2RequestActionFunction(srv, def, url)
|
|
152
|
+
return _sendV2RequestActionFunction(srv, def, req, url)
|
|
151
153
|
}
|
|
152
154
|
|
|
153
155
|
const _addHandlerActionFunction = (srv, def, target) => {
|
|
@@ -262,6 +264,8 @@ class RemoteService extends cds.Service {
|
|
|
262
264
|
}
|
|
263
265
|
|
|
264
266
|
this.on('*', async (req, next) => {
|
|
267
|
+
const { query } = req
|
|
268
|
+
if (!query && !(typeof req.path === 'string')) return next()
|
|
265
269
|
// early validation on first request for use case without remote API
|
|
266
270
|
// ideally, that's done on bootstrap of the remote service
|
|
267
271
|
if (typeof this.destination === 'object' && !this.destination.url)
|
|
@@ -269,9 +273,6 @@ class RemoteService extends cds.Service {
|
|
|
269
273
|
if (this._resilienceMiddlewares && !this._resilienceMiddlewares.timeout)
|
|
270
274
|
this._resilienceMiddlewares.timeout = cloudSdkResilience().timeout(this.requestTimeout)
|
|
271
275
|
|
|
272
|
-
const { query } = req
|
|
273
|
-
if (!query && !(typeof req.path === 'string')) return next()
|
|
274
|
-
|
|
275
276
|
const resolvedTarget = resolvedTargetOfQuery(query) || getTransition(req.target, this).target
|
|
276
277
|
const reqOptions = getReqOptions(req, query, this)
|
|
277
278
|
reqOptions.headers = _setHeaders(reqOptions.headers, req)
|
|
@@ -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
|
|
|
@@ -427,12 +427,13 @@ const _stringToReqOptions = (query, data, target) => {
|
|
|
427
427
|
return reqOptions
|
|
428
428
|
}
|
|
429
429
|
|
|
430
|
-
const _pathToReqOptions = (method, path, data, target) => {
|
|
430
|
+
const _pathToReqOptions = (method, path, data, target, namespace) => {
|
|
431
431
|
let url = path
|
|
432
432
|
if (!url.startsWith('/')) {
|
|
433
433
|
// extract entity name and instance identifier (either in "()" or after "/") from fully qualified path
|
|
434
434
|
const parts = path.match(/([\w.]*)([\W.]*)(.*)/)
|
|
435
435
|
if (!parts) url = '/' + path.match(/\w*$/)[0]
|
|
436
|
+
else if (url.startsWith(namespace)) url = '/' + parts[1].replace(namespace + '.', '') + parts[2] + parts[3]
|
|
436
437
|
else url = '/' + parts[1].match(/\w*$/)[0] + parts[2] + parts[3]
|
|
437
438
|
|
|
438
439
|
// normalize in case parts[2] already starts with /
|
|
@@ -459,7 +460,7 @@ const getReqOptions = (req, query, service) => {
|
|
|
459
460
|
? _cqnToReqOptions(query, service, req)
|
|
460
461
|
: typeof query === 'string'
|
|
461
462
|
? _stringToReqOptions(query, req.data, req.target)
|
|
462
|
-
: _pathToReqOptions(req.method, req.path, req.data, req.target)
|
|
463
|
+
: _pathToReqOptions(req.method, req.path, req.data, req.target, service.namespace)
|
|
463
464
|
|
|
464
465
|
if (service.kind === 'odata-v2' && req.event === 'READ' && reqOptions.url?.match(/(\/any\()|(\/all\()/)) {
|
|
465
466
|
req.reject(501, 'Lambda expressions are not supported in OData v2')
|
|
@@ -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
|
|
|
@@ -30,6 +30,7 @@ const _addKeysDeep = (keys, keysCollector, ignoreManagedBacklinks) => {
|
|
|
30
30
|
function _keysOf(entity, ignoreManagedBacklinks) {
|
|
31
31
|
const keysCollector = []
|
|
32
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
|