@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
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  const cds = require('../index'), { local } = cds.utils
3
- const COLORS = !!process.stdout.isTTY && !!process.stderr.isTTY
3
+ const COLORS = !!process.stdout.isTTY && !!process.stderr.isTTY && !process.env.NO_COLOR
4
4
  const GREY = COLORS ? '\x1b[2m' : ''
5
5
  const RESET = COLORS ? '\x1b[0m' : ''
6
6
  let DEBUG // IMPORTANT: initialized later after await cds.plugins
@@ -80,7 +80,7 @@ exports.create = async function cds_deploy_create (db, csn=db.model, o) {
80
80
  let schevo = o.schema_evolution === 'auto' || o['with-auto-schema-evolution'] || o['model-only'] || o['delta-from'] || (o.kind === 'postgres' && o.schema_evolution !== false);
81
81
  if (schevo) {
82
82
  const { prior, table_exists } = await get_prior_model()
83
- const { afterImage, drops: d, createsAndAlters } = cds.compile.to.sql.delta(csn, o, prior && JSON.parse(prior))
83
+ const { afterImage, drops: d, createsAndAlters } = cds.compile.to.sql.delta(csn, o, prior);
84
84
  const after = JSON.stringify(afterImage)
85
85
  if (!o.dry && after != prior) {
86
86
  if (!table_exists) {
@@ -135,7 +135,7 @@ exports.create = async function cds_deploy_create (db, csn=db.model, o) {
135
135
  let file = o['delta-from']
136
136
  if (file) {
137
137
  let prior = await cds.utils.read(file)
138
- return { prior }
138
+ return { prior: typeof prior === 'string' ? JSON.parse(prior) : prior }
139
139
  }
140
140
  if (o.dry) return {}
141
141
 
@@ -153,7 +153,7 @@ exports.create = async function cds_deploy_create (db, csn=db.model, o) {
153
153
 
154
154
  if (table_exists) {
155
155
  let [{ csn }] = await db.run('SELECT csn from cds_model')
156
- return { prior: csn, table_exists }
156
+ return { prior: csn && JSON.parse(csn), table_exists }
157
157
  }
158
158
  return { table_exists } // no prior csn
159
159
  }
@@ -33,6 +33,7 @@ class Config {
33
33
  if (NODE_ENV) profiles.push (NODE_ENV)
34
34
  if (CDS_ENV) profiles.push (...CDS_ENV.split(/\s*,\s*/))
35
35
  if (_home) _add_static_profiles (_home, profiles);
36
+ if (_home && this['project-nature'] === 'java') profiles.push('java')
36
37
  if (!profiles.includes('production')) profiles.push('development')
37
38
  this._profiles = new Set (profiles)
38
39
  this._profiles._defined = new Set
@@ -173,8 +174,8 @@ class Config {
173
174
  * For BAS only: to find out whether this is a Java or Node.js project
174
175
  */
175
176
  get "project-nature" () {
176
- const has_pom_xml = [this.folders.srv,'.'] .some (
177
- f => isfile (path.join (this._home, f, 'pom.xml'))
177
+ const has_pom_xml = [this.folders?.srv,'.'] .some (
178
+ f => f && isfile (path.join (this._home, f, 'pom.xml'))
178
179
  )
179
180
  return has_pom_xml ? 'java' : 'nodejs'
180
181
  }
@@ -193,7 +194,6 @@ class Config {
193
194
  // The following are internal APIs which can always change!
194
195
  //
195
196
 
196
-
197
197
  _add_to_process_env (cwd, filename) {
198
198
  const file = path.resolve (cwd,filename)
199
199
  try {
@@ -46,6 +46,7 @@ const _authentication_strategies = {
46
46
  tenants: {}
47
47
  },
48
48
  "mocked-auth": {
49
+ _kind: 'mocked', // REVISIT: workaround to distinguish from 'basic-auth' (cf. restrict_all_services)
49
50
  kind: 'basic-auth',
50
51
  users: {
51
52
  alice: { tenant: 't1', roles: [ ...admin ] },
@@ -149,7 +149,10 @@ const defaults = module.exports = {
149
149
  },
150
150
 
151
151
  build: {
152
- target: 'gen'
152
+ target: 'gen',
153
+ '[java]': {
154
+ target: '.'
155
+ }
153
156
  },
154
157
 
155
158
  mtx: {
@@ -163,6 +166,10 @@ const defaults = module.exports = {
163
166
  },
164
167
 
165
168
  cdsc: {
169
+ moduleLookupDirectories: ['node_modules/'],
170
+ '[java]': {
171
+ moduleLookupDirectories: ['node_modules/', 'target/cds/'],
172
+ }
166
173
  // cv2: {
167
174
  // _localized_entries: true,
168
175
  // _texts_entries: true,
@@ -11,7 +11,7 @@
11
11
  "profile": {
12
12
  "description": "A single static profile",
13
13
  "anyOf": [
14
- { "enum": [ "mtx-sidecar", "with-mtx-sidecar" ] },
14
+ { "enum": [ "mtx-sidecar", "with-mtx-sidecar", "java" ] },
15
15
  { "type": "string" }
16
16
  ]
17
17
  },
@@ -257,8 +257,32 @@
257
257
  "description": "Enables new middlewares support (experimental)."
258
258
  },
259
259
  "multitenancy": {
260
- "type": "boolean",
261
- "description": "Shortcut to enable multitenancy."
260
+ "oneOf": [
261
+ {
262
+ "type": "boolean",
263
+ "description": "Shortcut to enable multitenancy."
264
+ },
265
+ {
266
+ "type": "object",
267
+ "description": "Multitenancy configuration options.",
268
+ "properties": {
269
+ "jobs": {
270
+ "type": "object",
271
+ "description": "Configuration options for the built-in async job executor.",
272
+ "properties": {
273
+ "workerSize": {
274
+ "type": "number",
275
+ "description": "Number of workers running in parallel per database."
276
+ },
277
+ "clusterSize": {
278
+ "type": "number",
279
+ "description": "Number of databases executing parallel tasks."
280
+ }
281
+ }
282
+ }
283
+ }
284
+ }
285
+ ]
262
286
  },
263
287
  "extensibility": {
264
288
  "oneOf": [
@@ -3,7 +3,7 @@ const {existsSync, readdirSync} = require ('fs')
3
3
  const {join,dirname,resolve,parse,sep} = require ('path')
4
4
 
5
5
  const DEBUG = cds.debug('i18n')
6
- const _node_modules = sep + 'node_modules'
6
+ const _node_modules = cds.env.cdsc.moduleLookupDirectories.map(d => sep+d.slice(0, -1))
7
7
 
8
8
  module.exports = Object.assign (localize, {
9
9
  bundles4, folders4, folder4, bundle4
@@ -182,8 +182,8 @@ function folder4 (loc) {
182
182
  }
183
183
  //> no --> search up the folder hierarchy up to cds.root, cds.home, or some .../node_modules/<package>
184
184
  let next = dirname(loc)
185
- if (next.includes(_node_modules)) {
186
- if (next.endsWith(_node_modules)) return folder4[loc] = null
185
+ if (_node_modules.some(m => next.includes(m))) {
186
+ if (_node_modules.some(m => next.endsWith(m))) return folder4[loc] = null
187
187
  } else {
188
188
  if (!(
189
189
  next.startsWith(cds.root) ||
package/lib/index.js CHANGED
@@ -139,3 +139,7 @@ global.cds = cds // REVISIT: using global.cds seems wrong
139
139
 
140
140
  // install jest util if jest is defined
141
141
  if (process.env.CDS_JEST_MEM_FIX && typeof jest !== 'undefined') require('./utils/jest.js')
142
+
143
+ // Allow for import cds from '@sap/cds' without esModuleInterop
144
+ Object.defineProperty(module.exports, "__esModule", { value: true });
145
+ module.exports.default = module.exports
@@ -1,5 +1,6 @@
1
1
  const cds = require ('../index'), conf = cds.env.log
2
2
  const log = module.exports = exports = cds_log
3
+ const path = require('path')
3
4
 
4
5
  /**
5
6
  * Cache used for all constructed loggers.
@@ -167,7 +168,15 @@ const { ERROR, WARN, INFO, DEBUG, TRACE } = exports.levels = {
167
168
 
168
169
  ;(function _init() {
169
170
  const conf = cds.env.log
170
- if (conf.Logger) exports.Logger = require (conf.Logger) // Use configured logger in case of cds serve
171
+ if (conf.Logger) {
172
+ let resolvedPath
173
+ try { resolvedPath = require.resolve(conf.Logger) } catch {
174
+ try { resolvedPath = require.resolve(path.join(cds.root, conf.Logger)) } catch {
175
+ throw new Error(`Cannot find logger at "${conf.Logger}"`)
176
+ }
177
+ }
178
+ exports.Logger = require (resolvedPath) // Use configured logger in case of cds serve
179
+ }
171
180
  if (conf.service) {
172
181
  const {app} = cds, serveIn = app => require('./service').serveIn(app)
173
182
  app ? setImmediate(() => serveIn(app)) : cds.on('bootstrap', app => serveIn(app))
@@ -78,12 +78,16 @@ const _object_predicate = ([arg], _clause) => { // e.g. .where ({ID:4711, stock:
78
78
  pred.push('or', ...predicate4([x],_clause))
79
79
  continue
80
80
  }
81
+ if (k === 'not') {
82
+ pred.push('not', {xpr:predicate4([x],_clause)})
83
+ continue
84
+ }
81
85
  if (k === 'exists') {
82
- pred.push(pred.length && 'and', 'exists', typeof x === 'object' ? x : { ref: x.split('.') })
86
+ pred.push('and', 'exists', typeof x === 'object' ? x : { ref: x.split('.') })
83
87
  continue
84
88
  }
85
89
  if (k === 'not exists') {
86
- pred.push(pred.length && 'and', 'not', 'exists', typeof x === 'object' ? x : { ref: x.split('.') })
90
+ pred.push('and', 'not', 'exists', typeof x === 'object' ? x : { ref: x.split('.') })
87
91
  continue
88
92
  }
89
93
  else pred.push('and', parse.expr(k))
@@ -97,7 +101,7 @@ const _object_predicate = ([arg], _clause) => { // e.g. .where ({ID:4711, stock:
97
101
  else if (_clause === 'on' && typeof x === 'string') pred.push('=', { ref: x.split('.') })
98
102
  else pred.push('=', {val:x})
99
103
  }
100
- return pred.slice(1)
104
+ return pred[0] === 'and' ? pred.slice(1) : pred
101
105
  }
102
106
 
103
107
  const _fluid_predicate = (args) => { // e.g. .where ('ID=',4711, 'and stock >=',1)
package/lib/req/user.js CHANGED
@@ -1,5 +1,3 @@
1
- const PSEUDO_ROLES = ['system-user', 'internal-user']
2
-
3
1
  class User {
4
2
 
5
3
  constructor (_) {
@@ -8,27 +6,31 @@ class User {
8
6
  if (new.target === Anonymous) return
9
7
  else return new User.default
10
8
  }
11
- if (typeof _ === 'string') { this.id = _; return }
12
- for (let each in _) super[each === '_roles' ? 'roles' : each] = _[each] // overrides getters
13
- const roles = this.hasOwnProperty('roles') && this.roles // eslint-disable-line no-prototype-builtins
14
- if (Array.isArray(roles)) this.roles = roles.filter(r => !PSEUDO_ROLES.includes(r)).reduce ((p,n)=>{p[n]=1; return p},{})
15
- else PSEUDO_ROLES.forEach(r => delete this.roles[r])
9
+ else if (typeof _ === 'string') this.id = _
10
+ else Object.assign(this,_)
16
11
  }
17
12
 
18
13
  get attr() { return super.attr = {} }
14
+ set attr(a) { super.attr = a }
15
+
19
16
  get roles(){ return super.roles = {} }
20
- get _roles(){ return this.roles } // compatibility
21
-
22
- is (role) {
23
- return role === 'any' ||
24
- role === 'identified-user' ||
25
- role === 'system-user' && this._is_system ||
26
- role === 'internal-user' && this._is_internal ||
27
- role === 'authenticated-user' ||
28
- !!this.roles[role]
17
+ set roles(r) {
18
+ super.roles = !Array.isArray(r) ? r : r.reduce((p,n)=>{ p[n]=1; return p },{})
19
+ }
20
+
21
+ is (role) {
22
+ return (
23
+ role === 'authenticated-user' ||
24
+ role === 'identified-user' ||
25
+ role === 'any' ||
26
+ !!this.roles[role]
27
+ )
29
28
  }
30
29
  valueOf() { return this.id }
31
30
 
31
+ // compatibility
32
+ get _roles(){ return this.roles }
33
+ set _roles(r){ this.roles = r }
32
34
  }
33
35
 
34
36
  /**
@@ -1,10 +1,10 @@
1
- const { performance:{now} } = require ('perf_hooks')
1
+ const { performance } = require ('perf_hooks')
2
2
 
3
3
  module.exports = (prec = 1000) => function sap_statistics (req, res, next) {
4
4
  if (req.query['sap-statistics'] || req.headers['sap-statistics']) {
5
- const { writeHead } = res, t0 = now()
5
+ const { writeHead } = res, t0 = performance.now()
6
6
  res.writeHead = function (...args) {
7
- const total = ((now() - t0) / prec).toFixed(2)
7
+ const total = ((performance.now() - t0) / prec).toFixed(2)
8
8
  if (res.statusCode < 400) res.setHeader('sap-statistics', `total=${total}`)
9
9
  writeHead.call(this, ...args)
10
10
  }
@@ -24,19 +24,20 @@ if (!LOG._debug) module.exports = ()=>[]; else {
24
24
  }
25
25
  }
26
26
 
27
- const { performance:{now} } = require ('perf_hooks')
27
+ const { performance } = require ('perf_hooks')
28
28
  const { format } = require ('util')
29
29
 
30
30
  class PerfTrace extends Array {
31
31
  log (...details) {
32
- const e = { details, start:now() }
32
+ const e = { details, start:performance.now() }
33
+ console.log(e)
33
34
  return this.push(e), e
34
35
  }
35
36
  done (e) {
36
- return e.stop = now()
37
+ return e.stop = performance.now()
37
38
  }
38
39
  toString ({truncate}) {
39
- const t0 = this[0].start; if (!this[0].stop) this[0].stop = now()
40
+ const t0 = this[0].start; if (!this[0].stop) this[0].stop = performance.now()
40
41
  return '\n'+ this.map (e => truncate (format (
41
42
  (e.start - t0).toFixed(2).padStart(6), '→',
42
43
  (e.stop - t0).toFixed(2).padEnd(6), '= ',
@@ -114,16 +114,17 @@ const _ensure_target = (srv,req) => {
114
114
  req.target = typeof q === 'object' ? cds.infer(q,defs) : defs[req.path]
115
115
  }
116
116
 
117
- const _ensure_fqn = (x,p,srv, name = x[p]) => {
118
- if (typeof name === 'string') {
117
+ const _ensure_fqn = (x,p,srv, from = x[p]) => {
118
+ if (!from) return
119
+ if (typeof from === 'string') {
119
120
  if (srv.isDatabaseService) return
120
- if (srv.model && name in srv.model.definitions) return
121
- if (name.startsWith(srv.namespace)) return
122
- if (name.endsWith('_drafts')) return // REVISIT: rather fix test/fiori/localized-draft.test.js ?
123
- else x[p] = `${srv.namespace}.${name}`
124
- } else if (name.ref) {
125
- const [head] = name.ref
126
- head.id ? _ensure_fqn(head,'id',srv) : _ensure_fqn(name.ref,0,srv)
121
+ if (srv.model && from in srv.model.definitions) return
122
+ if (from.startsWith(srv.namespace)) return
123
+ if (from.endsWith('_drafts')) return // REVISIT: rather fix test/fiori/localized-draft.test.js ?
124
+ else x[p] = `${srv.namespace}.${from}`
125
+ } else if (from.ref) {
126
+ const [head] = from.ref
127
+ head.id ? _ensure_fqn(head,'id',srv) : _ensure_fqn(from.ref,0,srv)
127
128
  if (x.where) for (let y of x.where) if (y.SELECT) _ensure_fqn(y.SELECT,'from',srv)
128
129
  }
129
130
  }
@@ -44,6 +44,9 @@ const _error = (e) => {
44
44
  })
45
45
  const { code, message } = e.response && e.response.data && e.response.data.error || {}
46
46
  if (message) e.message = code && code !== 'null' ? `${code} - ${message}` : message
47
+ // Promote toJSON from prototype to own property to make it iterable
48
+ // eslint-disable-next-line no-self-assign
49
+ if (typeof jest !== 'undefined') e.toJSON = e.toJSON
47
50
  throw e
48
51
  }
49
52
 
@@ -23,6 +23,9 @@ class Test extends require('./axios') {
23
23
 
24
24
  // launch cds server...
25
25
  before (`launching ${cmd} ${args.join(' ')}...`, async () => {
26
+ // cds.plugins is filling cds.env before cds serve -> profile must be parsed here
27
+ const profile = args.indexOf('--profile')
28
+ if (profile !== -1) process.env.CDS_ENV = [...process.env.CDS_ENV?.split(',') ?? [], ...args[profile+1].split(',')]
26
29
  await cds.plugins
27
30
  cds.once ('listening', ({server,url}) => {
28
31
  const axp = Reflect.getOwnPropertyDescriptor(this,'axios')
@@ -5,6 +5,8 @@ const ux = module.exports = exports = new class {
5
5
  get inflect() { return super.inflect = require('./inflect') }
6
6
  get inspect() { return super.inspect = require('util').inspect }
7
7
  get uuid() { return super.uuid = require('@sap/cds-foss').uuid }
8
+ get yaml() { return super.yaml = require('@sap/cds-foss').yaml }
9
+ get pool() { return super.pool = require('@sap/cds-foss').pool }
8
10
  get tar() { return super.tar = require('./tar') }
9
11
  }
10
12
 
@@ -2,7 +2,8 @@ const cds = require('../cds')
2
2
  const LOG = cds.log()
3
3
 
4
4
  const _require = require('../common/utils/require')
5
- const { UNAUTHORIZED, FORBIDDEN, isRestricted } = require('./utils')
5
+ const { containsAnyRestrictions } = require('../common/utils/restrictions')
6
+ const { ODATA_UNAUTHORIZED } = require('../common/error/constants')
6
7
 
7
8
  let passport, logged
8
9
 
@@ -60,7 +61,7 @@ const _log = (req, challenges) => {
60
61
  const cap_auth_callback = (req, res, next, internalError, user, arg) => {
61
62
  // An internal error occurs during the authentication process
62
63
  if (internalError) {
63
- return res.status(401).json({ error: UNAUTHORIZED }) // no details to client
64
+ return res.status(401).json({ error: ODATA_UNAUTHORIZED }) // no details to client
64
65
  }
65
66
 
66
67
  let challenges
@@ -106,10 +107,9 @@ const _mountMockAuth = (srv, app, strategy, config) => {
106
107
 
107
108
  const _mountPassportAuth = (srv, app, strategy, config) => {
108
109
  if (strategy in { jwt: 1, xsuaa: 1 } && !config.credentials) {
109
- LOG._warn &&
110
- LOG.warn(`Authentication kind "${config.kind}" configured, but no XSUAA instance bound to application.
111
- This is NOT recommended in production!`)
112
- return
110
+ let msg = `Authentication kind "${config.kind}" configured, but no XSUAA instance bound to application.`
111
+ msg += ' Either bind an XSUAA instance, or switch to an authentication kind that does not require a binding.'
112
+ throw new Error(msg)
113
113
  }
114
114
 
115
115
  if (!passport) passport = _require('passport')
@@ -140,11 +140,12 @@ module.exports = (srv, options = srv.options) => {
140
140
  // NOTE: options.auth is not an official API
141
141
  let config = 'auth' in options ? options.auth : cds.env.requires.auth
142
142
  if (!config) {
143
+ // REVISIT: can config be falsy? req.user would be undefined!
143
144
  if (cds.requires.db && cds.requires.multitenancy) {
144
145
  process.exitCode = 1 // REVISIT: why exitCode needed?
145
146
  throw new Error('Authentication required for multitenancy')
146
147
  }
147
- if (isRestricted(srv)) {
148
+ if (containsAnyRestrictions(srv)) {
148
149
  process.exitCode = 1 // REVISIT: why exitCode needed?
149
150
  throw new Error('Authentication required for authorization checks')
150
151
  }
@@ -161,12 +162,6 @@ module.exports = (srv, options = srv.options) => {
161
162
  // mount authentication middleware or strategy
162
163
  if (!logged) LOG._debug && LOG.debug(`Using authentication`, { kind: config.kind })
163
164
 
164
- // Security by default: set restrict_all_services if not disabled
165
- // this is done dynamically to also cover custom auth impl
166
- if (process.env.NODE_ENV === 'production' && config.restrict_all_services !== false) {
167
- config.restrict_all_services = true
168
- }
169
-
170
165
  if (config.impl) {
171
166
  // mount custom authentication middleware
172
167
  _mountCustomAuth(srv, app, config)
@@ -184,15 +179,6 @@ module.exports = (srv, options = srv.options) => {
184
179
  }
185
180
  }
186
181
 
187
- // Security by default: enforce authenticated users in production if auth service bound
188
- if (
189
- cds.requires.multitenancy ||
190
- (process.env.NODE_ENV === 'production' && config.credentials && config.restrict_all_services)
191
- ) {
192
- if (!logged) LOG._debug && LOG.debug(`Enforcing authenticated users for all services`)
193
- app.use(cap_enforce_login)
194
- }
195
-
196
182
  // so we don't log the same stuff multiple times
197
183
  logged = true
198
184
 
@@ -206,13 +192,3 @@ const _strategy4 = config => {
206
192
  process.exitCode = 1 // REVISIT: why exitCode needed?
207
193
  throw new Error(`Authentication kind "${config.kind}" is not supported`)
208
194
  }
209
-
210
- const cap_enforce_login = (req, res, next) => {
211
- if (req.user && req.user.is('authenticated-user')) return next() // pass if user is authenticated
212
- if (!req.user || req.user._is_anonymous) {
213
- if (req.user && req.user._challenges) res.set('WWW-Authenticate', req.user._challenges.join(';'))
214
- return res.status(401).json({ error: UNAUTHORIZED }) // no details to client
215
- } else {
216
- return res.status(403).json({ error: FORBIDDEN }) // no details to client
217
- }
218
- }
@@ -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
  }
@@ -90,6 +90,7 @@ class ODataRequest extends cds.Request {
90
90
  // REVISIT needed in case of $batch, replace after removing okra
91
91
  const method = odataReq.getIncomingRequest().method
92
92
  const { user } = req
93
+ const tenant = req.tenant || user?.tenant
93
94
  const info = metaInfo(query, type, service, data, req, upsert)
94
95
  const { event, unbound } = info
95
96
  if (event === 'READ') {
@@ -98,7 +99,8 @@ class ODataRequest extends cds.Request {
98
99
  if (!isStreaming(segments)) handleStreamProperties(target, query, service.model, true)
99
100
  }
100
101
  const _queryOptions = odataReq.getQueryOptions()
101
- super({ event, target, data, query: unbound ? {} : query, user, method, headers, req, res, _queryOptions })
102
+ // prettier-ignore
103
+ super({ event, target, data, query: unbound ? {} : query, user, tenant, method, headers, req, res, _queryOptions })
102
104
  this._metaInfo = info.metadata
103
105
  } else {
104
106
  /*
@@ -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
  /*