@sap/cds 7.1.1 → 7.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (84) hide show
  1. package/CHANGELOG.md +68 -4
  2. package/apis/cds.d.ts +10 -6
  3. package/apis/connect.d.ts +0 -1
  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 +17 -14
  9. package/apis/services.d.ts +40 -29
  10. package/apis/test.d.ts +1 -2
  11. package/bin/serve.js +4 -4
  12. package/lib/auth/basic-auth.js +1 -1
  13. package/lib/auth/dummy-auth.js +2 -1
  14. package/lib/auth/ias-auth.js +68 -2
  15. package/lib/auth/index.js +5 -5
  16. package/lib/auth/jwt-auth.js +40 -24
  17. package/lib/auth/mocked-users.js +0 -13
  18. package/lib/auth/passport-basic.js +2 -0
  19. package/lib/auth/passport-digest.js +2 -0
  20. package/lib/compile/etc/_localized.js +0 -1
  21. package/lib/compile/extend.js +16 -0
  22. package/lib/compile/for/lean_drafts.js +38 -6
  23. package/lib/compile/resolve.js +7 -5
  24. package/lib/compile/to/json.js +6 -2
  25. package/lib/dbs/cds-deploy.js +4 -4
  26. package/lib/env/cds-env.js +3 -3
  27. package/lib/env/cds-requires.js +1 -0
  28. package/lib/env/defaults.js +8 -1
  29. package/lib/env/schemas/cds-rc.json +27 -3
  30. package/lib/i18n/localize.js +3 -3
  31. package/lib/index.js +4 -0
  32. package/lib/log/cds-log.js +10 -1
  33. package/lib/ql/Whereable.js +7 -3
  34. package/lib/req/user.js +18 -16
  35. package/lib/srv/middlewares/sap-statistics.js +3 -3
  36. package/lib/srv/middlewares/trace.js +5 -4
  37. package/lib/srv/srv-dispatch.js +10 -9
  38. package/lib/utils/axios.js +3 -0
  39. package/lib/utils/cds-test.js +3 -0
  40. package/lib/utils/cds-utils.js +2 -0
  41. package/libx/_runtime/auth/index.js +8 -32
  42. package/libx/_runtime/auth/strategies/ias-auth.js +1 -77
  43. package/libx/_runtime/auth/strategies/mock.js +1 -12
  44. package/libx/_runtime/auth/strategies/xssecUtils.js +2 -2
  45. package/libx/_runtime/cds-services/adapter/odata-v4/ODataRequest.js +3 -1
  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/cds-services/services/utils/compareJson.js +0 -9
  51. package/libx/_runtime/cds-services/services/utils/differ.js +8 -10
  52. package/libx/_runtime/common/composition/data.js +10 -7
  53. package/libx/_runtime/common/composition/insert.js +9 -5
  54. package/libx/_runtime/common/composition/update.js +18 -12
  55. package/libx/_runtime/common/error/constants.js +6 -1
  56. package/libx/_runtime/common/generic/auth/requires.js +11 -3
  57. package/libx/_runtime/common/generic/auth/restrict.js +22 -16
  58. package/libx/_runtime/common/generic/auth/restrictions.js +5 -2
  59. package/libx/_runtime/common/generic/crud.js +6 -0
  60. package/libx/_runtime/common/generic/paging.js +2 -1
  61. package/libx/_runtime/common/utils/cqn2cqn4sql.js +3 -5
  62. package/libx/_runtime/common/utils/resolveView.js +3 -1
  63. package/libx/_runtime/common/utils/restrictions.js +47 -0
  64. package/libx/_runtime/db/data-conversion/post-processing.js +3 -3
  65. package/libx/_runtime/db/generic/input.js +1 -1
  66. package/libx/_runtime/db/sql-builder/ExpressionBuilder.js +0 -17
  67. package/libx/_runtime/db/utils/coloredTxCommands.js +5 -3
  68. package/libx/_runtime/fiori/lean-draft.js +24 -19
  69. package/libx/_runtime/hana/driver.js +2 -4
  70. package/libx/_runtime/hana/pool.js +53 -57
  71. package/libx/_runtime/messaging/enterprise-messaging-utils/registerEndpoints.js +2 -2
  72. package/libx/_runtime/messaging/outbox/utils.js +1 -2
  73. package/libx/_runtime/remote/utils/client.js +1 -1
  74. package/libx/_runtime/sqlite/Service.js +0 -4
  75. package/libx/_runtime/sqlite/customBuilder/CustomFunctionBuilder.js +2 -1
  76. package/libx/odata/afterburner.js +6 -4
  77. package/libx/odata/cqn2odata.js +7 -7
  78. package/libx/odata/utils.js +4 -1
  79. package/libx/rest/RestAdapter.js +15 -16
  80. package/package.json +1 -1
  81. package/lib/auth/xsuaa-auth.js +0 -2
  82. package/libx/_runtime/auth/utils.js +0 -32
  83. package/libx/audit-log/client.cds +0 -0
  84. package/libx/audit-log/client.js +0 -0
