@sap/cds 8.4.1 → 8.5.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.
- package/CHANGELOG.md +41 -1
- package/_i18n/messages.properties +99 -0
- package/bin/serve.js +2 -2
- package/lib/compile/cdsc.js +9 -4
- package/lib/compile/to/srvinfo.js +4 -4
- package/lib/core/classes.js +5 -1
- package/lib/core/entities.js +1 -0
- package/lib/core/types.js +1 -1
- package/lib/dbs/cds-deploy.js +4 -1
- package/lib/env/defaults.js +7 -6
- package/lib/env/schemas/cds-rc.js +132 -22
- package/lib/i18n/bundles.js +111 -0
- package/lib/i18n/files.js +134 -0
- package/lib/i18n/index.js +63 -0
- package/lib/i18n/localize.js +101 -237
- package/lib/i18n/resources.js +150 -0
- package/lib/index.js +1 -0
- package/lib/log/format/aspects/cls.js +6 -1
- package/lib/log/format/json.js +1 -1
- package/lib/ql/CREATE.js +1 -0
- package/lib/ql/DELETE.js +1 -0
- package/lib/ql/DROP.js +1 -0
- package/lib/ql/INSERT.js +9 -8
- package/lib/ql/Query.js +18 -8
- package/lib/ql/SELECT.js +1 -0
- package/lib/ql/UPDATE.js +2 -1
- package/lib/ql/UPSERT.js +1 -1
- package/lib/ql/Whereable.js +3 -3
- package/lib/ql/cds-ql.js +12 -18
- package/lib/req/user.js +1 -0
- package/lib/req/validate.js +12 -3
- package/lib/srv/factory.js +2 -2
- package/lib/{auth → srv/middlewares/auth}/basic-auth.js +1 -1
- package/lib/{auth → srv/middlewares/auth}/dummy-auth.js +1 -1
- package/lib/srv/middlewares/auth/ias-auth.js +96 -0
- package/lib/{auth → srv/middlewares/auth}/index.js +2 -2
- package/lib/srv/middlewares/auth/jwt-auth.js +62 -0
- package/lib/{auth → srv/middlewares/auth}/mocked-users.js +1 -1
- package/lib/srv/middlewares/auth/xssec.js +7 -0
- package/lib/srv/middlewares/index.js +1 -1
- package/lib/utils/cds-utils.js +15 -19
- package/lib/utils/tar.js +2 -2
- package/libx/_runtime/cds-services/adapter/odata-v4/OData.js +2 -2
- package/libx/_runtime/cds-services/adapter/odata-v4/handlers/read.js +1 -0
- package/libx/_runtime/cds-services/adapter/odata-v4/handlers/update.js +1 -1
- package/libx/_runtime/common/error/frontend.js +2 -6
- package/libx/_runtime/common/error/log.js +7 -8
- package/libx/_runtime/common/error/utils.js +3 -7
- package/libx/_runtime/common/generic/auth/capabilities.js +1 -1
- package/libx/_runtime/common/generic/input.js +41 -6
- package/libx/_runtime/common/i18n/index.js +8 -15
- package/libx/_runtime/common/utils/compareJson.js +10 -1
- package/libx/_runtime/common/utils/resolveView.js +1 -1
- package/libx/_runtime/fiori/lean-draft.js +77 -26
- package/libx/_runtime/messaging/enterprise-messaging-utils/registerEndpoints.js +5 -1
- package/libx/_runtime/messaging/kafka.js +1 -1
- package/libx/odata/index.js +3 -0
- package/libx/odata/middleware/create.js +8 -6
- package/libx/odata/middleware/update.js +24 -21
- package/libx/odata/parse/afterburner.js +15 -2
- package/libx/odata/parse/grammar.peggy +24 -7
- package/libx/odata/parse/parser.js +1 -1
- package/libx/odata/utils/postProcess.js +4 -1
- package/libx/rest/RestAdapter.js +2 -1
- package/libx/rest/middleware/error.js +0 -50
- package/package.json +1 -1
- package/lib/auth/ias-auth.js +0 -68
- package/lib/auth/ias-claims.js +0 -34
- package/lib/auth/jwt-auth.js +0 -70
- package/libx/_runtime/common/i18n/messages.properties +0 -99
- 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
|
-
|
|
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
|
|
20
|
-
return {__proto__:this, [
|
|
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.
|
|
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
|
|
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 (
|
|
89
|
-
return `${
|
|
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
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
package/lib/ql/Whereable.js
CHANGED
|
@@ -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
|
|
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.
|
|
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
|
|
5
|
-
Object.
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
})
|
|
9
|
-
|
|
10
|
-
return
|
|
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 (
|
|
34
|
-
let
|
|
35
|
-
|
|
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(
|
|
43
|
-
return `${
|
|
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
package/lib/req/validate.js
CHANGED
|
@@ -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
|
-
|
|
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
|
}
|
package/lib/srv/factory.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
const cds = require('..'), { path, isfile,
|
|
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:
|
|
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 ('
|
|
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
|
|
|
@@ -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 ('
|
|
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
|
+
}
|
|
@@ -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('
|
|
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')
|
package/lib/utils/cds-utils.js
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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.
|
|
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 (!
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
|