@sap/cds 8.4.2 → 8.5.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 (73) hide show
  1. package/CHANGELOG.md +42 -1
  2. package/_i18n/messages.properties +99 -0
  3. package/bin/serve.js +2 -2
  4. package/lib/compile/cdsc.js +9 -4
  5. package/lib/compile/to/srvinfo.js +4 -4
  6. package/lib/core/entities.js +1 -0
  7. package/lib/core/types.js +1 -1
  8. package/lib/dbs/cds-deploy.js +5 -2
  9. package/lib/env/defaults.js +7 -6
  10. package/lib/env/schemas/cds-rc.js +132 -22
  11. package/lib/i18n/bundles.js +111 -0
  12. package/lib/i18n/files.js +134 -0
  13. package/lib/i18n/index.js +63 -0
  14. package/lib/i18n/localize.js +101 -237
  15. package/lib/i18n/resources.js +150 -0
  16. package/lib/index.js +1 -0
  17. package/lib/log/format/aspects/cls.js +6 -1
  18. package/lib/log/format/json.js +1 -1
  19. package/lib/ql/CREATE.js +1 -0
  20. package/lib/ql/DELETE.js +1 -0
  21. package/lib/ql/DROP.js +1 -0
  22. package/lib/ql/INSERT.js +9 -8
  23. package/lib/ql/Query.js +18 -8
  24. package/lib/ql/SELECT.js +1 -0
  25. package/lib/ql/UPDATE.js +2 -1
  26. package/lib/ql/UPSERT.js +1 -1
  27. package/lib/ql/Whereable.js +3 -3
  28. package/lib/ql/cds-ql.js +12 -18
  29. package/lib/req/user.js +1 -0
  30. package/lib/req/validate.js +12 -3
  31. package/lib/srv/factory.js +2 -2
  32. package/lib/{auth → srv/middlewares/auth}/basic-auth.js +1 -1
  33. package/lib/{auth → srv/middlewares/auth}/dummy-auth.js +1 -1
  34. package/lib/srv/middlewares/auth/ias-auth.js +96 -0
  35. package/lib/{auth → srv/middlewares/auth}/index.js +2 -2
  36. package/lib/srv/middlewares/auth/jwt-auth.js +62 -0
  37. package/lib/{auth → srv/middlewares/auth}/mocked-users.js +1 -1
  38. package/lib/srv/middlewares/auth/xssec.js +7 -0
  39. package/lib/srv/middlewares/index.js +1 -1
  40. package/lib/utils/cds-utils.js +15 -19
  41. package/lib/utils/tar.js +2 -2
  42. package/libx/_runtime/cds-services/adapter/odata-v4/OData.js +2 -2
  43. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/read.js +1 -0
  44. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/update.js +1 -1
  45. package/libx/_runtime/common/error/frontend.js +2 -6
  46. package/libx/_runtime/common/error/log.js +7 -8
  47. package/libx/_runtime/common/error/utils.js +3 -7
  48. package/libx/_runtime/common/generic/auth/capabilities.js +1 -1
  49. package/libx/_runtime/common/generic/input.js +41 -6
  50. package/libx/_runtime/common/i18n/index.js +8 -15
  51. package/libx/_runtime/common/utils/compareJson.js +10 -1
  52. package/libx/_runtime/common/utils/resolveView.js +1 -1
  53. package/libx/_runtime/fiori/lean-draft.js +77 -26
  54. package/libx/_runtime/messaging/enterprise-messaging-utils/registerEndpoints.js +5 -1
  55. package/libx/_runtime/messaging/kafka.js +1 -1
  56. package/libx/odata/ODataAdapter.js +2 -1
  57. package/libx/odata/index.js +3 -0
  58. package/libx/odata/middleware/batch.js +4 -0
  59. package/libx/odata/middleware/create.js +8 -6
  60. package/libx/odata/middleware/update.js +24 -21
  61. package/libx/odata/parse/afterburner.js +16 -3
  62. package/libx/odata/parse/grammar.peggy +24 -7
  63. package/libx/odata/parse/parser.js +1 -1
  64. package/libx/odata/utils/index.js +1 -0
  65. package/libx/odata/utils/postProcess.js +4 -1
  66. package/libx/rest/RestAdapter.js +2 -1
  67. package/libx/rest/middleware/error.js +0 -50
  68. package/package.json +1 -1
  69. package/lib/auth/ias-auth.js +0 -68
  70. package/lib/auth/ias-claims.js +0 -34
  71. package/lib/auth/jwt-auth.js +0 -70
  72. package/libx/_runtime/common/i18n/messages.properties +0 -99
  73. package/libx/_runtime/common/utils/require.js +0 -9
package/lib/ql/Query.js CHANGED
@@ -3,7 +3,14 @@ const cds = require('../index')
3
3
 