@@ -157,23 +157,6 @@ class ExpressionBuilder extends BaseBuilder {
157
157
  objects[i + 1].func = `not ${objects[i + 1].func}`
158
158
  return 1
159
159
  }
160
- if (objects[i].func || (objects[i + 2] && objects[i + 2].func)) {
161
- // sqlite requires leading 0 for numbers in datetime functions
162
- const f = objects[i].func ? i : OPERATORS.has(objects[i + 1]) ? i + 2 : i - 2
163
- const v = objects[i].val ? i : OPERATORS.has(objects[i + 1]) ? i + 2 : i - 2
164
- if (
165
- objects[f] &&
166
- cds.db &&
167
- ((SQLITE_DATETIME_FUNCTIONS.has(objects[f].func) && cds.db.kind === 'sqlite') ||
168
- (HANA_DATETIME_FUNCTIONS.has(objects[f].func) && cds.db.kind === 'hana'))
169
- ) {
170
- if (objects[v] && objects[v].val !== undefined && typeof objects[v].val === 'number') {
171
- objects[v] = { val: `${objects[v].val < 10 ? 0 : ''}${objects[v].val}` }
172
- if (objects[f].func === 'second') objects[v].val = _fillAfterDot(objects[v].val)
173
- }
174
- }
175
- return 0
176
- }
177
160
 
