@sap/cds 7.1.2 → 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.
- package/CHANGELOG.md +55 -4
- package/apis/cds.d.ts +10 -6
- package/apis/connect.d.ts +0 -1
- package/apis/core.d.ts +54 -5
- package/apis/log.d.ts +19 -6
- package/apis/models.d.ts +0 -18
- package/apis/ql.d.ts +23 -23
- package/apis/serve.d.ts +17 -14
- package/apis/services.d.ts +40 -29
- package/apis/test.d.ts +1 -2
- package/bin/serve.js +4 -4
- package/lib/auth/basic-auth.js +1 -1
- package/lib/auth/dummy-auth.js +2 -1
- package/lib/auth/ias-auth.js +68 -2
- package/lib/auth/index.js +5 -5
- package/lib/auth/jwt-auth.js +40 -24
- package/lib/auth/mocked-users.js +0 -13
- package/lib/auth/passport-basic.js +2 -0
- package/lib/auth/passport-digest.js +2 -0
- package/lib/compile/etc/_localized.js +0 -1
- package/lib/compile/extend.js +16 -0
- package/lib/compile/for/lean_drafts.js +38 -6
- package/lib/compile/resolve.js +7 -5
- package/lib/compile/to/json.js +6 -2
- package/lib/dbs/cds-deploy.js +3 -3
- package/lib/env/cds-env.js +3 -3
- package/lib/env/cds-requires.js +1 -0
- package/lib/env/defaults.js +8 -1
- package/lib/env/schemas/cds-rc.json +27 -3
- package/lib/i18n/localize.js +3 -3
- package/lib/index.js +4 -0
- package/lib/log/cds-log.js +10 -1
- package/lib/ql/Whereable.js +7 -3
- package/lib/req/user.js +18 -16
- package/lib/srv/middlewares/sap-statistics.js +3 -3
- package/lib/srv/middlewares/trace.js +5 -4
- package/lib/srv/srv-dispatch.js +10 -9
- package/lib/utils/axios.js +3 -0
- package/lib/utils/cds-test.js +3 -0
- package/lib/utils/cds-utils.js +2 -0
- package/libx/_runtime/auth/index.js +8 -32
- package/libx/_runtime/auth/strategies/ias-auth.js +1 -77
- package/libx/_runtime/auth/strategies/mock.js +1 -12
- package/libx/_runtime/auth/strategies/xssecUtils.js +2 -2
- package/libx/_runtime/cds-services/adapter/odata-v4/handlers/request.js +11 -9
- package/libx/_runtime/cds-services/adapter/odata-v4/odata-to-cqn/ExpressionToCQN.js +5 -0
- package/libx/_runtime/cds-services/adapter/odata-v4/odata-to-cqn/applyToCQN.js +5 -2
- package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-commons/uri/UriParser.js +4 -0
- package/libx/_runtime/common/composition/data.js +5 -3
- package/libx/_runtime/common/composition/insert.js +6 -3
- package/libx/_runtime/common/composition/update.js +12 -8
- package/libx/_runtime/common/error/constants.js +6 -1
- package/libx/_runtime/common/generic/auth/requires.js +11 -3
- package/libx/_runtime/common/generic/auth/restrict.js +21 -15
- package/libx/_runtime/common/generic/auth/restrictions.js +5 -2
- package/libx/_runtime/common/generic/crud.js +6 -0
- package/libx/_runtime/common/generic/paging.js +2 -1
- package/libx/_runtime/common/utils/cqn2cqn4sql.js +3 -5
- package/libx/_runtime/common/utils/resolveView.js +3 -1
- package/libx/_runtime/common/utils/restrictions.js +47 -0
- package/libx/_runtime/db/data-conversion/post-processing.js +3 -3
- package/libx/_runtime/db/generic/input.js +1 -1
- package/libx/_runtime/db/sql-builder/ExpressionBuilder.js +0 -17
- package/libx/_runtime/fiori/lean-draft.js +6 -3
- package/libx/_runtime/hana/driver.js +2 -4
- package/libx/_runtime/hana/pool.js +1 -1
- package/libx/_runtime/messaging/enterprise-messaging-utils/registerEndpoints.js +2 -2
- package/libx/_runtime/messaging/outbox/utils.js +1 -2
- package/libx/_runtime/remote/utils/client.js +1 -1
- package/libx/_runtime/sqlite/Service.js +0 -4
- package/libx/_runtime/sqlite/customBuilder/CustomFunctionBuilder.js +2 -1
- package/libx/odata/afterburner.js +5 -3
- package/libx/odata/cqn2odata.js +7 -7
- package/libx/odata/utils.js +4 -1
- package/libx/rest/RestAdapter.js +15 -16
- package/package.json +1 -1
- package/lib/auth/xsuaa-auth.js +0 -2
- package/libx/_runtime/auth/utils.js +0 -32
- package/libx/audit-log/client.cds +0 -0
- package/libx/audit-log/client.js +0 -0
package/lib/dbs/cds-deploy.js
CHANGED
|
@@ -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
|
|
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
|
}
|
package/lib/env/cds-env.js
CHANGED
|
@@ -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
|
|
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 {
|
package/lib/env/cds-requires.js
CHANGED
|
@@ -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 ] },
|
package/lib/env/defaults.js
CHANGED
|
@@ -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
|
-
"
|
|
261
|
-
|
|
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": [
|
package/lib/i18n/localize.js
CHANGED
|
@@ -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
|
|
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(
|
|
186
|
-
if (next.endsWith(
|
|
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
|
package/lib/log/cds-log.js
CHANGED
|
@@ -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)
|
|
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))
|
package/lib/ql/Whereable.js
CHANGED
|
@@ -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(
|
|
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(
|
|
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')
|
|
12
|
-
|
|
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
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
role === '
|
|
27
|
-
role === '
|
|
28
|
-
|
|
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
|
|
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
|
|
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), '= ',
|
package/lib/srv/srv-dispatch.js
CHANGED
|
@@ -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,
|
|
118
|
-
if (
|
|
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 &&
|
|
121
|
-
if (
|
|
122
|
-
if (
|
|
123
|
-
else x[p] = `${srv.namespace}.${
|
|
124
|
-
} else if (
|
|
125
|
-
const [head] =
|
|
126
|
-
head.id ? _ensure_fqn(head,'id',srv) : _ensure_fqn(
|
|
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
|
}
|
package/lib/utils/axios.js
CHANGED
|
@@ -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
|
|
package/lib/utils/cds-test.js
CHANGED
|
@@ -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')
|
package/lib/utils/cds-utils.js
CHANGED
|
@@ -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 {
|
|
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:
|
|
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
|
-
|
|
110
|
-
|
|
111
|
-
|
|
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 (
|
|
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
|
-
|
|
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.
|
|
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.
|
|
15
|
-
if (info.getClientId() === credentials.clientid) user.
|
|
14
|
+
user.roles['system-user'] = true
|
|
15
|
+
if (info.getClientId() === credentials.clientid) user.roles['internal-user'] = true
|
|
16
16
|
}
|
|
17
17
|
}
|
|
18
18
|
}
|
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
const cds = require('../../../../cds')
|
|
2
2
|
|
|
3
|
-
const {
|
|
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
|
|
7
|
-
const
|
|
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 (
|
|
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(
|
|
33
|
+
return next(ODATA_UNAUTHORIZED)
|
|
32
34
|
}
|
|
33
35
|
|
|
34
|
-
// check @requires as soon as possible (DoS)
|
|
35
|
-
if (
|
|
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(
|
|
43
|
+
return next(ODATA_UNAUTHORIZED)
|
|
42
44
|
}
|
|
43
|
-
return next(
|
|
45
|
+
return next(ODATA_FORBIDDEN)
|
|
44
46
|
}
|
|
45
47
|
|
|
46
48
|
/*
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
const cds = require('../../../../cds')
|
|
2
|
+
|
|
1
3
|
const odata = require('../okra/odata-server')
|
|
2
4
|
const ExpressionKind = odata.uri.Expression.ExpressionKind
|
|
3
5
|
const BinaryOperatorKind = odata.uri.BinaryExpression.OperatorKind
|
|
@@ -41,7 +43,10 @@ class ExpressionToCQN {
|
|
|
41
43
|
case EdmPrimitiveTypeKind.Int16:
|
|
42
44
|
case EdmPrimitiveTypeKind.Int32:
|
|
43
45
|
return { val: parseInt(value) }
|
|
46
|
+
case EdmPrimitiveTypeKind.Int64:
|
|
47
|
+
return { val: value.toString() }
|
|
44
48
|
case EdmPrimitiveTypeKind.Decimal:
|
|
49
|
+
return cds.env.features.compat_decimal ? { val: parseFloat(value) } : { val: value.toString() }
|
|
45
50
|
case EdmPrimitiveTypeKind.Single:
|
|
46
51
|
case EdmPrimitiveTypeKind.Double:
|
|
47
52
|
return { val: parseFloat(value) }
|
|
@@ -225,8 +225,11 @@ const applyToCQN = (transformations, entity, model) => {
|
|
|
225
225
|
case TransformationKind.BOTTOM_TOP:
|
|
226
226
|
_addBottomTopTransformation(transformation, res)
|
|
227
227
|
break
|
|
228
|
-
default:
|
|
229
|
-
|
|
228
|
+
default: {
|
|
229
|
+
const numericKind = transformation.getKind()
|
|
230
|
+
const stringKind = Object.entries(TransformationKind).find(([_, v]) => v === numericKind)?.[0]
|
|
231
|
+
throw getFeatureNotSupportedError(`Transformation "${stringKind || numericKind}" with query option $apply`)
|
|
232
|
+
}
|
|
230
233
|
}
|
|
231
234
|
}
|
|
232
235
|
|