@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.
Files changed (83) hide show
  1. package/CHANGELOG.md +68 -4
  2. package/apis/cds.d.ts +10 -6
  3. package/apis/connect.d.ts +1 -2
  4. package/apis/core.d.ts +54 -5
  5. package/apis/log.d.ts +19 -6
  6. package/apis/models.d.ts +0 -18
  7. package/apis/ql.d.ts +23 -23
  8. package/apis/serve.d.ts +18 -15
  9. package/apis/services.d.ts +67 -56
  10. package/apis/test.d.ts +1 -2
  11. package/bin/serve.js +4 -4
  12. package/common.cds +4 -4
  13. package/lib/auth/basic-auth.js +1 -1
  14. package/lib/auth/dummy-auth.js +2 -1
  15. package/lib/auth/ias-auth.js +68 -2
  16. package/lib/auth/index.js +5 -5
  17. package/lib/auth/jwt-auth.js +40 -24
  18. package/lib/auth/mocked-users.js +0 -13
  19. package/lib/auth/passport-basic.js +2 -0
  20. package/lib/auth/passport-digest.js +2 -0
  21. package/lib/compile/etc/_localized.js +0 -1
  22. package/lib/compile/extend.js +16 -0
  23. package/lib/compile/for/lean_drafts.js +38 -6
  24. package/lib/compile/resolve.js +7 -5
  25. package/lib/compile/to/json.js +6 -2
  26. package/lib/dbs/cds-deploy.js +3 -3
  27. package/lib/env/cds-env.js +3 -3
  28. package/lib/env/cds-requires.js +1 -0
  29. package/lib/env/defaults.js +8 -1
  30. package/lib/env/schemas/cds-rc.json +27 -3
  31. package/lib/i18n/localize.js +3 -3
  32. package/lib/index.js +4 -0
  33. package/lib/log/cds-log.js +10 -1
  34. package/lib/ql/Whereable.js +7 -3
  35. package/lib/req/user.js +18 -16
  36. package/lib/srv/middlewares/sap-statistics.js +3 -3
  37. package/lib/srv/middlewares/trace.js +5 -4
  38. package/lib/srv/srv-dispatch.js +10 -9
  39. package/lib/utils/axios.js +3 -0
  40. package/lib/utils/cds-test.js +3 -0
  41. package/lib/utils/cds-utils.js +2 -0
  42. package/libx/_runtime/auth/index.js +8 -32
  43. package/libx/_runtime/auth/strategies/ias-auth.js +1 -77
  44. package/libx/_runtime/auth/strategies/mock.js +1 -12
  45. package/libx/_runtime/auth/strategies/xssecUtils.js +2 -2
  46. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/request.js +11 -9
  47. package/libx/_runtime/cds-services/adapter/odata-v4/odata-to-cqn/ExpressionToCQN.js +5 -0
  48. package/libx/_runtime/cds-services/adapter/odata-v4/odata-to-cqn/applyToCQN.js +5 -2
  49. package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-commons/uri/UriParser.js +4 -0
  50. package/libx/_runtime/common/composition/data.js +5 -3
  51. package/libx/_runtime/common/composition/insert.js +6 -3
  52. package/libx/_runtime/common/composition/update.js +12 -8
  53. package/libx/_runtime/common/error/constants.js +6 -1
  54. package/libx/_runtime/common/generic/auth/requires.js +11 -3
  55. package/libx/_runtime/common/generic/auth/restrict.js +21 -15
  56. package/libx/_runtime/common/generic/auth/restrictions.js +5 -2
  57. package/libx/_runtime/common/generic/crud.js +6 -0
  58. package/libx/_runtime/common/generic/paging.js +3 -1
  59. package/libx/_runtime/common/i18n/messages.properties +1 -0
  60. package/libx/_runtime/common/utils/cqn2cqn4sql.js +3 -5
  61. package/libx/_runtime/common/utils/resolveView.js +3 -1
  62. package/libx/_runtime/common/utils/restrictions.js +47 -0
  63. package/libx/_runtime/db/data-conversion/post-processing.js +3 -3
  64. package/libx/_runtime/db/generic/input.js +1 -1
  65. package/libx/_runtime/db/sql-builder/ExpressionBuilder.js +0 -17
  66. package/libx/_runtime/fiori/lean-draft.js +27 -24
  67. package/libx/_runtime/hana/driver.js +2 -4
  68. package/libx/_runtime/hana/pool.js +1 -1
  69. package/libx/_runtime/messaging/enterprise-messaging-utils/registerEndpoints.js +2 -2
  70. package/libx/_runtime/messaging/outbox/utils.js +1 -2
  71. package/libx/_runtime/remote/Service.js +10 -9
  72. package/libx/_runtime/remote/utils/client.js +4 -3
  73. package/libx/_runtime/sqlite/Service.js +0 -4
  74. package/libx/_runtime/sqlite/customBuilder/CustomFunctionBuilder.js +2 -1
  75. package/libx/odata/afterburner.js +5 -3
  76. package/libx/odata/cqn2odata.js +7 -7
  77. package/libx/odata/utils.js +4 -1
  78. package/libx/rest/RestAdapter.js +15 -16
  79. package/package.json +1 -1
  80. package/lib/auth/xsuaa-auth.js +0 -2
  81. package/libx/_runtime/auth/utils.js +0 -32
  82. package/libx/audit-log/client.cds +0 -0
  83. package/libx/audit-log/client.js +0 -0
@@ -1,77 +1 @@
1
- const cds = require('../../../../lib')
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._is_system = true
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._is_system = true
15
- if (info.getClientId() === credentials.clientid) user._is_internal = true
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 { UNAUTHORIZED, FORBIDDEN, getRequiresAsArray, isRestricted } = require('../../../../auth/utils')
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 requires = getRequiresAsArray(srv.definition)
7
- const restricted = isRestricted(srv)
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 (restricted && path.endsWith('/$batch') && req.user._is_anonymous) {
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(UNAUTHORIZED)
33
+ return next(ODATA_UNAUTHORIZED)
32
34
  }
33
35
 
34
- // check @requires as soon as possible (DoS)
35
- if (requires && !requires.some(r => user.is(r))) {
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(UNAUTHORIZED)
43
+ return next(ODATA_UNAUTHORIZED)
42
44
  }
43
- return next(FORBIDDEN)
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
- throw getFeatureNotSupportedError(`Transformation "${transformation.getKind()}" with query option $apply`)
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
- return
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 { getRequiresAsArray } = require('../../../auth/utils')
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 && req.target.actions) {
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
- const requires = getRequiresAsArray(definition)
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 _isStaticAuth = resolvedApplicables => {
41
- return (
42
- resolvedApplicables.length === 1 &&
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 _handleStaticAuth = (resolvedApplicables, req) => {
72
- const op = resolvedApplicables[0]._xpr.find(ele => typeof ele === 'string')
73
- const vals = resolvedApplicables[0]._xpr.filter(ele => typeof ele === 'object' && ele.val).map(ele => ele.val)
74
-
75
- if (_evalStatic(op, vals)) {
76
- // static clause grants access => done
77
- return
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
- if (_isStaticAuth(resolvedApplicables)) {
247
- return _handleStaticAuth(resolvedApplicables, req)
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) => restrict.grant === '*' || restrict.grant === eventName
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', EDIT: 'UPDATE' }[event] || event
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
- if (_cached in def) return def[_cached]
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 and all the sub items of query.UPSERT are referenced only 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