178
161
  if ((objects[i + 1] === '=' || objects[i + 1] in NOT_EQUAL) && objects[i + 2] && objects[i + 2].val === null) {
179
162
  this._addNullOrNotNull(objects[i], objects[i + 1])
@@ -1,5 +1,7 @@
1
+ const COLORS = !!process.stdout.isTTY && !!process.stderr.isTTY && !process.env.NO_COLOR
2
+
1
3
  module.exports = {
2
- BEGIN: '\x1b[1m\x1b[33mBEGIN\x1b[0m',
3
- COMMIT: '\x1b[1m\x1b[32mCOMMIT\x1b[0m',
4
- ROLLBACK: '\x1b[1m\x1b[91mROLLBACK\x1b[0m'
4
+ BEGIN: COLORS ? '\x1b[1m\x1b[33mBEGIN\x1b[0m' : 'BEGIN',
5
+ COMMIT: COLORS ? '\x1b[1m\x1b[32mCOMMIT\x1b[0m' : 'COMMIT',
6
+ ROLLBACK: COLORS ? '\x1b[1m\x1b[91mROLLBACK\x1b[0m' : 'ROLLBACK'
5
7
  }
@@ -87,6 +87,7 @@ const _redirectRefToActives = (ref, model) => {
87
87
  }
88
88
 
89
89
  const h = cds.ApplicationService.prototype.handle
90
+
90
91
  /* eslint-disable complexity */
91
92
  cds.ApplicationService.prototype.handle = async function (req) {
92
93
  const handle = h.bind(this)
@@ -286,9 +287,13 @@ cds.ApplicationService.prototype.handle = async function (req) {
286
287
  if (req.target.actions?.[req.event] && draftParams.IsActiveEntity === false) {
287
288
  if (query.SELECT?.from?.ref) query.SELECT.from.ref = _redirectRefToDrafts(query.SELECT.from.ref, this.model)
288
289
  const rootQuery = query.clone()
289
- rootQuery.SELECT.columns = [{ ref: ['DraftAdministrativeData'], expand: [{ ref: ['InProcessByUser'] }] }]
290
+ const columns = Object_keys(query._target.keys)
291
+ .filter(k => k !== 'IsActiveEntity')
292
+ .map(k => ({ ref: [k] }))
293
+ columns.push({ ref: ['DraftAdministrativeData'], expand: [{ ref: ['InProcessByUser'] }] })
294
+ rootQuery.SELECT.columns = columns
290
295
  rootQuery.SELECT.one = true
291
- const root = await rootQuery
296
+ const root = await cds.run(rootQuery)
292
297
  if (!root) req.reject(404)
293
298
  if (root.DraftAdministrativeData?.InProcessByUser !== cds.context.user.id) req.reject(403)
294
299
  const _req = _newReq(req, query, draftParams, { event: req.event })
@@ -334,8 +339,8 @@ cds.ApplicationService.prototype.handle = async function (req) {
334
339
  }
335
340
 
336
341
  req.query = query
337
- const result = await handle(req)
338
- return result
342
+
343
+ return handle(req)
339
344
  }
340
345
 
341
346
  // REVISIT: It's not optimal to first calculate the whole result array and only later
@@ -410,7 +415,7 @@ const Read = {
410
415
  draftsQuery.SELECT.orderBy = undefined
411
416
  draftsQuery.SELECT.columns = keys.map(k => ({ ref: [k] }))
412
417
 
413
- const drafts = await draftsQuery
418
+ const drafts = await draftsQuery.where({ HasActiveEntity: true })
414
419
  const res = await Read.onlyActives(run, query.where(Read.whereNotIn(query._target, drafts)), {
415
420
  ignoreDrafts: true
416
421
  })
@@ -450,6 +455,7 @@ const Read = {
450
455
  },
451
456
  all: async function (run, query) {
452
457
  LOG.debug('List Editing Status: All')
458
+ if (!query._drafts) return []
453
459
  query._drafts.SELECT.count = false
454
460
  query._drafts.SELECT.limit = undefined // We need all entries for the keys to properly select actives (count)
455
461
  const isCount = _isCount(query._drafts)
@@ -567,21 +573,17 @@ const Read = {
567
573
  if (!actives.length) return []
568
574
  const drafts = cds.ql.clone(query._drafts)
569
575
  drafts.SELECT.where = Read.whereIn(query._target, actives)
570
- if (drafts.SELECT.columns?.some(c => c === '*')) {
571
- drafts.SELECT.columns = drafts.SELECT.columns.filter(c => c !== '*')
572
- if (!drafts.SELECT.columns.some(c => c.ref?.[0] === 'DraftAdministrativeData_DraftUUID')) {
573
- drafts.SELECT.columns.push({ ref: ['DraftAdministrativeData_DraftUUID'] })
574
- }
575
- }
576
- const relevantColumns = ['DraftAdministrativeData', 'DraftAdministrativeData_DraftUUID']
577
- drafts.SELECT.columns = (
578
- drafts.SELECT.columns?.filter(c => c.ref && relevantColumns.includes(c.ref[0])) ||
579
- relevantColumns.map(k => ({ ref: [k] }))
580
- ).concat(
581
- Object_keys(query._target.keys)
582
- .filter(k => k !== 'IsActiveEntity')
583
- .map(k => ({ ref: [k] }))
576
+ const newColumns = Object_keys(query._target.keys)
577
+ .filter(k => k !== 'IsActiveEntity')
578
+ .map(k => ({ ref: [k] }))
579
+ if (
580
+ !drafts.SELECT.columns ||
581
+ drafts.SELECT.columns.some(c => c === '*' || c.ref?.[0] === 'DraftAdministrativeData_DraftUUID')
584
582
  )
583
+ newColumns.push({ ref: ['DraftAdministrativeData_DraftUUID'] })
584
+ const draftAdmin = drafts.SELECT.columns?.find(c => c.ref?.[0] === 'DraftAdministrativeData')
585
+ if (draftAdmin) newColumns.push(draftAdmin)
586
+ drafts.SELECT.columns = newColumns
585
587
  drafts.SELECT.count = undefined
586
588
  drafts.SELECT.search = undefined
587
589
  drafts.SELECT.one = undefined
@@ -662,6 +664,7 @@ function _cleansed(query, model) {
662
664
  draftsQuery._target = undefined
663
665
  const [root, ...tail] = draftsQuery.SELECT.from.ref
664
666
  const draft = model.definitions[root.id || root].drafts
667
+ if (!draft) return
665
668
  draftsQuery.SELECT.from = {
666
669
  ref: [root.id ? { ...root, id: draft.name } : draft.name, ...tail]
667
670
  }
@@ -1090,6 +1093,7 @@ async function onPrepare(req) {
1090
1093
  module.exports = {
1091
1094
  impl() {
1092
1095
  if (!this._datasource) this._datasource = cds.db
1096
+
1093
1097
  function _wrapped(handler, isActiveEntity) {
1094
1098
  const fn = function (req, next) {
1095
1099
  if (!req.target?.drafts || (isActiveEntity && req.target.isDraft) || (!isActiveEntity && !req.target.isDraft))
@@ -1098,6 +1102,7 @@ module.exports = {
1098
1102
  }
1099
1103
  return fn
1100
1104
  }
1105
+
1101
1106
  // Also runs those handlers if they're annotated with @odata.draft.enabled through extensibility
1102
1107
  this.on('NEW', '*', _wrapped(onNew, false))
1103
1108
  this.on('EDIT', '*', _wrapped(onEdit, true))
@@ -195,11 +195,9 @@ const _getHanaDriver = name => {
195
195
  LOG._debug && LOG.debug(`Failed to require "hdb" with error "${e.message}". Trying "@sap/hana-client" next.`)
196
196
  return _getHanaDriver('@sap/hana-client')
197
197
  } else if (isConfigured) {
198
- throw new Error(`"${name}" could not be 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
 
@@ -15,18 +15,20 @@ const _getMassagedCreds = function (creds) {
15
15
  return creds
16
16
  }
17
17
 
18
- // NOTE: disableCache: true means "force fetch credentials from service manager"
19
- async function credentials4(tenant, { disableCache = false }) {
20
- const { credentials } = cds.env.requires.db
18
+ // `disableCache: true` means to force fetch credentials from service manager
19
+ async function credentials4(tenant, db) {
20
+ const { disableCache = false, options } = db
21
+ const credentials = options?.credentials ?? cds.env.requires.db?.credentials
21
22
  if (!credentials) throw new Error('No database credentials provided')
23
+
22
24
  if (cds.requires.multitenancy) {
23
25
  // eslint-disable-next-line cds/no-missing-dependencies
24
26
  const res = await require('@sap/cds-mtxs/lib').xt.serviceManager.get(tenant, { disableCache })
25
27
  return _getMassagedCreds(res.credentials)
26
- } else {
27
- if (typeof credentials !== 'object' || !credentials.host) throw new Error('Malformed database credentials provided')
28
- return _getMassagedCreds(credentials)
29
28
  }
29
+
30
+ if (typeof credentials !== 'object' || !credentials.host) throw new Error('Malformed database credentials provided')
31
+ return _getMassagedCreds(credentials)
30
32
  }
31
33
 
32
34
  function factory4(creds, tenant) {
@@ -89,55 +91,48 @@ const pools = new Map()
89
91
 
90
92
  async function pool4(tenant, db) {
91
93
  if (!pools.get(tenant)) {
92
- pools.set(
93
- tenant,
94
- new Promise((resolve, reject) => {
95
- credentials4(tenant, db)
96
- .then(creds => {
97
- const config = _getPoolConfig()
98
- LOG._info && LOG.info('effective pool configuration:', config)
99
- const p = pool.createPool(factory4(creds, tenant), config)
100
-
101
- const INVALID_CREDENTIALS_WARNING = `Could not establish connection for tenant "${tenant}". Existing pool will be drained.`
102
- const INVALID_CREDENTIALS_ERROR = new Error(
103
- `Create is blocked for tenant "${tenant}" due to invalid credentials.`
104
- )
105
-
106
- /*
107
- * The error listener for "factoryCreateError" is registered to find out failed connection attempts.
108
- * If it fails due to invalid credentials, we delete the current pool from the pools map and overwrite the
109
- * pool factory create function.
110
- * Background is that generic-pool will continue to try to open a connection by calling the factory create
111
- * function until the "acquireTimeoutMillis" is reached.
112
- * This ends up in many connection attempts for one request even though the credentials are invalid.
113
- * Because of the deletion in the map, subsequent requests will fetch the credentials again.
114
- */
115
- p.on('factoryCreateError', async function (err) {
116
- if (err._connectError) {
117
- LOG._warn && LOG.warn(INVALID_CREDENTIALS_WARNING)
118
- pools.delete(tenant)
119
- if (p._factory && p._factory.create) {
120
- // reject after 100 ms to not block CPU completely
121
- p._factory.create = () =>
122
- new Promise((resolve, reject) => setTimeout(() => reject(INVALID_CREDENTIALS_ERROR), 100))
123
- }
124
- await p.drain()
125
- await p.clear()
126
- }
127
- })
94
+ const poolPromise = new Promise((resolve, reject) => {
95
+ credentials4(tenant, db)
96
+ .then(creds => {
97
+ const config = _getPoolConfig()
98
+ LOG._info && LOG.info('effective pool configuration:', config)
99
+ const p = pool.createPool(factory4(creds, tenant), config)
100
+ const INVALID_CREDENTIALS_WARNING = `Could not establish connection for tenant "${tenant}". Existing pool will be drained.`
101
+ const INVALID_CREDENTIALS_ERROR = new Error(
102
+ `Create is blocked for tenant "${tenant}" due to invalid credentials.`
103
+ )
128
104
 
129
- resolve(p)
130
- })
131
- .catch(e => {
132
- // delete pools entry if fetching credentials failed
133
- pools.delete(tenant)
134
- reject(e)
105
+ // The error listener for `factoryCreateError` is registered to detect failed connection attempts.
106
+ // If it fails due to invalid credentials, we delete the current pool from the pools map and overwrite the
107
+ // pool factory create function.
108
+ // The background is that the generic pool will keep trying to establish a connection by invoking the factory
109
+ // create function until the `acquireTimeoutMillis` is reached.
110
+ // This leads to numerous connection attempts for a single request, even when the credentials are invalid.
111
+ // Due to the deletion in the map, subsequent requests will retrieve the credentials again.
112
+ p.on('factoryCreateError', async function (err) {
113
+ if (err._connectError) {
114
+ LOG._warn && LOG.warn(INVALID_CREDENTIALS_WARNING)
115
+ pools.delete(tenant)
116
+ if (p._factory && p._factory.create) {
117
+ // reject after 100 ms to not block CPU completely
118
+ p._factory.create = () =>
119
+ new Promise((resolve, reject) => setTimeout(() => reject(INVALID_CREDENTIALS_ERROR), 100))
120
+ }
121
+ await p.drain()
122
+ await p.clear()
123
+ }
135
124
  })
136
- }).then(p => {
137
- pools.set(tenant, p)
138
- return p
139
- })
140
- )
125
+
126
+ resolve(p)
127
+ })
128
+ .catch(e => {
129
+ // delete pools entry if fetching credentials failed
130
+ pools.delete(tenant)
131
+ reject(e)
132
+ })
133
+ })
134
+
135
+ pools.set(tenant, poolPromise)
141
136
  }
142
137
 
143
138
  if ('then' in pools.get(tenant)) {
@@ -150,9 +145,10 @@ async function pool4(tenant, db) {
150
145
  async function resilientAcquire(pool, attempts = 1) {
151
146
  // max 3 attempts
152
147
  attempts = Math.min(attempts, 3)
153
- let client
154
- let err
155
- let attempt = 0
148
+ let client,
149
+ err,
150
+ attempt = 0
151
+
156
152
  while (!client && attempt < attempts) {
157
153
  try {
158
154
  client = await pool.acquire()
@@ -4,7 +4,7 @@ const express = require('express')
4
4
  const getTenantInfo = require('./getTenantInfo.js')
5
5
  const isSecured = () => cds.requires.auth && (cds.requires.auth.impl || cds.requires.auth.credentials)
6
6
  const _require = require('../../common/utils/require')
7
- const { 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) {
@@ -416,7 +416,7 @@ const _stringToReqOptions = (query, data, target) => {
416
416
  const cleanQuery = query.trim()
417
417
  const blankIndex = cleanQuery.substring(0, 8).indexOf(' ')
418
418
  const reqOptions = {
419
- method: cleanQuery.substring(0, blankIndex).toUpperCase(),
419
+ method: cleanQuery.substring(0, blankIndex).toUpperCase() || 'GET',
420
420
  url: encodeURI(formatPath(cleanQuery.substring(blankIndex, cleanQuery.length).trim()))
421
421
  }
422
422
 
@@ -98,10 +98,6 @@ module.exports = class SQLiteDatabase extends DatabaseService {
98
98
  }
99
99
  }
100
100
 
101
- getDbUrl(tenant) {
102
- return this.url4(tenant)
103
- }
104
-
105
101
  url4(tenant) {
106
102
  const credentials = this.options.credentials || this.options || {}
107
103
  let dbUrl = credentials.database || credentials.url || credentials.host || ':memory:'
@@ -87,7 +87,7 @@ class CustomFunctionBuilder extends FunctionBuilder {
87
87
  }
88
88
 
89
89
  _timeFunction(functionName, args) {
90
- this._outputObj.sql.push('strftime(')
90
+ this._outputObj.sql.push('CAST(strftime(')
91
91
  this._outputObj.sql.push(dateTimeFunctions.get(functionName), ',')
92
92
  if (typeof args === 'string') {
93
93
  this._outputObj.sql.push(args, ')')
@@ -95,6 +95,7 @@ class CustomFunctionBuilder extends FunctionBuilder {
95
95
  this._addFunctionArgs(args)
96
96
  this._outputObj.sql.push(')')
97
97
  }
98
+ this._outputObj.sql.push(' as decimal)')
98
99
  }
99
100
  }
100
101
 
@@ -28,8 +28,9 @@ const _addKeysDeep = (keys, keysCollector, ignoreManagedBacklinks) => {
28
28
  }
29
29
 
30
30
  function _keysOf(entity, ignoreManagedBacklinks) {
31
- if (!entity || !entity.keys) return
32
31
  const keysCollector = []
32
+ if (!entity || !entity.keys) return keysCollector
33
+
33
34
  _addKeysDeep(entity.keys, keysCollector, ignoreManagedBacklinks)
34
35
  return keysCollector
35
36
  }
@@ -174,15 +175,16 @@ function _convertVal(element, value) {
174
175
  throw new Error('Not a valid integer') // TODO
175
176
 
176
177
  case 'cds.String':
177
- case 'cds.LargeString':
178
+ case 'cds.LargeString':
179
+ return String(value)
180
+ case 'cds.Double':
181
+ return parseFloat(value)
178
182
  case 'cds.Decimal':
179
183
  case 'cds.DecimalFloat':
180
- case 'cds.Double':
181
184
  case 'cds.Int64':
182
185
  case 'cds.Integer64':
183
186
  if (typeof value === 'string') return value
184
187
  return String(value)
185
-
186
188
  case 'cds.Boolean':
187
189
  return typeof value === 'string' ? value === 'true' : value
188
190
 
@@ -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.1",
3
+ "version": "7.2.0",
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