@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
@@ -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 = {}, cqn = {}) => {
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 = Object_keys(query._target.keys)
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
- const result = await handle(req)
342
- return result
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 = Object_keys(query._target.keys).filter(k => k !== 'IsActiveEntity')
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.columns = keys.map(k => ({ ref: [k] }))
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 = undefined // We need all entries for the keys to properly select actives (count)
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
- const keys = Object_keys(query._target.keys).filter(k => k !== 'IsActiveEntity')
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 = keys.map(k => ({ ref: [k] })).concat(additionalCols)
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 = Object_keys(target.keys).filter(k => k !== 'IsActiveEntity')
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 = Object_keys(query._target.keys)
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 _keys = Object_keys(target.keys).filter(k => k !== 'IsActiveEntity')
597
- const hash = row => _keys.map(k => row[k]).reduce((res, curr) => res + '|$|' + curr, '')
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(keys)
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 required. Please make sure it is installed.`)
198
+ throw new Error(`"${name}" could not be found. Please make sure it is installed.`)
199
199
  } else {
200
- throw new Error(
201
- 'Neither "hdb" nor "@sap/hana-client" could be required. Please make sure one of them is installed.'
202
- )
200
+ throw new Error('Neither "hdb" nor "@sap/hana-client" could be found. Please make sure one of them is installed.')
203
201
  }
204
202
  }
205
203
  }
@@ -1,7 +1,7 @@
1
1
  const cds = require('../cds')
2
2
  const LOG = cds.log('pool|db')
3
3
 
4
- const { pool } = require('@sap/cds-foss')
4
+ const { pool } = cds.utils
5
5
  const hana = require('./driver')
6
6
  const getError = require('../common/error')
7
7
 
@@ -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 { UNAUTHORIZED } = require('../../auth/utils')
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: UNAUTHORIZED })
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
- Promise.resolve().then(async () => {
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}`, data: req.data, _binary: isBinary })
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
 
@@ -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
  }
@@ -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) {
@@ -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
- // REVISIT: This is authorization enforcement which is protocol-independent -> should move to service layer
68
- const requires = srv.definition?.['@requires']
69
- if (requires) {
70
- const ok = typeof requires === 'string' ? req.user.is(requires) : requires.some(r => req.user.is(r))
71
- if (ok) return next()
72
- } else {
73
- return next() // neither of the above
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sap/cds",
3
- "version": "7.1.2",
3
+ "version": "7.2.1",
4
4
  "description": "SAP Cloud Application Programming Model - CDS for Node.js",
5
5
  "homepage": "https://cap.cloud.sap/",
6
6
  "keywords": [
@@ -1,2 +0,0 @@
1
- // REVISIT: "kind": "xsuaa-auth" does not work because cds.requires logic does not resolve it. Intentional?
2
- module.exports = require('./jwt-auth')
@@ -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
File without changes