@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
|
@@ -1,77 +1 @@
|
|
|
1
|
-
|
|
2
|
-
const _require = require('../../common/utils/require')
|
|
3
|
-
// _require for better error message
|
|
4
|
-
const express = _require('express')
|
|
5
|
-
const passport = _require('passport')
|
|
6
|
-
const { JWTStrategy } = _require('@sap/xssec')
|
|
7
|
-
const LOG = cds.log('auth')
|
|
8
|
-
|
|
9
|
-
const RESERVED_ATTRIBUTES = new Set([
|
|
10
|
-
'aud',
|
|
11
|
-
'azp',
|
|
12
|
-
'exp',
|
|
13
|
-
'ext_attr',
|
|
14
|
-
'iat',
|
|
15
|
-
'ias_iss',
|
|
16
|
-
'iss',
|
|
17
|
-
'jti',
|
|
18
|
-
'sub',
|
|
19
|
-
'user_uuid',
|
|
20
|
-
'zone_uuid',
|
|
21
|
-
'zid'
|
|
22
|
-
])
|
|
23
|
-
|
|
24
|
-
module.exports = function ias_auth(config) {
|
|
25
|
-
// warn if no credentials
|
|
26
|
-
if (!config.credentials) {
|
|
27
|
-
LOG._warn &&
|
|
28
|
-
LOG.warn(`
|
|
29
|
-
No IAS instance bound to application, but "${config.kind}" configured.
|
|
30
|
-
This is NOT recommended in production!
|
|
31
|
-
`)
|
|
32
|
-
|
|
33
|
-
return (req, res, next) => next()
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
passport.use('IAS', new JWTStrategy(config.credentials))
|
|
37
|
-
return express
|
|
38
|
-
.Router()
|
|
39
|
-
.use(passport.authenticate('IAS', { session: false, failWithError: true }))
|
|
40
|
-
.use((req, res, next) => {
|
|
41
|
-
// grant_type === client_credentials or x509
|
|
42
|
-
if (req.tokenInfo.getClientId() === req.tokenInfo.getSubject()) {
|
|
43
|
-
req.user = new cds.User({
|
|
44
|
-
id: 'system',
|
|
45
|
-
roles: ['authenticated-user'],
|
|
46
|
-
attr: {}
|
|
47
|
-
})
|
|
48
|
-
req.user._is_system = true
|
|
49
|
-
} else {
|
|
50
|
-
// add all unknown attributes to req.user.attr in order to keep public API small
|
|
51
|
-
const payload = req.tokenInfo.getPayload()
|
|
52
|
-
const attributes = Object.keys(payload)
|
|
53
|
-
.filter(k => !RESERVED_ATTRIBUTES.has(k))
|
|
54
|
-
.reduce((attrs, k) => {
|
|
55
|
-
attrs[k] = payload[k]
|
|
56
|
-
return attrs
|
|
57
|
-
}, {})
|
|
58
|
-
|
|
59
|
-
req.user = new cds.User({
|
|
60
|
-
id: req.user.id,
|
|
61
|
-
roles: ['authenticated-user'],
|
|
62
|
-
attr: attributes
|
|
63
|
-
})
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
req.tenant = req.tokenInfo.getZoneId()
|
|
67
|
-
next()
|
|
68
|
-
})
|
|
69
|
-
.use((err, req, res, _next) => {
|
|
70
|
-
if (req.tokenInfo) {
|
|
71
|
-
LOG?.debug('error during token validation', req.tokenInfo.getErrorObject())
|
|
72
|
-
}
|
|
73
|
-
// REVISIT: reject request immediately as our other auth strategies do
|
|
74
|
-
// should we call next(err)? -> I don't think so; it's not an error, is it?
|
|
75
|
-
res.status(401).json({ code: '401', message: 'Unauthorized' }) // REVISIT: this is OData style?
|
|
76
|
-
})
|
|
77
|
-
}
|
|
1
|
+
module.exports = require('../../../../lib/auth/ias-auth')
|
|
@@ -21,17 +21,6 @@ class MockStrategy {
|
|
|
21
21
|
if (user.password && user.password !== password) return this.fail(CHALLENGE)
|
|
22
22
|
|
|
23
23
|
const { features } = req.headers
|
|
24
|
-
// Only in the mock strategy the pseudo roles are kept in the role list.
|
|
25
|
-
// In all other cases pseudo roles are filtered out.
|
|
26
|
-
if (user.roles) {
|
|
27
|
-
if (Array.isArray(user.roles)) {
|
|
28
|
-
if (user.roles.includes('system-user')) user._is_system = true
|
|
29
|
-
if (user.roles.includes('internal-user')) user._is_internal = true
|
|
30
|
-
} else {
|
|
31
|
-
if ('system-user' in user.roles) user._is_system = true
|
|
32
|
-
if ('internal-user' in user.roles) user._is_internal = true
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
24
|
this.success(new cds.User(features ? { ...user, features } : user))
|
|
36
25
|
}
|
|
37
26
|
}
|
|
@@ -55,7 +44,7 @@ const _init_users = (users, tenants = {}) => {
|
|
|
55
44
|
Array.isArray(user.roles) ? user.roles.push(...scopes) : (user.roles = scopes)
|
|
56
45
|
}
|
|
57
46
|
if (user.jwt.grant_type === 'client_credentials' || user.jwt.grant_type === 'client_x509') {
|
|
58
|
-
user.
|
|
47
|
+
Array.isArray(user.roles) ? user.roles.push('system-user') : (user.roles = ['system-user'])
|
|
59
48
|
}
|
|
60
49
|
if (!user.tenant && user.jwt.zid) user.tenant = user.jwt.zid
|
|
61
50
|
}
|
|
@@ -11,8 +11,8 @@ const addRolesFromGrantType = (user, info, credentials) => {
|
|
|
11
11
|
// > not "weak"
|
|
12
12
|
user.roles['authenticated-user'] = true
|
|
13
13
|
if (grantType in CLIENT) {
|
|
14
|
-
user.
|
|
15
|
-
if (info.getClientId() === credentials.clientid) user.
|
|
14
|
+
user.roles['system-user'] = true
|
|
15
|
+
if (info.getClientId() === credentials.clientid) user.roles['internal-user'] = true
|
|
16
16
|
}
|
|
17
17
|
}
|
|
18
18
|
}
|
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
const cds = require('../../../../cds')
|
|
2
2
|
|
|
3
|
-
const {
|
|
3
|
+
const { containsAnyRestrictions, getAccessRestrictions } = require('../../../../common/utils/restrictions')
|
|
4
|
+
const { ODATA_UNAUTHORIZED, ODATA_FORBIDDEN } = require('../../../../common/error/constants')
|
|
4
5
|
|
|
5
6
|
module.exports = srv => {
|
|
6
|
-
const
|
|
7
|
-
const
|
|
7
|
+
const containsRestrictions = containsAnyRestrictions(srv)
|
|
8
|
+
const accessRestrictions = getAccessRestrictions(srv)
|
|
8
9
|
|
|
10
|
+
// eslint-disable-next-line complexity
|
|
9
11
|
return function ODataRequestHandler(odataReq, odataRes, next) {
|
|
10
12
|
const req = odataReq.getBatchApplicationData()
|
|
11
13
|
? odataReq.getBatchApplicationData().req
|
|
@@ -24,23 +26,23 @@ module.exports = srv => {
|
|
|
24
26
|
}
|
|
25
27
|
|
|
26
28
|
// in case of $batch we need to challenge directly, as the header is not processed if in $batch response body
|
|
27
|
-
if (
|
|
29
|
+
if (containsRestrictions && path.endsWith('/$batch') && req.user._is_anonymous) {
|
|
28
30
|
// NOTE: "return req._login()" would not invoke custom error handlers
|
|
29
31
|
if (req._login) res.set('WWW-Authenticate', `Basic realm="Users"`)
|
|
30
32
|
else if (user._challenges) res.set('WWW-Authenticate', user._challenges.join(';'))
|
|
31
|
-
return next(
|
|
33
|
+
return next(ODATA_UNAUTHORIZED)
|
|
32
34
|
}
|
|
33
35
|
|
|
34
|
-
// check @requires as soon as possible (DoS)
|
|
35
|
-
if (
|
|
36
|
+
// check @restrict and @requires as soon as possible (DoS)
|
|
37
|
+
if (!accessRestrictions.some(r => user.is(r))) {
|
|
36
38
|
// > unauthorized or forbidden?
|
|
37
39
|
if (req.user._is_anonymous) {
|
|
38
40
|
// NOTE: "return req._login()" would not invoke custom error handlers
|
|
39
41
|
if (req._login) res.set('WWW-Authenticate', `Basic realm="Users"`)
|
|
40
42
|
else if (user._challenges) res.set('WWW-Authenticate', user._challenges.join(';'))
|
|
41
|
-
return next(
|
|
43
|
+
return next(ODATA_UNAUTHORIZED)
|
|
42
44
|
}
|
|
43
|
-
return next(
|
|
45
|
+
return next(ODATA_FORBIDDEN)
|
|
44
46
|
}
|
|
45
47
|
|
|
46
48
|
/*
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
const cds = require('../../../../cds')
|
|
2
|
+
|
|
1
3
|
const odata = require('../okra/odata-server')
|
|
2
4
|
const ExpressionKind = odata.uri.Expression.ExpressionKind
|
|
3
5
|
const BinaryOperatorKind = odata.uri.BinaryExpression.OperatorKind
|
|
@@ -41,7 +43,10 @@ class ExpressionToCQN {
|
|
|
41
43
|
case EdmPrimitiveTypeKind.Int16:
|
|
42
44
|
case EdmPrimitiveTypeKind.Int32:
|
|
43
45
|
return { val: parseInt(value) }
|
|
46
|
+
case EdmPrimitiveTypeKind.Int64:
|
|
47
|
+
return { val: value.toString() }
|
|
44
48
|
case EdmPrimitiveTypeKind.Decimal:
|
|
49
|
+
return cds.env.features.compat_decimal ? { val: parseFloat(value) } : { val: value.toString() }
|
|
45
50
|
case EdmPrimitiveTypeKind.Single:
|
|
46
51
|
case EdmPrimitiveTypeKind.Double:
|
|
47
52
|
return { val: parseFloat(value) }
|
|
@@ -225,8 +225,11 @@ const applyToCQN = (transformations, entity, model) => {
|
|
|
225
225
|
case TransformationKind.BOTTOM_TOP:
|
|
226
226
|
_addBottomTopTransformation(transformation, res)
|
|
227
227
|
break
|
|
228
|
-
default:
|
|
229
|
-
|
|
228
|
+
default: {
|
|
229
|
+
const numericKind = transformation.getKind()
|
|
230
|
+
const stringKind = Object.entries(TransformationKind).find(([_, v]) => v === numericKind)?.[0]
|
|
231
|
+
throw getFeatureNotSupportedError(`Transformation "${stringKind || numericKind}" with query option $apply`)
|
|
232
|
+
}
|
|
230
233
|
}
|
|
231
234
|
}
|
|
232
235
|
|
|
@@ -175,6 +175,10 @@ class UriParser {
|
|
|
175
175
|
* @param {UriInfo} uriInfo the result of parsing
|
|
176
176
|
*/
|
|
177
177
|
parseQueryOptions (queryOptions, uriInfo) {
|
|
178
|
+
// EXPERIMENTAL FEATURE FLAGS!
|
|
179
|
+
const { okra_skip_query_options, odata_new_parser } = global.cds.env.features
|
|
180
|
+
if (okra_skip_query_options && odata_new_parser) return
|
|
181
|
+
|
|
178
182
|
const lastSegment = uriInfo.getLastSegment()
|
|
179
183
|
const crossjoinEntitySets = lastSegment.getCrossjoinEntitySets()
|
|
180
184
|
const aliases = uriInfo.getAliases()
|
|
@@ -29,6 +29,7 @@ const _isSameEntityInWhere = (where, target, persistentObj) => {
|
|
|
29
29
|
const key = where[i].ref
|
|
30
30
|
const val = where[i + 2].val
|
|
31
31
|
const sign = where[i + 1]
|
|
32
|
+
|
|
32
33
|
// eslint-disable-next-line
|
|
33
34
|
if (target.elements[key].key && key in persistentObj && sign === '=' && val !== persistentObj[key]) {
|
|
34
35
|
return false
|
|
@@ -255,6 +256,7 @@ const _select = ({
|
|
|
255
256
|
const _selectDeepUpdateData = async args => {
|
|
256
257
|
const { model, compositionTree, entityName, data, root, selectData, tx, selectAllColumns, where, parentKeys } = args
|
|
257
258
|
let result = []
|
|
259
|
+
|
|
258
260
|
if (!where && parentKeys && parentKeys.length && Object.keys(parentKeys[0]).length) {
|
|
259
261
|
const keys0 = Object.keys(parentKeys[0])
|
|
260
262
|
const keys = { list: keys0.map(pk => ({ ref: [pk] })) }
|
|
@@ -296,9 +298,7 @@ const _selectDeepUpdateData = async args => {
|
|
|
296
298
|
root: false
|
|
297
299
|
}
|
|
298
300
|
|
|
299
|
-
// REVISIT: remove null elements
|
|
300
|
-
subs.data = subs.data.filter(d => d)
|
|
301
|
-
|
|
301
|
+
subs.data = subs.data.filter(d => d) // REVISIT: remove null elements
|
|
302
302
|
return _selectDeepUpdateData({ ...args, ...subs })
|
|
303
303
|
})
|
|
304
304
|
)
|
|
@@ -310,6 +310,7 @@ const _selectDeepUpdateData = async args => {
|
|
|
310
310
|
const _resolveOrderBy = (orderBy, transitions) => {
|
|
311
311
|
// no resolved entity found
|
|
312
312
|
if (!transitions?.length) return
|
|
313
|
+
|
|
313
314
|
// if there are no renamed fields, no need to resolve
|
|
314
315
|
if (!transitions[0].mapping.size) return
|
|
315
316
|
if (orderBy) orderBy.map(el => (el.ref[0] = transitions[0].mapping.get(el.ref[0]).ref[0]))
|
|
@@ -321,6 +322,7 @@ const _resolveOrderBy = (orderBy, transitions) => {
|
|
|
321
322
|
|
|
322
323
|
const selectDeepUpdateData = (service, model, req, selectAllColumns = false) => {
|
|
323
324
|
const query = req.query
|
|
325
|
+
|
|
324
326
|
// REVISIT this should be done somewhere before, so it is not done twice for deep updates
|
|
325
327
|
const sqlQuery = cqn2cqn4sql(query, model)
|
|
326
328
|
|
|
@@ -17,9 +17,8 @@ function _hasCompOrAssoc(entity, k) {
|
|
|
17
17
|
|
|
18
18
|
const _addSubDeepInsertCQN = (model, compositionTree, data, cqns, draft) => {
|
|
19
19
|
compositionTree.compositionElements.forEach(element => {
|
|
20
|
-
if (element.skipPersistence)
|
|
21
|
-
|
|
22
|
-
}
|
|
20
|
+
if (element.skipPersistence) return
|
|
21
|
+
|
|
23
22
|
// element source must be changed in comp tree
|
|
24
23
|
const subEntity = model.definitions[element.source]
|
|
25
24
|
const into = ctUtils.addDraftSuffix(draft, subEntity.name)
|
|
@@ -38,15 +37,19 @@ const _addSubDeepInsertCQN = (model, compositionTree, data, cqns, draft) => {
|
|
|
38
37
|
}
|
|
39
38
|
}
|
|
40
39
|
}
|
|
40
|
+
|
|
41
41
|
return result
|
|
42
42
|
}, [])
|
|
43
|
+
|
|
43
44
|
if (insertCQN.INSERT.entries.length > 0) {
|
|
44
45
|
cqns.push(insertCQN)
|
|
45
46
|
}
|
|
47
|
+
|
|
46
48
|
if (subData.length > 0) {
|
|
47
49
|
_addSubDeepInsertCQN(model, element, subData, cqns, draft)
|
|
48
50
|
}
|
|
49
51
|
})
|
|
52
|
+
|
|
50
53
|
return cqns
|
|
51
54
|
}
|
|
52
55
|
|
|
@@ -1,13 +1,10 @@
|
|
|
1
1
|
const cds = require('../../cds')
|
|
2
|
-
|
|
3
2
|
const { getCompositionTree } = require('./tree')
|
|
4
3
|
const { getDeepInsertCQNs } = require('./insert')
|
|
5
4
|
const { getDeepDeleteCQNs } = require('./delete')
|
|
6
5
|
const ctUtils = require('./utils')
|
|
7
|
-
|
|
8
6
|
const { ensureNoDraftsSuffix } = require('../utils/draft')
|
|
9
7
|
const { deepCopyObject } = require('../utils/copy')
|
|
10
|
-
|
|
11
8
|
const getError = require('../../common/error')
|
|
12
9
|
const { getEntityNameFromUpdateCQN } = require('../utils/cqn')
|
|
13
10
|
|
|
@@ -31,28 +28,35 @@ const _serializedKey = (entity, data) => {
|
|
|
31
28
|
|
|
32
29
|
const _dataByKey = (entity, data) => {
|
|
33
30
|
const dataByKey = new Map()
|
|
31
|
+
|
|
34
32
|
for (const entry of data) {
|
|
35
33
|
dataByKey.set(_serializedKey(entity, entry), entry)
|
|
36
34
|
}
|
|
35
|
+
|
|
37
36
|
return dataByKey
|
|
38
37
|
}
|
|
39
38
|
|
|
40
39
|
function _addSubDeepUpdateCQNForDelete({ entity, data, selectData, entityName, deleteCQNs }) {
|
|
41
40
|
const dataByKey = _dataByKey(entity, data)
|
|
41
|
+
|
|
42
42
|
if (selectData.length && selectData[0] && Object.keys(selectData[0]).length) {
|
|
43
43
|
for (let j = 0; j < selectData.length; j += CHUNK_SIZE) {
|
|
44
44
|
const deleteCQN = { DELETE: { from: entityName, where: [] } }
|
|
45
|
+
|
|
45
46
|
for (let i = j; i < j + CHUNK_SIZE && i < selectData.length; i++) {
|
|
46
47
|
const selectEntry = selectData[i]
|
|
47
48
|
if (!selectEntry) continue
|
|
49
|
+
|
|
48
50
|
const dataEntry = dataByKey.get(_serializedKey(entity, selectEntry))
|
|
49
51
|
if (!dataEntry) {
|
|
50
52
|
if (deleteCQN.DELETE.where.length > 0) {
|
|
51
53
|
deleteCQN.DELETE.where.push('or')
|
|
52
54
|
}
|
|
55
|
+
|
|
53
56
|
deleteCQN.DELETE.where.push({ xpr: [...ctUtils.whereKey(ctUtils.key(entity, selectEntry))] })
|
|
54
57
|
}
|
|
55
58
|
}
|
|
59
|
+
|
|
56
60
|
if (deleteCQN.DELETE.where.length) deleteCQNs.push(deleteCQN)
|
|
57
61
|
}
|
|
58
62
|
}
|
|
@@ -63,6 +67,7 @@ const _unwrapVal = obj => {
|
|
|
63
67
|
const value = obj[key]
|
|
64
68
|
if (value && value.val) obj[key] = value.val
|
|
65
69
|
}
|
|
70
|
+
|
|
66
71
|
return obj
|
|
67
72
|
}
|
|
68
73
|
|
|
@@ -120,6 +125,7 @@ const _diffData = (newData, oldData, entity, newEntry, oldEntry, model) => {
|
|
|
120
125
|
function _addSubDeepUpdateCQNForUpdateInsert({ entity, entityName, data, selectData, updateCQNs, insertCQN, model }) {
|
|
121
126
|
const selectDataByKey = _dataByKey(entity, selectData)
|
|
122
127
|
const deepUpdateData = []
|
|
128
|
+
|
|
123
129
|
for (const entry of data) {
|
|
124
130
|
if (entry === null) continue
|
|
125
131
|
|
|
@@ -138,6 +144,7 @@ function _addSubDeepUpdateCQNForUpdateInsert({ entity, entityName, data, selectD
|
|
|
138
144
|
// inserts are handled deep so they must not be put into deepUpdateData
|
|
139
145
|
}
|
|
140
146
|
}
|
|
147
|
+
|
|
141
148
|
return deepUpdateData
|
|
142
149
|
}
|
|
143
150
|
|
|
@@ -152,9 +159,7 @@ async function _addSubDeepUpdateCQNCollect(model, cqns, updateCQNs, insertCQN, d
|
|
|
152
159
|
cqns[0] = cqns[0] || []
|
|
153
160
|
const deepInsertCQNs = getDeepInsertCQNs(model, insertCQN)
|
|
154
161
|
deepInsertCQNs.forEach(insertCQN => {
|
|
155
|
-
const intoCQN = cqns[0].find(cqn =>
|
|
156
|
-
return cqn.INSERT && cqn.INSERT.into === insertCQN.INSERT.into
|
|
157
|
-
})
|
|
162
|
+
const intoCQN = cqns[0].find(cqn => cqn.INSERT?.into === insertCQN.INSERT.into)
|
|
158
163
|
if (!intoCQN) {
|
|
159
164
|
cqns[0].push(insertCQN)
|
|
160
165
|
} else {
|
|
@@ -202,6 +207,7 @@ async function _addSubDeepUpdateCQNRecursion({ model, compositionTree, entity, d
|
|
|
202
207
|
if (selectEntry[element.name] === null && entry[element.name] === null) {
|
|
203
208
|
continue
|
|
204
209
|
}
|
|
210
|
+
|
|
205
211
|
_addToData(selectSubData, entity, element, selectEntry)
|
|
206
212
|
}
|
|
207
213
|
|
|
@@ -246,7 +252,6 @@ const _addSubDeepUpdateCQN = async ({ model, compositionTree, data, selectData,
|
|
|
246
252
|
})
|
|
247
253
|
|
|
248
254
|
await _addSubDeepUpdateCQNCollect(model, cqns, updateCQNs, insertCQN, deleteCQNs, req)
|
|
249
|
-
|
|
250
255
|
if (deepUpdateData.length === 0) return Promise.resolve()
|
|
251
256
|
|
|
252
257
|
return _addSubDeepUpdateCQNRecursion({
|
|
@@ -282,7 +287,6 @@ const hasDeepUpdate = (model, cqn) => {
|
|
|
282
287
|
const getDeepUpdateCQNs = async (model, req, selectData) => {
|
|
283
288
|
const { query } = req
|
|
284
289
|
if (!Array.isArray(selectData)) selectData = [selectData]
|
|
285
|
-
|
|
286
290
|
if (selectData.length === 0) return []
|
|
287
291
|
if (selectData.length > 1) throw getError('Deep update can only be performed on a single instance')
|
|
288
292
|
|
|
@@ -11,5 +11,10 @@ module.exports = {
|
|
|
11
11
|
DEFAULT_SEVERITY: 2,
|
|
12
12
|
MIN_SEVERITY: 1,
|
|
13
13
|
MAX_SEVERITY: 4,
|
|
14
|
-
MULTIPLE_ERRORS: 'MULTIPLE_ERRORS'
|
|
14
|
+
MULTIPLE_ERRORS: 'MULTIPLE_ERRORS',
|
|
15
|
+
/*
|
|
16
|
+
* OData
|
|
17
|
+
*/
|
|
18
|
+
ODATA_UNAUTHORIZED: { statusCode: 401, code: '401', message: 'Unauthorized' },
|
|
19
|
+
ODATA_FORBIDDEN: { statusCode: 403, code: '403', message: 'Forbidden' }
|
|
15
20
|
}
|
|
@@ -1,7 +1,12 @@
|
|
|
1
1
|
const { reject, getRejectReason, getAuthRelevantEntity } = require('./utils')
|
|
2
2
|
const { CRUD_EVENTS } = require('./constants')
|
|
3
3
|
|
|
4
|
-
const
|
|
4
|
+
const _getRequiresAsArray = definition =>
|
|
5
|
+
definition['@requires']
|
|
6
|
+
? Array.isArray(definition['@requires'])
|
|
7
|
+
? definition['@requires']
|
|
8
|
+
: [definition['@requires']]
|
|
9
|
+
: false
|
|
5
10
|
|
|
6
11
|
function handler(req) {
|
|
7
12
|
if (req.user._is_privileged) {
|
|
@@ -13,7 +18,7 @@ function handler(req) {
|
|
|
13
18
|
if (req.event in CRUD_EVENTS) {
|
|
14
19
|
// > CRUD
|
|
15
20
|
definition = getAuthRelevantEntity(req, this.model, ['@requires', '@restrict'])
|
|
16
|
-
} else if (req.target
|
|
21
|
+
} else if (req.target?.actions) {
|
|
17
22
|
// > bound
|
|
18
23
|
definition = req.target.actions[req.event]
|
|
19
24
|
} else {
|
|
@@ -23,7 +28,10 @@ function handler(req) {
|
|
|
23
28
|
|
|
24
29
|
if (!definition) return
|
|
25
30
|
|
|
26
|
-
|
|
31
|
+
// also check target entity for bound operations
|
|
32
|
+
const requires =
|
|
33
|
+
_getRequiresAsArray(definition) ||
|
|
34
|
+
(['action', 'function'].includes(definition.kind) && req.target && _getRequiresAsArray(req.target))
|
|
27
35
|
if (!requires || requires.some(role => req.user.is(role))) return
|
|
28
36
|
|
|
29
37
|
reject(req, getRejectReason(req, '@requires', definition))
|
|
@@ -37,11 +37,9 @@ const _getResolvedApplicables = (applicables, req) => {
|
|
|
37
37
|
return resolvedApplicables
|
|
38
38
|
}
|
|
39
39
|
|
|
40
|
-
const
|
|
41
|
-
return (
|
|
42
|
-
|
|
43
|
-
resolvedApplicables[0]._xpr.length === 3 &&
|
|
44
|
-
resolvedApplicables[0]._xpr.every(ele => typeof ele !== 'object' || ele.val)
|
|
40
|
+
const _getStaticAuthRestrictions = resolvedApplicables => {
|
|
41
|
+
return resolvedApplicables.filter(
|
|
42
|
+
resolved => resolved && resolved._xpr.length === 3 && resolved._xpr.every(ele => typeof ele !== 'object' || ele.val)
|
|
45
43
|
)
|
|
46
44
|
}
|
|
47
45
|
|
|
@@ -68,14 +66,14 @@ const _evalStatic = (op, vals) => {
|
|
|
68
66
|
}
|
|
69
67
|
}
|
|
70
68
|
|
|
71
|
-
const
|
|
72
|
-
const
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
69
|
+
const _handleStaticAuthRestrictions = (resolvedApplicables, req) => {
|
|
70
|
+
const isAllowed = resolvedApplicables.some(restriction => {
|
|
71
|
+
const op = restriction._xpr.find(ele => typeof ele === 'string')
|
|
72
|
+
const vals = restriction._xpr.filter(ele => typeof ele === 'object' && ele.val).map(ele => ele.val)
|
|
73
|
+
return _evalStatic(op, vals)
|
|
74
|
+
})
|
|
75
|
+
// static clause grants access => done
|
|
76
|
+
if (isAllowed) return
|
|
79
77
|
|
|
80
78
|
// static clause forbids access => forbidden
|
|
81
79
|
return reject(req)
|
|
@@ -224,8 +222,15 @@ async function handler(req) {
|
|
|
224
222
|
return
|
|
225
223
|
}
|
|
226
224
|
|
|
225
|
+
// READ UPDATE DELETE on draft enabled entities are unrestricted, because only the owner can access them
|
|
226
|
+
const draftUnRestrictedEvents = ['READ', 'UPDATE', 'DELETE', 'CREATE']
|
|
227
|
+
if (definition.isDraft && draftUnRestrictedEvents.includes(req.event)) {
|
|
228
|
+
return
|
|
229
|
+
}
|
|
230
|
+
|
|
227
231
|
let restrictions = this.getRestrictions.call(this, definition, req.event, req.user)
|
|
228
232
|
if (restrictions instanceof Promise) restrictions = await restrictions
|
|
233
|
+
|
|
229
234
|
if (!restrictions) {
|
|
230
235
|
// > unrestricted
|
|
231
236
|
return
|
|
@@ -243,8 +248,9 @@ async function handler(req) {
|
|
|
243
248
|
const resolvedApplicables = _getResolvedApplicables(restrictions, req)
|
|
244
249
|
|
|
245
250
|
// REVISIT: support more complex statics
|
|
246
|
-
|
|
247
|
-
|
|
251
|
+
const staticAuthRestriction = _getStaticAuthRestrictions(resolvedApplicables)
|
|
252
|
+
if (staticAuthRestriction.length > 0) {
|
|
253
|
+
return _handleStaticAuthRestrictions(staticAuthRestriction, req)
|
|
248
254
|
}
|
|
249
255
|
|
|
250
256
|
if (req.event === 'READ') {
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
const WRITE_EVENTS = { CREATE: 1, NEW: 1, UPDATE: 1, PATCH: 1, DELETE: 1, CANCEL: 1, EDIT: 1 }
|
|
2
2
|
const CRUD = Object.assign({ READ: 1 }, WRITE_EVENTS)
|
|
3
|
+
const cds = require('../../../cds')
|
|
3
4
|
|
|
4
5
|
/**
|
|
5
6
|
* Returns the applicable restrictions for the current request as follows:
|
|
@@ -109,12 +110,14 @@ const getNormalizedRestrictions = (definition, definitions) => {
|
|
|
109
110
|
return isRestricted ? restricts : null
|
|
110
111
|
}
|
|
111
112
|
|
|
112
|
-
const _isGrantAccessAllowed = (eventName, restrict) =>
|
|
113
|
+
const _isGrantAccessAllowed = (eventName, restrict) =>
|
|
114
|
+
restrict.grant === '*' || (eventName === 'EDIT' && restrict.grant === 'UPDATE') || restrict.grant === eventName
|
|
115
|
+
|
|
113
116
|
const _isToAccessAllowed = (user, restrict) => restrict.to.some(role => user.is(role))
|
|
114
117
|
|
|
115
118
|
const getApplicableRestrictions = (restrictions, event, user) => {
|
|
116
119
|
return restrictions.filter(restrict => {
|
|
117
|
-
const eventName = { NEW: 'CREATE'
|
|
120
|
+
const eventName = cds.env.fiori.lean_draft ? event : { NEW: 'CREATE' }[event] || event
|
|
118
121
|
return _isGrantAccessAllowed(eventName, restrict) && _isToAccessAllowed(user, restrict)
|
|
119
122
|
})
|
|
120
123
|
}
|
|
@@ -14,6 +14,12 @@ const _targetEntityDoesNotExist = async req => {
|
|
|
14
14
|
exports.impl = cds.service.impl(function () {
|
|
15
15
|
// eslint-disable-next-line complexity
|
|
16
16
|
this.on(['CREATE', 'READ', 'UPDATE', 'DELETE', 'UPSERT'], '*', async function (req) {
|
|
17
|
+
if (!req.query) {
|
|
18
|
+
throw getError({
|
|
19
|
+
code: 501,
|
|
20
|
+
message: 'The request has no query and cannot be served generically.'
|
|
21
|
+
})
|
|
22
|
+
}
|
|
17
23
|
if (typeof req.query !== 'string' && req.target && req.target._hasPersistenceSkip) {
|
|
18
24
|
throw getError({
|
|
19
25
|
code: 501,
|
|
@@ -10,7 +10,8 @@ const MAX = cds.env.query?.limit?.max || 1000
|
|
|
10
10
|
const _cached = Symbol('@cds.query.limit')
|
|
11
11
|
|
|
12
12
|
const getPageSize = def => {
|
|
13
|
-
|
|
13
|
+
// do not look at prototypes re cached settings
|
|
14
|
+
if (Object.hasOwn(def, _cached)) return def[_cached]
|
|
14
15
|
let max = def['@cds.query.limit.max'] ?? def._service?.['@cds.query.limit.max'] ?? MAX
|
|
15
16
|
let _default =
|
|
16
17
|
def['@cds.query.limit.default'] ??
|
|
@@ -34,6 +35,7 @@ const commonGenericPaging = function (req) {
|
|
|
34
35
|
}
|
|
35
36
|
|
|
36
37
|
const _addPaging = function ({ SELECT }, target) {
|
|
38
|
+
if (SELECT.limit === false) return
|
|
37
39
|
const { rows } = SELECT.limit || (SELECT.limit = {})
|
|
38
40
|
const conf = getPageSize(target)
|
|
39
41
|
SELECT.limit.rows = {
|
|
@@ -83,6 +83,7 @@ CRUD_VIA_NAVIGATION_NOT_SUPPORTED=CRUD via navigations is not yet supported
|
|
|
83
83
|
|
|
84
84
|
# draft
|
|
85
85
|
DRAFT_ALREADY_EXISTS=A draft for this entity already exists
|
|
86
|
+
DRAFT_NOT_EXISTING=No draft for this entity exists
|
|
86
87
|
DRAFT_LOCKED_BY_ANOTHER_USER=The entity is locked by user "{0}"
|
|
87
88
|
DRAFT_MODIFICATION_ONLY_VIA_ROOT=A draft can only be modified via its root entity
|
|
88
89
|
|
|
@@ -3,7 +3,6 @@
|
|
|
3
3
|
const cds = require('../../cds')
|
|
4
4
|
const { SELECT, INSERT, DELETE, UPDATE } = cds.ql
|
|
5
5
|
const Query = require('../../../../lib/ql/Query')
|
|
6
|
-
|
|
7
6
|
const { resolveView } = require('./resolveView')
|
|
8
7
|
const { ensureNoDraftsSuffix, getDraftColumnsCQNForDraft, ensureDraftsSuffix } = require('./draft')
|
|
9
8
|
const { flattenStructuredSelect, OPERATIONS_MAP } = require('./structured')
|
|
@@ -807,8 +806,7 @@ const _convertSelect = (query, model, _options) => {
|
|
|
807
806
|
const cols = getColumns(target, { onlyNames: true, filterVirtual: true })
|
|
808
807
|
query.columns(cols)
|
|
809
808
|
if (target._isDraftEnabled && query._target._unresolved) {
|
|
810
|
-
query.SELECT.columns.push(...getDraftColumnsCQNForDraft(target))
|
|
811
|
-
query.SELECT.columns.push({ ref: ['DraftAdministrativeData_DraftUUID'] })
|
|
809
|
+
query.SELECT.columns.push(...getDraftColumnsCQNForDraft(target), { ref: ['DraftAdministrativeData_DraftUUID'] })
|
|
812
810
|
}
|
|
813
811
|
}
|
|
814
812
|
}
|
|
@@ -822,7 +820,8 @@ const _convertUpsert = (query, model) => {
|
|
|
822
820
|
|
|
823
821
|
const target = model.definitions[resolvedIntoClause]
|
|
824
822
|
if (!target) {
|
|
825
|
-
// if there is no target, just return original query, as a copy is not deep anyways
|
|
823
|
+
// if there is no target, just return original query, as a copy is not deep anyways
|
|
824
|
+
// and all the sub items of query.UPSERT are referenced only anyways
|
|
826
825
|
return query
|
|
827
826
|
}
|
|
828
827
|
|
|
@@ -867,7 +866,6 @@ const _convertInsert = (query, model) => {
|
|
|
867
866
|
|
|
868
867
|
const target = model.definitions[resolvedIntoClause]
|
|
869
868
|
if (!target) return insert
|
|
870
|
-
|
|
871
869
|
return resolveView(insert, model, cds.db)
|
|
872
870
|
}
|
|
873
871
|
|
|
@@ -246,7 +246,8 @@ const _newWhereRef = (newWhereElement, transition, alias, tableName, isSubSelect
|
|
|
246
246
|
if (mapped) newRef[1] = mapped.ref[0]
|
|
247
247
|
} else {
|
|
248
248
|
const mapped = transition.mapping.get(newRef[0])
|
|
249
|
-
if (isSubSelect && mapped) {
|
|
249
|
+
if (isSubSelect && mapped && newRef.length === 1) {
|
|
250
|
+
// Add a table alias prefix only for not-yet-qualified refs
|
|
250
251
|
newRef.unshift(transition.target.name)
|
|
251
252
|
newRef[1] = mapped.ref[0]
|
|
252
253
|
} else {
|
|
@@ -734,6 +735,7 @@ const resolveView = (query, model, service) => {
|
|
|
734
735
|
// restore logger and clear _event
|
|
735
736
|
LOG = _LOG
|
|
736
737
|
_event = undefined
|
|
738
|
+
|
|
737
739
|
return newQuery
|
|
738
740
|
}
|
|
739
741
|
|