4
4
  class Query {
5
5
 
6
- constructor(_={}) { this[this.cmd] = _ }
6
+ /**
7
+ * The kind of query, as in CQN's SELECT, INSERT, UPDATE, DELETE, CREATE, DROP.
8
+ * Overridden in subclasses to return the respective kind.
9
+ */
10
+ get kind() { return this.constructor.name }
11
+ get _(){ return this[this.kind] }
12
+
13
+ constructor(_={}) { this[this.kind] = _ }
7
14
 
8
15
  /**
9
16
  * Note to self: We can't use .as (alias) as that would conflict with
@@ -16,12 +23,12 @@ class Query {
16
23
 
17
24
  /** Creates a derived instance that initially inherits all properties. */
18
25
  clone (_) {
19
- const cmd = this.cmd || Object.keys(this)[0]
20
- return {__proto__:this, [cmd]: {__proto__:this[cmd],..._} }
26
+ const kind = this.kind || Object.keys(this)[0]
27
+ return {__proto__:this, [kind]: {__proto__:this[kind],..._} }
21
28
  }
22
29
 
23
30
  flat (q=this) {
24
- let x = q.cmd || Object.keys(q)[0], y = q[x]
31
+ let x = q.kind || Object.keys(q)[0], y = q[x]
25
32
  let protos = [y]; for (let o=y; o.__proto__;) protos.push (o = o.__proto__)
26
33
  q[x] = Object.assign ({}, ...protos.reverse())
27
34
  if (y.columns) for (let c of y.columns) if (c.SELECT) (this||Query.prototype).flat(c)
@@ -36,7 +43,7 @@ class Query {
36
43
  /** Turns all queries into Thenables which execute with primary db by default */
37
44
  get then() {
38
45
  const srv = this._srv || cds.db || cds.error `Can't execute query as no primary database is connected.`
39
- const q = new AsyncResource('await cds.query')
46
+ const q = new AsyncResource('await cds.query')
40
47
  return (r,e) => q.runInAsyncScope (srv.run, srv, this).then (r,e)
41
48
  }
42
49
 
@@ -75,7 +82,7 @@ class Query {
75
82
  }
76
83
 
77
84
  _add (property, values) {
78
- const _ = this[this.cmd], pd = Reflect.getOwnPropertyDescriptor (_,property)
85
+ const {_} = this, pd = Reflect.getOwnPropertyDescriptor (_,property)
79
86
  _[property] = !pd || !pd.value ? values : [ ...pd.value, ...values ]
80
87
  return this
81
88
  }
@@ -85,8 +92,8 @@ class Query {
85
92
  return value
86
93
  }
87
94
 
88
- valueOf (cmd=this.cmd) {
89
- return `${cmd} ${_name(this._target.name)} `
95
+ valueOf (prelude = this.kind) {
96
+ return `${prelude} ${_name(this._target.name)} `
90
97
  }
91
98
 
92
99
  get _target_ref() {
@@ -115,6 +122,9 @@ class Query {
115
122
  return cds.infer (this, m?.definitions)
116
123
  }
117
124
  set target(t) { this._set('target',t) }
125
+
126
+ /** @deprecated use .kind instead */
127
+ get cmd() { return this.kind }
118
128
  }
119
129
 
120
130
  const _name = cds.env.sql.names === 'quoted' ? n =>`"${n}"` : n => n.replace(/[.:]/g,'_')
package/lib/ql/SELECT.js CHANGED
@@ -3,6 +3,7 @@ const cds = require('../index')
3
3
 
4
4
  module.exports = class Query extends Whereable {
5
5
 
6
+ get kind() { return 'SELECT' }
6
7
  static _api() {
7
8
  const $ = Object.assign
8
9
  return $((..._) => new this()._select_or_from(..._), {
package/lib/ql/UPDATE.js CHANGED
@@ -3,6 +3,7 @@ const { parse } = require('../index')
3
3
 
4
4
  module.exports = class Query extends Whereable {
5
5
 
6
+ get kind() { return 'UPDATE' }
6
7
  static _api() {
7
8
  return Object.assign ((..._) => (new this).entity(..._), {
8
9
  entity: (..._) => (new this).entity(..._),
@@ -96,4 +97,4 @@ const _comma_separated_exprs = (s) => {
96
97
  return all
97
98
  }
98
99
 
99
- const operators = { '=':1, '-=':2, '+=':2, '*=':2, '/=':2, '%=':2 }
100
+ const operators = { '=':1, '-=':2, '+=':2, '*=':2, '/=':2, '%=':2 }
package/lib/ql/UPSERT.js CHANGED
@@ -1,3 +1,3 @@
1
1
  module.exports = class Query extends require('./INSERT') {
2
- get _target_ref(){ return this.UPSERT.into }
2
+ get kind() { return 'UPSERT' }
3
3
  }
@@ -11,12 +11,12 @@ class Query extends require('./Query') {
11
11
  if (!args[0]) return this
12
12
  let pred = predicate4(args, _where)
13
13
  if (pred && pred.length > 0) {
14
- let _ = this[this.cmd]
14
+ let {_} = this
15
15
  const clause = _where ?? (
16
16
  _.having ? 'having' :
17
17
  _.where ? 'where' :
18
18
  _.from?.on ? 'on' :
19
- error (`Invalid attempt to call '${this.cmd}.${and_or}()' before a prior call to '${this.cmd}.where()'`)
19
+ error (`Invalid attempt to call '${this.kind}.${and_or}()' before a prior call to '${this.kind}.where()'`)
20
20
  )
21
21
  if (clause === 'on') _ = _.from
22
22
  let left = _[clause]
@@ -102,7 +102,7 @@ const _object_predicate = ([arg], _clause) => { // e.g. .where ({ID:4711, stock:
102
102
  else if (x instanceof Buffer) pred.push('=', {val:x})
103
103
  else if (x instanceof RegExp) pred.push('like', {val:x})
104
104
  else if (x instanceof Date) pred.push('=', {val:x})
105
- else if (typeof x === 'object') for (let op in x) x[op]?.in ? pred.push(op, ...predicate4([x[op]],_clause)) : pred.push(op, val(x[op])) // REVIST: Should always be proper recursion
105
+ else if (typeof x === 'object') for (let op in x) x[op]?.in ? pred.push(op, ...predicate4([x[op]],_clause)) : pred.push(op, val(x[op])) // REVIST: Should always be proper recursion
106
106
  else if (_clause === 'on' && typeof x === 'string') pred.push('=', { ref: x.split('.') })
107
107
  else pred.push('=', {val:x})
108
108
  }
package/lib/ql/cds-ql.js CHANGED
@@ -1,16 +1,13 @@
1
1
  const Query = require('./Query')
2
2
  require = path => { // eslint-disable-line no-global-assign
3
3
  const clazz = module.require (path); if (!clazz._api) return clazz
4
- const kind = path.match(/\w+$/)[0]
5
- Object.defineProperties (clazz.prototype, {
6
- kind: { value: kind },
7
- cmd: { value: kind }
8
- })
9
- const api = clazz._api()
10
- return Object.assign (function (...args) {
11
- if (new.target) return new clazz (...args) // allows: new SELECT
12
- return api (...args) // allows: SELECT(...).from()
13
- }, { class: clazz }, api)
4
+ const factory = clazz._api()
5
+ const constructor = Object.assign (function (...args) {
6
+ if (new.target) return new clazz (...args) // allows: new SELECT
7
+ return factory (...args) // allows: SELECT(...).from()
8
+ }, factory) // allows: SELECT.from()
9
+ constructor.class = clazz
10
+ return constructor
14
11
  }
15
12
 
16
13
  module.exports = exports = {
@@ -25,22 +22,19 @@ module.exports = exports = {
25
22
  }
26
23
 
27
24
  exports.clone = function (q,_) {
28
- // q = this.query(q)
29
25
  return Query.prototype.clone.call(q,_)
30
26
  }
31
27
 
32
28
  exports.query = function (q) {
33
- if (!q || q instanceof Query) return q
34
- let kind = Object.keys(q)[0]; if (!kind) return
35
- let clazz = exports[kind]; if (!clazz) return q
36
- return new clazz (q[kind])
37
- },
29
+ if (q instanceof Query) return q
30
+ for (let k in q) return k in this ? new this[k](q[k]) : q
31
+ }
38
32
 
39
33
  exports._reset = ()=>{ // for strange tests only
40
34
  const cds = require('../index')
41
35
  const _name = cds.env.sql.names === 'quoted' ? n =>`"${n}"` : n => n.replace(/[.:]/g,'_')
42
- Object.defineProperty (Query.prototype,'valueOf',{ configurable:1, value: function(cmd=this.cmd) {
43
- return `${cmd} ${_name(this._target.name)} `
36
+ Object.defineProperty (Query.prototype,'valueOf',{ configurable:1, value: function(kind=this.kind) {
37
+ return `${kind} ${_name(this._target.name)} `
44
38
  }})
45
39
  return this
46
40
  }
package/lib/req/user.js CHANGED
@@ -74,3 +74,4 @@ Object.defineProperty (exports, 'default', {
74
74
  get() { return this._default },
75
75
  })
76
76
  exports._default = exports.anonymous
77
+ exports.class = User
@@ -30,7 +30,7 @@ class Validation {
30
30
  typeof n === 'number' ? p + `[${n}]` : //> some/array[1]...
31
31
  p && n ? p+'/'+n : n //> some/element...
32
32
  ),'')
33
- if (val) err.args = [ val, ...args ]
33
+ if (val !== undefined) err.args = [ val, ...args ]
34
34
  return err
35
35
  }
36
36
 
@@ -111,7 +111,14 @@ const $any = class any {
111
111
  }
112
112
  if (this['@assert.range'] && !this.enum) {
113
113
  const [ min, max ] = this['@assert.range']
114
- asserts.push ((v,p,ctx) => v == null || min <= v && v <= max || ctx.error ('ASSERT_RANGE', p, this.name, v, min, max))
114
+ if (min['='] === '_') min.val = -Infinity
115
+ if (max['='] === '_') max.val = +Infinity
116
+ asserts.push (
117
+ min.val !== undefined && max.val !== undefined ? (v,p,ctx) => v == null || min.val < v && v < max.val || ctx.error ('ASSERT_RANGE', p, this.name, v, '>'+min.val, '<'+max.val) :
118
+ min.val !== undefined ? (v,p,ctx) => v == null || min.val < v && v <= max || ctx.error ('ASSERT_RANGE', p, this.name, v, '>'+min.val, max) :
119
+ max.val !== undefined ? (v,p,ctx) => v == null || min <= v && v < max.val || ctx.error ('ASSERT_RANGE', p, this.name, v, min, '<'+max.val) :
120
+ (v,p,ctx) => v == null || min <= v && v <= max || ctx.error ('ASSERT_RANGE', p, this.name, v, min, max)
121
+ )
115
122
  }
116
123
  if (this['@assert.enum'] || this['@assert.range'] && this.enum) {
117
124
  const vals = Object.entries(this.enum).map(([k,v]) => 'val' in v ? v.val : k)
@@ -198,7 +205,9 @@ class struct extends $any {
198
205
  for (let each in data) { // will work for structured payloads as well as flattened ones with universal CSN
199
206
  let /** @type {$any} */ d = elements[each]
200
207
  if (!d) ctx.unknown (each, this, data)
201
- else if (ctx.cleanse && d._is_readonly()) delete data[each]
208
+ else if (ctx.cleanse && d._is_readonly() && !d.key) delete data[each]
209
+ // @Core.Immutable processed only for root, children are handled when knowing db state
210
+ else if (ctx.cleanse && d['@Core.Immutable'] && !ctx.insert && !path) delete data[each]
202
211
  else if (d['@cds.validate'] !== false) d.validate (data[each], path_, ctx)
203
212
  }
204
213
  }
@@ -1,4 +1,4 @@
1
- const cds = require('..'), { path, isfile, _redacted } = cds.utils
1
+ const cds = require('..'), { path, isfile, redacted } = cds.utils
2
2
  const paths = Array.from (new Set ([ cds.root, ...require.resolve.paths('x') ]))
3
3
  const DEBUG = cds.debug('cds.service.factory',); DEBUG && DEBUG ({ 'cds.root':cds.root, paths })
4
4
 
@@ -10,7 +10,7 @@ const ServiceFactory = function (name, model, options) { //NOSONAR
10
10
  const serve = !(conf && conf.external && (!o.mocked || conf.credentials))
11
11
  const defs = !model ? {[name]:{}} : model.definitions || cds.error `Invalid argument for 'model': ${model}`
12
12
  const def = !name || name === 'db' ? {} : defs[name] || {}
13
- DEBUG?. ({ name, definition:def, options:_redacted(o) })
13
+ DEBUG?. ({ name, definition:def, options:redacted(o) })
14
14
 
15
15
  let it /* eslint-disable no-cond-assign */
16
16
  if (it = o.with) return _use (it) // from cds.serve (<options>)
@@ -1,6 +1,6 @@
1
1
  module.exports = function basic_auth (options) {
2
2
 
3
- const cds = require ('../index'), DEBUG = cds.debug('basic|auth')
3
+ const cds = require ('../../../index'), DEBUG = cds.debug('basic|auth')
4
4
  const users = require ('./mocked-users') (options)
5
5
  const login_required = options.login_required || cds.requires.multitenancy || process.env.NODE_ENV === 'production' && options.credentials
6
6
 
@@ -1,4 +1,4 @@
1
- const cds = require ('../index'), {privileged} = cds.User
1
+ const cds = require ('../../../index'), {privileged} = cds.User
2
2
 
3
3
  module.exports = function dummy_auth() {
4
4
  return function dummy_auth (req, res, next) {
@@ -0,0 +1,96 @@
1
+ const cds = require('../../../index.js')
2
+ const LOG = cds.log('auth')
3
+
4
+ const xssec = require('./xssec')
5
+
6
+ // REVISIT: Why do we need to know and do that?
7
+ const KNOWN_CLAIMS = Object.values({
8
+ /*
9
+ * JWT claims (https://datatracker.ietf.org/doc/html/rfc7519#section-4)
10
+ */
11
+ ISSUER: 'iss',
12
+ SUBJECT: 'sub',
13
+ AUDIENCE: 'aud',
14
+ EXPIRATION_TIME: 'exp',
15
+ NOT_BEFORE: 'nbf',
16
+ ISSUED_AT: 'iat',
17
+ JWT_ID: 'jti',
18
+ /*
19
+ * TokenClaims (com.sap.cloud.security.token.TokenClaims)
20
+ */
21
+ // ISSUER: "iss", //> already in JWT claims
22
+ IAS_ISSUER: 'ias_iss',
23
+ // EXPIRATION: "exp", //> already in JWT claims
24
+ // AUDIENCE: "aud", //> already in JWT claims
25
+ // NOT_BEFORE: "nbf", //> already in JWT claims
26
+ // SUBJECT: "sub", //> already in JWT claims
27
+ // USER_NAME: 'user_name', //> do not exclude
28
+ // GIVEN_NAME: 'given_name', //> do not exclude
29
+ // FAMILY_NAME: 'family_name', //> do not exclude
30
+ // EMAIL: 'email', //> do not exclude
31
+ SAP_GLOBAL_SCIM_ID: 'scim_id',
32
+ SAP_GLOBAL_USER_ID: 'user_uuid', //> exclude for now
33
+ SAP_GLOBAL_ZONE_ID: 'zone_uuid',
34
+ // GROUPS: 'groups', //> do not exclude
35
+ AUTHORIZATION_PARTY: 'azp',
36
+ CNF: 'cnf',
37
+ CNF_X5T: 'x5t#S256',
38
+ // own
39
+ APP_TENANT_ID: 'app_tid'
40
+ })
41
+
42
+
43
+ module.exports = function ias_auth(config) {
44
+ // cds.env.requires.auth.known_claims is not an official config!
45
+ const { kind, credentials, known_claims = KNOWN_CLAIMS } = config
46
+ const skipped_attrs = known_claims.reduce((a,x) => { a[x] = 1; return a }, {})
47
+
48
+ if (!credentials) throw new Error(
49
+ `Authentication kind "${kind}" configured, but no IAS instance bound to application. ` +
50
+ 'Either bind an IAS instance, or switch to an authentication kind that does not require a binding.'
51
+ )
52
+
53
+ function getUser(tokenInfo) {
54
+ const payload = tokenInfo.getPayload()
55
+
56
+ const clientid = tokenInfo.getClientId()
57
+ if (clientid === payload.sub) { //> grant_type === client_credentials or x509
58
+ const roles = { 'system-user': 1 }
59
+ if (clientid === credentials.clientid) roles['internal-user'] = 1
60
+ return new cds.User({ id: 'system', roles, tokenInfo })
61
+ }
62
+
63
+ // add all unknown attributes to req.user.attr in order to keep public API small
64
+ const attr = {}
65
+ for (const key in payload) {
66
+ if (key in skipped_attrs) continue // REVISIT: Why do we need to do that?
67
+ else attr[key] = payload[key]
68
+ }
69
+
70
+ // REVISIT: just don't such things, please! -> We're just piling up tech dept through tons of unoficcial long tail APIs like that!
71
+ // REVISIT: looks like wrong direction to me, btw
72
+ // same api as xsuaa-auth for easier migration
73
+ if (attr.user_name) attr.logonName = attr.user_name
74
+ if (attr.given_name) attr.givenName = attr.given_name
75
+ if (attr.family_name) attr.familyName = attr.family_name
76
+
77
+ return new cds.User({ id: payload.sub, attr, tokenInfo })
78
+ }
79
+
80
+ // NOTE: Use named function for better stack traces... for the actual middleware, of course, not that much for the factory!
81
+ return function ias_auth (req, _, next) {
82
+ if (!req.headers.authorization) return next()
83
+ const token = req.headers.authorization.slice(7) // skip /^bearer /
84
+ xssec.createSecurityContext(token, credentials, 'IAS', function (err, securityContext, tokenInfo) {
85
+
86
+ if (err) LOG.warn('User could not be authenticated due to error:', err)
87
+ if (!securityContext) return next(401)
88
+ else req.authInfo = securityContext //> compat req.authInfo
89
+
90
+ const ctx = cds.context
91
+ ctx.user = getUser(tokenInfo)
92
+ ctx.tenant = tokenInfo.getZoneId()
93
+ next()
94
+ })
95
+ }
96
+ }
@@ -1,4 +1,4 @@
1
- const cds = require ('../index')
1
+ const cds = require ('../../../index.js')
2
2
 
3
3
  const _builtin = {
4
4
  mocked: 'basic-auth',
@@ -30,7 +30,7 @@ module.exports = function auth_factory (o) {
30
30
 
31
31
  // try resolving the impl, throw if not found
32
32
  const config = { kind, impl: cds.utils.local(impl) }
33
- // use cds.resolve() to allow './srv/auth.js' and 'srv/auth.js'
33
+ // use cds.resolve() to allow './srv/auth.js' and 'srv/auth.js' -> REVISIT: cds.resolve() is not needed here, and not meant for that !
34
34
  try { impl = require.resolve (cds.resolve (impl)?.[0], {paths:[cds.root]}) } catch {
35
35
  throw cds.error `Didn't find auth implementation for ${config}`
36
36
  }
@@ -0,0 +1,62 @@
1
+ const cds = require('../../../index.js')
2
+ const LOG = cds.log('auth')
3
+
4
+ const xssec = require('./xssec')
5
+
6
+ module.exports = function jwt_auth(config) {
7
+ const { kind, credentials } = config
8
+
9
+ if (!credentials) throw new Error(
10
+ `Authentication kind "${kind}" configured, but no XSUAA instance bound to application. ` +
11
+ 'Either bind an XSUAA instance, or switch to an authentication kind that does not require a binding.'
12
+ )
13
+
14
+ const xsappname = credentials.xsappname.length + 1
15
+
16
+ function getUser(tokenInfo) {
17
+ const payload = tokenInfo.getPayload()
18
+
19
+ let id = payload.user_name
20
+
21
+ const roles = {}
22
+ for (let scope of payload.scope) {
23
+ let role = scope.slice(xsappname) // Roles = scope names w/o xsappname...
24
+ if (role in { 'internal-user': 1, 'system-user': 1 }) continue // Disallow setting system roles from external
25
+ else roles[role] = 1
26
+ }
27
+
28
+ // Add system roles in case of client credentials flow
29
+ if (payload.grant_type in { client_credentials: 1, client_x509: 1 }) {
30
+ id = 'system'
31
+ roles['system-user'] = 1
32
+ if (tokenInfo.getClientId() === credentials.clientid) roles['internal-user'] = 1
33
+ }
34
+
35
+ const attr = { ... payload['xs.user.attributes'] }
36
+ if (kind === 'xsuaa') {
37
+ attr.logonName = payload.user_name
38
+ attr.givenName = payload.given_name
39
+ attr.familyName = payload.family_name
40
+ attr.email = payload.email
41
+ }
42
+
43
+ return new cds.User({ id, roles, attr, tokenInfo })
44
+ }
45
+
46
+ // NOTE: Use named function for better stack traces... for the actual middleware, of course, not that much for the factory!
47
+ return function jwt_auth (req, _, next) {
48
+ if (!req.headers.authorization) return next()
49
+ const token = req.headers.authorization.slice(7) // skip /^bearer /
50
+ xssec.createSecurityContext(token, credentials, function (err, securityContext, tokenInfo) {
51
+
52
+ if (err) LOG.warn('User could not be authenticated due to error:', err)
53
+ if (!securityContext) return next(401)
54
+ else req.authInfo = securityContext //> compat req.authInfo
55
+
56
+ const ctx = cds.context
57
+ ctx.user = getUser(tokenInfo)
58
+ ctx.tenant = tokenInfo.getZoneId()
59
+ next()
60
+ })
61
+ }
62
+ }
@@ -1,4 +1,4 @@
1
- const cds = require ('../index'), { User } = cds
1
+ const cds = require ('../../../index.js'), { User } = cds
2
2
  const LOG = cds.log('auth')
3
3
 
4
4
  class MockedUsers {
@@ -0,0 +1,7 @@
1
+ try {
2
+ const xssec = require('@sap/xssec')
3
+ module.exports = xssec.v3 || xssec // use v3 compat api // REVISIT: why ???
4
+ } catch (e) {
5
+ if (e.code === 'MODULE_NOT_FOUND') e.message = `Cannot find '@sap/xssec'. Make sure to install it with 'npm i @sap/xssec'\n` + e.message
6
+ throw e
7
+ }
@@ -1,4 +1,4 @@
1
- const auth = exports.auth = require('../../auth')
1
+ const auth = exports.auth = require('./auth')
2
2
  const context = exports.context = require('./cds-context')
3
3
  const ctx_model = exports.ctx_model = require('./ctx-model')
4
4
  const errors = exports.errors = require('./errors')
@@ -111,13 +111,13 @@ exports.stack = (depth=11) => {
111
111
  * **WARNING:** This is an **expensive** function → handle with care!
112
112
  * @returns {[ filename:string, line:number, column:number ]}
113
113
  */
114
- exports.location = function (){
114
+ exports.location = function() {
115
115
  const l = this.stack(3)[2]
116
116
  return [ l.getFileName(), l.getLineNumber(), l.getColumnNumber() ]
117
117
  }
118
118
 
119
119
 
120
- exports.exists = function exists (x) {
120
+ exports.exists = function(x) {
121
121
  if (x) {
122
122
  const y = resolve (cds.root,x)
123
123
  return fs.existsSync(y)
@@ -167,29 +167,25 @@ exports.read = async function read (file, _encoding) {
167
167
 
168
168
  exports.write = function write (file, data, o) {
169
169
  if (arguments.length === 1) return {to:(...path) => write(join(...path),file)}
170
- if (typeof data === 'object' && !Buffer.isBuffer(data))
171
- data = JSON.stringify(data, null, ' '.repeat(o && o.spaces)) + '\n'
170
+ if (typeof data === 'object' && !Buffer.isBuffer(data)) {
171
+ let indent = o?.spaces || file.match(/(package|.cdsrc).json$/) && 2
172
+ data = JSON.stringify(data, null, indent) + '\n'
173
+ }
174
+ const f = resolve (cds.root,file)
175
+ return exports.mkdirp (dirname(f)).then (()=> fs.promises.writeFile (f,data,o))
176
+ }
177
+
178
+ exports.append = function append (file, data, o) {
179
+ if (arguments.length === 1) return {to:(...path) => append(join(...path), data)}
172
180
  const f = resolve (cds.root,file)
173
- return fs.mkdirp (dirname(f)).then (()=> fs.promises.writeFile (f,data,o))
181
+ return exports.mkdirp (dirname(f)).then (()=> fs.promises.writeFile (f,data,o))
174
182
  }
175
183
 
176
184
  exports.copy = function copy (x,y) {
177
185
  if (arguments.length === 1) return {to:(...path) => copy(x,join(...path))}
178
186
  const src = resolve (cds.root,x)
179
187
  const dst = resolve (cds.root,y)
180
- if (fs.promises.cp) return fs.promises.cp (src,dst,{recursive:true})
181
- return fs.mkdirp (dirname(dst)) .then (async ()=>{
182
- if (fs.isdir(src)) {
183
- const entries = await fs.promises.readdir(src)
184
- return Promise.all (entries.map (async each => {
185
- const e = join (src,each)
186
- const f = join (dst,each)
187
- return copy (e,f)
188
- }))
189
- } else {
190
- return fs.promises.copyFile (src,dst)
191
- }
192
- })
188
+ return fs.promises.cp (src,dst,{recursive:true})
193
189
  }
194
190
 
195
191
  exports.mkdirp = async function (...path) {
@@ -308,7 +304,7 @@ const SECRETS = /(passw)|(cert)|(ca)|(secret)|(key)/i
308
304
  * @param {any} cred - object or array with credentials
309
305
  * @returns {any}
310
306
  */
311
- exports._redacted = function _redacted(cred) {
307
+ exports.redacted = function _redacted(cred) {
312
308
  if (!cred) return cred
313
309
  if (Array.isArray(cred)) return cred.map(c => typeof c === 'string' ? '...' : _redacted(c))
314
310
  if (typeof cred === 'object') {
package/lib/utils/tar.js CHANGED
@@ -81,9 +81,9 @@ const tarInfo = async (info) => {
81
81
  const logDebugTar = async () => {
82
82
  const LOG = cds.log('tar')
83
83
  if (!LOG?._debug) return
84
- try {
85
- LOG (`tar version: ${await tarInfo('version')}`)
84
+ try {
86
85
  LOG (`tar path: ${await tarInfo('path')}`)
86
+ LOG (`tar version: ${await tarInfo('version')}`)
87
87
  } catch (err) {
88
88
  LOG('tar error', err)
89
89
  }
@@ -52,13 +52,13 @@ function _log(level, arg) {
52
52
  const _details = obj.details
53
53
 
54
54
  // REVISIT: the (ugly!) stuff re code is somewhat of a duplicate to what we do in normalizeError -> refactor with new adapters!
55
- obj.message = getErrorMessage(obj)
55
+ obj.message = getErrorMessage(obj, '')
56
56
  if (_code) obj.code = obj.code.match?.(/^ASSERT_/) ? 400 : obj.code
57
57
  if (_details) {
58
58
  const details = []
59
59
  const codes = new Set()
60
60
  for (const d of obj.details) {
61
- const entry = Object.assign({}, d, { message: getErrorMessage(d) })
61
+ const entry = Object.assign({}, d, { message: getErrorMessage(d, '') })
62
62
  if (!_code) {
63
63
  const k = d.statusCode ? 'statusCode' : d.status ? 'status' : d.code ? 'code' : undefined
64
64
  const v = d[k]?.match?.(/^ASSERT_/) ? 400 : d[k]
@@ -561,6 +561,7 @@ const read = service => {
561
561
  } else if (result.value && isStreaming(odataReq.getUriInfo().getPathSegments())) {
562
562
  if (odataRes._response.destroyed) {
563
563
  err = new Error('Response is closed while streaming')
564
+ err.code = 'ERR_STREAM_PREMATURE_CLOSE'
564
565
  tx.rollback(err).catch(() => {})
565
566
  } else {
566
567
  // REVISIT: temp workaround for url streaming
@@ -189,7 +189,7 @@ const update = service => {
189
189
  // try UPDATE and, on 404 error, try CREATE
190
190
  ;[result, req] = await _updateThenCreate(req, odataReq, odataRes, tx)
191
191
 
192
- if (!(primitive && !_hasEtag(req.target)) && req._.readAfterWrite) {
192
+ if ((!primitive || _hasEtag(req.target)) && req._.readAfterWrite) {
193
193
  // REVISIT:
194
194
  // Performance: For `isReturnMinimal` it's enough to just read the etag.
195
195
  // Note: Without read access, one cannot return the etag.
@@ -8,11 +8,7 @@
8
8
  */
9
9
  const localeFrom = require('../../../../lib/req/locale')
10
10
  const { getErrorMessage } = require('./utils')
11
- let _i18n
12
- const i18n = (...args) => {
13
- if (!_i18n) _i18n = require('../i18n')
14
- return _i18n(...args)
15
- }
11
+ const { i18n } = require('../../../../lib')
16
12
 
17
13
  const {
18
14
  ALLOWED_PROPERTIES_MAP,
@@ -121,7 +117,7 @@ const _statusCodeFromDetails = uniqueStatusCodes => {
121
117
  }
122
118
 
123
119
  const _getSanitizedError = (statusCode, locale) => ({
124
- error: { code: String(statusCode), message: i18n(statusCode, locale) },
120
+ error: { code: String(statusCode), message: i18n.messages.at(statusCode, locale) }, // REVISIT: don't do that
125
121
  statusCode
126
122
  })
127
123
 
@@ -1,15 +1,11 @@
1
1
  // REVISIT: I think we can remove this file. Log output doesn't need localization.
2
+ // REVISIT: We should remove the i18n machinery from log output.
2
3
  const cds = require('../../cds')
3
4
  const LOG = cds.log()
4
5
 
5
- let _i18n
6
- const i18n = (...args) => {
7
- if (!_i18n) _i18n = require('../i18n')
8
- return _i18n(...args)
9
- }
10
-
11
6
  const { isClientError } = require('./frontend')
12
7
 
8
+ // REVISIT: Where is that being used...?
13
9
  module.exports = err => {
14
10
  // REVISIT: how does level behave compared to _log in (legacy) odata adapter?
15
11
  const level = isClientError(err) ? 'warn' : 'error'
@@ -18,11 +14,14 @@ module.exports = err => {
18
14
  // replace messages in toLog with developer texts (i.e., undefined locale)
19
15
  const _message = err.message
20
16
  const _details = err.details
21
- err.message = i18n(err.message || err.code, undefined, err.args) || err.message
17
+ // REVISIT: We shouldn't run log output thru i18n machinery at all!
18
+ err.message = cds.i18n.messages.at(err.message || err.code, '', err.args) || err.message
22
19
  if (err.details) {
23
20
  const details = []
24
21
  for (const d of err.details) {
25
- details.push(Object.assign({}, d, { message: i18n(d.message || d.code, undefined, d.args) || d.message }))
22
+ details.push(
23
+ Object.assign({}, d, { message: cds.i18n.messages.at(d.message || d.code, '', d.args) || d.message })
24
+ )
26
25
  }
27
26
  err.details = details
28
27
  }
@@ -1,9 +1,4 @@
1
- let _i18n
2
- // REVISIT does it really make sense to delay i18n require here?
3
- const i18n = (...args) => {
4
- if (!_i18n) _i18n = require('../i18n')
5
- return _i18n(...args)
6
- }
1
+ const { i18n } = require('../../../../lib')
7
2
 
8
3
  /**
9
4
  * Gets the localized error message for an error based on message, code, statusCode or status
@@ -11,9 +6,10 @@ const i18n = (...args) => {
11
6
  * @param {*} locale can be undefined for default language
12
7
  * @returns localized error message
13
8
  */
9
+ // REVISIT: Where is that being used...?
14
10
  function getErrorMessage(error, locale) {
15
11
  const key = error.message || error.code || error.status || error.statusCode || '500'
16
- const txt = i18n(key, locale, error.args)
12
+ const txt = i18n.messages.at(key, locale, error.args)
17
13
  return txt || error.message || String(error.code || error.status || error.statusCode)
18
14
  }
19
15