@sap/cds 7.1.1 → 7.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +68 -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 +4 -4
- 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/ODataRequest.js +3 -1
- 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/cds-services/services/utils/compareJson.js +0 -9
- package/libx/_runtime/cds-services/services/utils/differ.js +8 -10
- package/libx/_runtime/common/composition/data.js +10 -7
- package/libx/_runtime/common/composition/insert.js +9 -5
- package/libx/_runtime/common/composition/update.js +18 -12
- 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 +22 -16
- 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/db/utils/coloredTxCommands.js +5 -3
- package/libx/_runtime/fiori/lean-draft.js +24 -19
- package/libx/_runtime/hana/driver.js +2 -4
- package/libx/_runtime/hana/pool.js +53 -57
- 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 +6 -4
- 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/apis/services.d.ts
CHANGED
|
@@ -9,6 +9,9 @@ import { ref } from './cqn'
|
|
|
9
9
|
// import { Service } from './cds'
|
|
10
10
|
|
|
11
11
|
export class QueryAPI {
|
|
12
|
+
|
|
13
|
+
entities : LinkedModel['entities']
|
|
14
|
+
|
|
12
15
|
/**
|
|
13
16
|
* @see [docs](https://cap.cloud.sap/docs/node.js/services#srv-run)
|
|
14
17
|
*/
|
|
@@ -57,7 +60,7 @@ export class QueryAPI {
|
|
|
57
60
|
(query: Query): Promise<ResultSet | any>
|
|
58
61
|
(query: string, args?: any[] | object): Promise<ResultSet | any>
|
|
59
62
|
}
|
|
60
|
-
|
|
63
|
+
|
|
61
64
|
/**
|
|
62
65
|
* @see [docs](https://cap.cloud.sap/docs/node.js/cds-facade?q=cds.delete)
|
|
63
66
|
*/
|
|
@@ -86,7 +89,7 @@ export class QueryAPI {
|
|
|
86
89
|
*/
|
|
87
90
|
tx(context?: object): Transaction
|
|
88
91
|
transaction(context?: object): Transaction
|
|
89
|
-
|
|
92
|
+
|
|
90
93
|
/**
|
|
91
94
|
* @see [docs](https://pages.github.tools.sap/cap/docs/node.js/cds-context-tx?q=spawn#cds-spawn)
|
|
92
95
|
*/
|
|
@@ -166,8 +169,8 @@ export class Service extends QueryAPI {
|
|
|
166
169
|
* @see [capire docs](https://cap.cloud.sap/docs/node.js/services#srv-emit)
|
|
167
170
|
*/
|
|
168
171
|
emit: {
|
|
169
|
-
<T = any>(details: { event:
|
|
170
|
-
<T = any>(event:
|
|
172
|
+
<T = any>(details: { event: EventArg; data?: object; headers?: object }): Promise<T>
|
|
173
|
+
<T = any>(event: EventArg, data?: object, headers?: object): Promise<T>
|
|
171
174
|
}
|
|
172
175
|
|
|
173
176
|
/**
|
|
@@ -175,12 +178,12 @@ export class Service extends QueryAPI {
|
|
|
175
178
|
* @see [capire docs](https://cap.cloud.sap/docs/node.js/services#srvsend--method-path-data-headers--results-)
|
|
176
179
|
*/
|
|
177
180
|
send: {
|
|
178
|
-
<T = any>(event:
|
|
179
|
-
<T = any>(event:
|
|
180
|
-
<T = any>(details: { event:
|
|
181
|
+
<T = any>(event: EventArg, path: string, data?: object, headers?: object): Promise<T>
|
|
182
|
+
<T = any>(event: EventArg, data?: object, headers?: object): Promise<T>
|
|
183
|
+
<T = any>(details: { event: EventArg; data?: object; headers?: object }): Promise<T>
|
|
181
184
|
<T = any>(details: { query: ConstructedQuery; data?: object; headers?: object }): Promise<T>
|
|
182
|
-
<T = any>(details: { method:
|
|
183
|
-
<T = any>(details: { event:
|
|
185
|
+
<T = any>(details: { method: EventName; path: string; data?: object; headers?: object }): Promise<T>
|
|
186
|
+
<T = any>(details: { event: EventName; entity: Definition | string; data?: object; params?: object }): Promise<T>
|
|
184
187
|
}
|
|
185
188
|
|
|
186
189
|
/**
|
|
@@ -213,28 +216,28 @@ export class Service extends QueryAPI {
|
|
|
213
216
|
}
|
|
214
217
|
|
|
215
218
|
// The central method to dispatch events
|
|
216
|
-
dispatch(msg:
|
|
219
|
+
dispatch(msg: Event): Promise<any>
|
|
217
220
|
|
|
218
221
|
// Provider API
|
|
219
222
|
prepend(fn: ServiceImpl): Promise<this>
|
|
220
|
-
on<T extends Constructable>(eve:
|
|
223
|
+
on<T extends Constructable>(eve: EventArg, entity: T, handler: CRUDEventHandler.On<InstanceType<T>, InstanceType<T> | void | Error>): this
|
|
221
224
|
on<P,R>(boundAction: (args: P) => R, service: string, handler: ActionEventHandler<P, void | Error | R>): this
|
|
222
225
|
on<P,R>(action: (args: P) => R, handler: ActionEventHandler<P, void | Error | R>): this
|
|
223
|
-
on(eve:
|
|
224
|
-
on(eve:
|
|
226
|
+
on(eve: EventArg, entity: Target, handler: OnEventHandler): this
|
|
227
|
+
on(eve: EventArg, handler: OnEventHandler): this
|
|
225
228
|
|
|
226
229
|
|
|
227
230
|
// onSucceeded (eve: Events, entity: Target, handler: EventHandler): this
|
|
228
231
|
// onSucceeded (eve: Events, handler: EventHandler): this
|
|
229
232
|
// onFailed (eve: Events, entity: Target, handler: EventHandler): this
|
|
230
233
|
// onFailed (eve: Events, handler: EventHandler): this
|
|
231
|
-
before<T extends Constructable>(eve:
|
|
232
|
-
before(eve:
|
|
233
|
-
before(eve:
|
|
234
|
-
after<T extends Constructable>(eve:
|
|
235
|
-
after(eve:
|
|
236
|
-
after(eve:
|
|
237
|
-
reject(eves:
|
|
234
|
+
before<T extends Constructable>(eve: EventArg, entity: T, handler: CRUDEventHandler.Before<InstanceType<T>, InstanceType<T> | void | Error>): this
|
|
235
|
+
before(eve: EventArg, entity: Target, handler: EventHandler): this
|
|
236
|
+
before(eve: EventArg, handler: EventHandler): this
|
|
237
|
+
after<T extends Constructable>(eve: EventArg, entity: T, handler: CRUDEventHandler.After<InstanceType<T>, InstanceType<T> | void | Error>): this
|
|
238
|
+
after(eve: EventArg, entity: Target, handler: ResultsHandler): this
|
|
239
|
+
after(eve: EventArg, handler: ResultsHandler): this
|
|
240
|
+
reject(eves: EventArg, ...entity: Target[]): this
|
|
238
241
|
}
|
|
239
242
|
|
|
240
243
|
export interface Transaction extends Service {
|
|
@@ -242,6 +245,9 @@ export interface Transaction extends Service {
|
|
|
242
245
|
rollback(): Promise<void>
|
|
243
246
|
}
|
|
244
247
|
|
|
248
|
+
export class ApplicationService extends Service {}
|
|
249
|
+
export class MessagingService extends Service {}
|
|
250
|
+
export class RemoteService extends Service {}
|
|
245
251
|
export class DatabaseService extends Service {
|
|
246
252
|
deploy(model?: csn | string): Promise<csn>
|
|
247
253
|
begin(): Promise<void>
|
|
@@ -251,26 +257,29 @@ export class DatabaseService extends Service {
|
|
|
251
257
|
|
|
252
258
|
export interface ResultSet extends Array<{}> {}
|
|
253
259
|
|
|
254
|
-
|
|
260
|
+
declare class cds {
|
|
255
261
|
/**
|
|
256
262
|
* @see [capire docs](https://cap.cloud.sap/docs/node.js/services)
|
|
257
263
|
*/
|
|
258
264
|
Service: typeof Service
|
|
265
|
+
Request: typeof Request
|
|
266
|
+
Event: typeof Event
|
|
267
|
+
EventContext: typeof EventContext
|
|
259
268
|
|
|
260
269
|
/**
|
|
261
270
|
* @see [capire docs](https://cap.cloud.sap/docs/node.js/app-services)
|
|
262
271
|
*/
|
|
263
|
-
ApplicationService: typeof
|
|
272
|
+
ApplicationService: typeof ApplicationService
|
|
264
273
|
|
|
265
274
|
/**
|
|
266
275
|
* @see [capire docs](https://cap.cloud.sap/docs/node.js/remote-services)
|
|
267
276
|
*/
|
|
268
|
-
RemoteService: typeof
|
|
277
|
+
RemoteService: typeof RemoteService
|
|
269
278
|
|
|
270
279
|
/**
|
|
271
280
|
* @see [capire docs](https://cap.cloud.sap/docs/node.js/messaging)
|
|
272
281
|
*/
|
|
273
|
-
MessagingService: typeof
|
|
282
|
+
MessagingService: typeof MessagingService
|
|
274
283
|
|
|
275
284
|
/**
|
|
276
285
|
* @see [capire docs](https://cap.cloud.sap/docs/node.js/databases)
|
|
@@ -336,7 +345,7 @@ interface ResultsHandler {
|
|
|
336
345
|
* Represents the invocation context of incoming request and event messages.
|
|
337
346
|
* @see [capire docs](https://cap.cloud.sap/docs/node.js/requests)
|
|
338
347
|
*/
|
|
339
|
-
|
|
348
|
+
export class EventContext {
|
|
340
349
|
timestamp: Date
|
|
341
350
|
locale: string
|
|
342
351
|
id: string
|
|
@@ -347,7 +356,7 @@ interface EventContext {
|
|
|
347
356
|
/**
|
|
348
357
|
* @see [capire docs](https://cap.cloud.sap/docs/node.js/requests)
|
|
349
358
|
*/
|
|
350
|
-
|
|
359
|
+
export class Event extends EventContext {
|
|
351
360
|
event: string
|
|
352
361
|
data: any
|
|
353
362
|
headers: {}
|
|
@@ -379,7 +388,7 @@ interface Options {
|
|
|
379
388
|
/**
|
|
380
389
|
* @see [capire docs](https://cap.cloud.sap/docs/node.js/requests)
|
|
381
390
|
*/
|
|
382
|
-
|
|
391
|
+
export class Request extends Event {
|
|
383
392
|
params: (string | {})[]
|
|
384
393
|
method: string
|
|
385
394
|
path: string
|
|
@@ -414,8 +423,10 @@ interface Request extends EventMessage {
|
|
|
414
423
|
reject(message: { code?: number | string; message: string; target?: string; args?: {}, status?: number }): Error
|
|
415
424
|
}
|
|
416
425
|
|
|
417
|
-
|
|
418
|
-
|
|
426
|
+
export default cds
|
|
427
|
+
|
|
428
|
+
type EventArg = EventName | EventName[]
|
|
429
|
+
type EventName = (CRUD | TX | HTTP | DRAFT) | (CustomOp & {})
|
|
419
430
|
type CRUD = 'CREATE' | 'READ' | 'UPDATE' | 'DELETE'
|
|
420
431
|
type DRAFT = 'NEW' | 'EDIT' | 'PATCH' | 'SAVE'
|
|
421
432
|
type HTTP = 'GET' | 'PUT' | 'POST' | 'PATCH' | 'DELETE'
|
package/apis/test.d.ts
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { AxiosInstance } from 'axios';
|
|
2
2
|
import chai from 'chai';
|
|
3
|
-
import * as main_cds from './cds';
|
|
4
3
|
import * as http from 'http';
|
|
5
4
|
import { Service } from './services';
|
|
6
5
|
|
|
@@ -37,7 +36,7 @@ declare class Test extends Axios {
|
|
|
37
36
|
get expect(): typeof chai.expect;
|
|
38
37
|
get assert(): typeof chai.assert;
|
|
39
38
|
get data(): DataUtil;
|
|
40
|
-
get cds(): typeof
|
|
39
|
+
get cds(): typeof import('./cds').default;
|
|
41
40
|
|
|
42
41
|
then(r: (args: { server: http.Server, url: string }) => void): void;
|
|
43
42
|
|
package/bin/serve.js
CHANGED
|
@@ -111,7 +111,7 @@ module.exports = Object.assign ( serve, {
|
|
|
111
111
|
database, if any, and only adds an in-memory database if no
|
|
112
112
|
persistent one is configured.
|
|
113
113
|
|
|
114
|
-
Requires an
|
|
114
|
+
Requires an SQLite driver to be installed. For example: _npm i @cap-js/sqlite_.
|
|
115
115
|
|
|
116
116
|
# EXAMPLES
|
|
117
117
|
|
|
@@ -168,7 +168,7 @@ async function serve (all=[], o={}) {
|
|
|
168
168
|
const cds_server = await _local_server_js() || cds.server
|
|
169
169
|
if (!o.silent) _prepare_logging ()
|
|
170
170
|
|
|
171
|
-
// The following things are meant for dev mode, which can be overruled by feature
|
|
171
|
+
// The following things are meant for dev mode, which can be overruled by feature flags...
|
|
172
172
|
const {features} = cds.env
|
|
173
173
|
{
|
|
174
174
|
// handle --with-mocks resp. --mocked
|
|
@@ -206,8 +206,8 @@ async function serve (all=[], o={}) {
|
|
|
206
206
|
|
|
207
207
|
const LOG = cds.log('cli|server')
|
|
208
208
|
cds.shutdown = _shutdown //> for programmatic invocation
|
|
209
|
-
process.on('unhandledRejection', e => _shutdown (e, cds.log(
|
|
210
|
-
process.on('uncaughtException', e => _shutdown (e, cds.log(
|
|
209
|
+
process.on('unhandledRejection', e => _shutdown (e, cds.log().error('❗️Uncaught',e))) //> using std logger to have it labelled with [cds] - instead of [cli] -
|
|
210
|
+
process.on('uncaughtException', e => _shutdown (e, cds.log().error('❗️Uncaught',e))) //> using std logger to have it labelled with [cds] - instead of [cli] -
|
|
211
211
|
process.on('SIGINT', cds.watched ? _shutdown : (s,n)=>_shutdown(s,n,console.log())) //> newline after ^C
|
|
212
212
|
process.on('SIGHUP', _shutdown)
|
|
213
213
|
process.on('SIGHUP2', _shutdown)
|
package/lib/auth/basic-auth.js
CHANGED
|
@@ -12,7 +12,7 @@ module.exports = function basic_auth (options) {
|
|
|
12
12
|
// get basic authorization header
|
|
13
13
|
let auth = req.headers.authorization
|
|
14
14
|
// enforce login if requested
|
|
15
|
-
if (!auth || !auth.match(/^basic/i)) return login_required ? req._login('Logged in user required!') : next()
|
|
15
|
+
if (!auth || !auth.match(/^basic/i)) return login_required ? req._login('Logged in user required!') : req.user = new cds.User.default, next()
|
|
16
16
|
// decode user credentials from autorization header
|
|
17
17
|
let [id,pwd] = Buffer.from(auth.slice(6),'base64').toString().split(':')
|
|
18
18
|
// verify user credentials and set req.user
|
package/lib/auth/dummy-auth.js
CHANGED
package/lib/auth/ias-auth.js
CHANGED
|
@@ -1,2 +1,68 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
const cds = require('../')
|
|
2
|
+
const LOG = cds.log('auth')
|
|
3
|
+
|
|
4
|
+
// _require for better error message
|
|
5
|
+
const _require = require('../../libx/_runtime/common/utils/require')
|
|
6
|
+
const express = _require('express')
|
|
7
|
+
const passport = _require('passport')
|
|
8
|
+
const { JWTStrategy } = _require('@sap/xssec')
|
|
9
|
+
|
|
10
|
+
const RESERVED_ATTRIBUTES = new Set(['aud', 'azp', 'exp', 'ext_attr', 'iat', 'ias_iss', 'iss', 'jti', 'sub', 'user_uuid', 'zone_uuid', 'zid'])
|
|
11
|
+
|
|
12
|
+
module.exports = function ias_auth(config) {
|
|
13
|
+
if (!config.credentials) {
|
|
14
|
+
let msg = `Authentication kind "${config.kind}" configured, but no IAS instance bound to application.`
|
|
15
|
+
msg += ' Either bind an IAS instance, or switch to an authentication kind that does not require a binding.'
|
|
16
|
+
throw new Error(msg)
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
passport.use('IAS', new JWTStrategy(config.credentials))
|
|
20
|
+
|
|
21
|
+
return express
|
|
22
|
+
.Router()
|
|
23
|
+
.use((req, res, next) => {
|
|
24
|
+
// callback needed in order to suppress 401 and continue with anonymous to allow @requires: 'any'/ restrict_all_services=false
|
|
25
|
+
const callback = (err, user, info, _status) => {
|
|
26
|
+
if (err) return next(err)
|
|
27
|
+
|
|
28
|
+
if (user) {
|
|
29
|
+
req.user = user
|
|
30
|
+
req.authInfo = info
|
|
31
|
+
} else {
|
|
32
|
+
req.user = new cds.User.default
|
|
33
|
+
if (LOG._debug && req.tokenInfo)
|
|
34
|
+
LOG.debug('Error during token validation:', req.tokenInfo.getErrorObject().message)
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
next()
|
|
38
|
+
}
|
|
39
|
+
passport.authenticate('IAS', { session: false }, callback)(req, res, next)
|
|
40
|
+
})
|
|
41
|
+
.use((req, res, next) => {
|
|
42
|
+
if (!('authInfo' in req)) return next()
|
|
43
|
+
|
|
44
|
+
if (req.tokenInfo.getClientId() === req.tokenInfo.getSubject()) //> grant_type === client_credentials or x509
|
|
45
|
+
req.user = new cds.User({
|
|
46
|
+
id: 'system',
|
|
47
|
+
roles: ['authenticated-user', 'system-user'],
|
|
48
|
+
attr: {}
|
|
49
|
+
})
|
|
50
|
+
else {
|
|
51
|
+
// add all unknown attributes to req.user.attr in order to keep public API small
|
|
52
|
+
const payload = req.tokenInfo.getPayload()
|
|
53
|
+
const attributes = Object.keys(payload)
|
|
54
|
+
.filter(k => !RESERVED_ATTRIBUTES.has(k))
|
|
55
|
+
.reduce((attrs, k) => { attrs[k] = payload[k]; return attrs }, {})
|
|
56
|
+
|
|
57
|
+
req.user = new cds.User({
|
|
58
|
+
id: req.user.id,
|
|
59
|
+
roles: ['authenticated-user'],
|
|
60
|
+
attr: attributes
|
|
61
|
+
})
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
req.tenant = req.tokenInfo.getZoneId()
|
|
65
|
+
|
|
66
|
+
next()
|
|
67
|
+
})
|
|
68
|
+
}
|
package/lib/auth/index.js
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
|
|
2
1
|
const cds = require ('../index'), { path, local } = cds.utils
|
|
3
2
|
|
|
4
3
|
const _require = require; require = cds.lazified (module) // eslint-disable-line no-global-assign
|
|
@@ -12,23 +11,24 @@ module.exports = Object.assign (auth_factory, {
|
|
|
12
11
|
})
|
|
13
12
|
require = _require // eslint-disable-line no-global-assign
|
|
14
13
|
|
|
15
|
-
|
|
16
14
|
/**
|
|
17
15
|
* Constructs one of the above middlewares as configured
|
|
18
16
|
*/
|
|
19
17
|
function auth_factory (options) {
|
|
20
18
|
const o = { ...options, ...cds.requires.auth }
|
|
21
19
|
let kind = o.impl ? 'custom' : o.kind || o.strategy
|
|
22
|
-
let middleware = cds.auth[kind]
|
|
20
|
+
let middleware = cds.auth[kind] || cds.auth[kind?.replace(/-auth$/, '')]
|
|
23
21
|
if (middleware) {
|
|
24
|
-
|
|
22
|
+
// REVISIT: here, kind may be misleading, e.g., "basic-auth" instead of "mocked" -> _kind workaround
|
|
23
|
+
const _kind = (o._kind || kind).replace(/-auth$/, '') //> official auth kinds are NOT postfixed with "-auth"
|
|
24
|
+
cds.log().info ('using authentication:', { kind: _kind }, '\n')
|
|
25
25
|
} else {
|
|
26
26
|
let impl = kind === 'custom' ? cds.resolve (o.impl)?.[0] : path.resolve (__dirname, kind)
|
|
27
27
|
try { impl = require.resolve (impl) } catch {
|
|
28
28
|
const e = o.impl ? `Cannot find custom impl at: ${o.impl}` : `Cannot find unknown auth kind: ${o.kind}`
|
|
29
29
|
throw cds.error(e)
|
|
30
30
|
}
|
|
31
|
-
cds.log().info ('using
|
|
31
|
+
cds.log().info ('using authentication:', { kind, impl: local(impl) }, '\n')
|
|
32
32
|
middleware = require(impl)
|
|
33
33
|
}
|
|
34
34
|
if ((typeof middleware === 'function' && middleware.length === 3) || Array.isArray(middleware)) {
|
package/lib/auth/jwt-auth.js
CHANGED
|
@@ -1,41 +1,64 @@
|
|
|
1
1
|
const cds = require('../')
|
|
2
|
-
const
|
|
2
|
+
const LOG = cds.log('auth')
|
|
3
|
+
|
|
3
4
|
// _require for better error message
|
|
5
|
+
const _require = require('../../libx/_runtime/common/utils/require')
|
|
4
6
|
const express = _require('express')
|
|
5
7
|
const passport = _require('passport')
|
|
6
8
|
const { JWTStrategy } = _require('@sap/xssec')
|
|
7
|
-
const LOG = cds.log('auth')
|
|
8
9
|
|
|
9
10
|
module.exports = function jwt_auth(config) {
|
|
10
|
-
// warn if no credentials
|
|
11
11
|
if (!config.credentials) {
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
return (req,res,next) => next()
|
|
12
|
+
let msg = `Authentication kind "${config.kind}" configured, but no XSUAA instance bound to application.`
|
|
13
|
+
msg += ' Either bind an IAS instance, or switch to an authentication kind that does not require a binding.'
|
|
14
|
+
throw new Error(msg)
|
|
16
15
|
}
|
|
17
16
|
|
|
18
|
-
passport.use(
|
|
17
|
+
passport.use('JWT', new JWTStrategy(config.credentials))
|
|
18
|
+
|
|
19
19
|
return express
|
|
20
20
|
.Router()
|
|
21
|
-
.use(passport.authenticate(config.kind, { session: false }))
|
|
22
21
|
.use((req, res, next) => {
|
|
22
|
+
// callback needed in order to suppress 401 and continue with anonymous to allow @requires: 'any'/ restrict_all_services=false
|
|
23
|
+
const callback = (err, user, info, _status) => {
|
|
24
|
+
if (err) return next(err)
|
|
25
|
+
|
|
26
|
+
if (user) {
|
|
27
|
+
req.user = user
|
|
28
|
+
req.authInfo = info
|
|
29
|
+
} else {
|
|
30
|
+
req.user = new cds.User.default
|
|
31
|
+
if (LOG._debug && req.tokenInfo)
|
|
32
|
+
LOG.debug('Error during token validation:', req.tokenInfo.getErrorObject().message)
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
next()
|
|
36
|
+
}
|
|
37
|
+
passport.authenticate('JWT', { session: false }, callback)(req, res, next)
|
|
38
|
+
})
|
|
39
|
+
.use((req, res, next) => {
|
|
40
|
+
if (!('authInfo' in req)) return next()
|
|
41
|
+
|
|
23
42
|
const payload = req.tokenInfo.getPayload()
|
|
24
43
|
|
|
25
44
|
let id = req.user.id
|
|
26
|
-
let _is_system, _is_internal
|
|
27
45
|
|
|
28
|
-
|
|
46
|
+
// Roles = scope names w/o xsappname
|
|
47
|
+
let xsappname = new RegExp(`^${config.credentials.xsappname}\\.`)
|
|
48
|
+
let roles = payload.scope.map(s => s.replace(xsappname, ''))
|
|
49
|
+
|
|
50
|
+
// Disallow setting system roles from external
|
|
51
|
+
roles = roles.filter(r => !(r in { 'internal-user': 1, 'system-user': 1 }))
|
|
52
|
+
|
|
29
53
|
roles.push('identified-user')
|
|
30
54
|
if (payload.grant_type) {
|
|
31
55
|
// > not "weak"
|
|
32
56
|
roles.push('authenticated-user')
|
|
33
57
|
|
|
34
|
-
|
|
35
|
-
if (payload.grant_type in CLIENT) {
|
|
58
|
+
if (payload.grant_type in { client_credentials: 1, client_x509: 1 }) {
|
|
36
59
|
id = 'system'
|
|
37
|
-
|
|
38
|
-
if (req.tokenInfo.getClientId() === config.credentials.clientid)
|
|
60
|
+
roles.push('system-user')
|
|
61
|
+
if (req.tokenInfo.getClientId() === config.credentials.clientid) roles.push('internal-user')
|
|
39
62
|
}
|
|
40
63
|
}
|
|
41
64
|
|
|
@@ -47,16 +70,9 @@ module.exports = function jwt_auth(config) {
|
|
|
47
70
|
attr.email = req.authInfo.getEmail()
|
|
48
71
|
}
|
|
49
72
|
|
|
50
|
-
req.user = new cds.User({ id, roles, attr
|
|
73
|
+
req.user = new cds.User({ id, roles, attr })
|
|
51
74
|
req.tenant = req.tokenInfo.getZoneId?.()
|
|
75
|
+
|
|
52
76
|
next()
|
|
53
77
|
})
|
|
54
|
-
.use((err, req, res, _next) => {
|
|
55
|
-
if (req.tokenInfo) {
|
|
56
|
-
LOG?.debug('error during token validation', req.tokenInfo.getErrorObject())
|
|
57
|
-
}
|
|
58
|
-
// REVISIT: reject request immediately as our other auth strategies do
|
|
59
|
-
// should we call next(err)? -> I don't think so; it's not an error, is it?
|
|
60
|
-
res.status(401).json({ code: '401', message: 'Unauthorized' }) // REVISIT: this is OData style?
|
|
61
|
-
})
|
|
62
78
|
}
|
package/lib/auth/mocked-users.js
CHANGED
|
@@ -10,19 +10,6 @@ class MockedUsers {
|
|
|
10
10
|
if (typeof v === 'boolean') continue
|
|
11
11
|
if (typeof v === 'string') v = { password:v }
|
|
12
12
|
let id = _configured(v).id || k
|
|
13
|
-
|
|
14
|
-
// Only for mock users the pseudo roles are kept in the role list.
|
|
15
|
-
// In all other cases pseudo roles are filtered out.
|
|
16
|
-
if (v.roles) {
|
|
17
|
-
if (Array.isArray(v.roles)) {
|
|
18
|
-
if (v.roles.includes('system-user')) v._is_system = true
|
|
19
|
-
if (v.roles.includes('internal-user')) v._is_internal = true
|
|
20
|
-
} else {
|
|
21
|
-
if ('system-user' in v.roles) v._is_system = true
|
|
22
|
-
if ('internal-user' in v.roles) v._is_internal = true
|
|
23
|
-
}
|
|
24
|
-
} else v.roles = []
|
|
25
|
-
|
|
26
13
|
let u = users[id] = new User ({ id, ...v })
|
|
27
14
|
let fts = tenants[u.tenant]?.features
|
|
28
15
|
if (fts && !u.features) u.features = fts
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
// REVISIT: either document passport basic auth or remove it
|
|
2
|
+
|
|
1
3
|
/* eslint-disable cds/no-missing-dependencies */
|
|
2
4
|
module.exports = function passport_basic_auth (options) {
|
|
3
5
|
// const session = require('express-session')({ secret:'secret', resave:false, saveUninitialized:true, })
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
// REVISIT: either document passport digest auth or remove it
|
|
2
|
+
|
|
1
3
|
/* eslint-disable cds/no-missing-dependencies */
|
|
2
4
|
module.exports = function passport_digest_auth (options) {
|
|
3
5
|
// const session = require('express-session')({ secret:'secret', resave:false, saveUninitialized:true, })
|
package/lib/compile/extend.js
CHANGED
|
@@ -3,15 +3,31 @@ const { extend } = require ('../lazy')
|
|
|
3
3
|
|
|
4
4
|
module.exports = o => o.definitions ? { with(...csns) {
|
|
5
5
|
|
|
6
|
+
// merge all extension csns
|
|
6
7
|
const csn=o, merged = { definitions: {}, extensions: [] }
|
|
7
8
|
for (const { definitions, extensions } of csns) {
|
|
8
9
|
if (definitions) Object.assign(merged.definitions, definitions)
|
|
9
10
|
if (extensions) merged.extensions.push(...extensions)
|
|
10
11
|
}
|
|
12
|
+
|
|
13
|
+
// extend given base csn with merged extensions
|
|
11
14
|
const extended = compile({
|
|
12
15
|
'base.csn': compile.to.json(csn),
|
|
13
16
|
'ext.csn': compile.to.json(merged)
|
|
14
17
|
})
|
|
18
|
+
|
|
19
|
+
// handle localized extension elements
|
|
20
|
+
for (let ext of merged.extensions) {
|
|
21
|
+
for (let name in ext.elements) {
|
|
22
|
+
const e = ext.elements[name]
|
|
23
|
+
if (e.localized) {
|
|
24
|
+
// add localized element also to respective .texts entity
|
|
25
|
+
const texts = extended.definitions[ext.extend+'.texts']
|
|
26
|
+
texts.elements[name] ??= { ...e, localized:null }
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
15
31
|
extended.$sources = csn.$sources // required to load resources like i18n later on
|
|
16
32
|
return extended
|
|
17
33
|
|
|
@@ -72,17 +72,49 @@ module.exports = function cds_compile_for_lean_drafts(csn) {
|
|
|
72
72
|
// Positive list would be bigger (search, requires, fiori, ...)
|
|
73
73
|
if (draft['@readonly']) draft['@readonly'] = undefined
|
|
74
74
|
if (draft['@insertonly']) draft['@insertonly'] = undefined
|
|
75
|
-
if (draft['@restrict'])
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
75
|
+
if (draft['@restrict']) {
|
|
76
|
+
const restrictions = ['CREATE', 'WRITE', '*']
|
|
77
|
+
draft['@restrict'] = draft['@restrict']
|
|
78
|
+
.map(d => ({
|
|
79
|
+
...d,
|
|
80
|
+
grant:
|
|
81
|
+
d.grant && Array.isArray(d.grant)
|
|
82
|
+
? d.grant.filter(g => restrictions.includes(g))
|
|
83
|
+
: typeof d.grant === 'string' && restrictions.includes(d.grant)
|
|
84
|
+
? [d.grant]
|
|
85
|
+
: []
|
|
86
|
+
}))
|
|
87
|
+
.filter(r => r.grant.length > 0)
|
|
88
|
+
if (draft['@restrict'].length > 0) {
|
|
89
|
+
// Change WRITE & CREATE to NEW
|
|
90
|
+
draft['@restrict'] = draft['@restrict'].map(d => {
|
|
91
|
+
if (d.grant.includes('WRITE') || d.grant.includes('CREATE')) {
|
|
92
|
+
return { ...d, grant: 'NEW' }
|
|
93
|
+
}
|
|
94
|
+
return d
|
|
95
|
+
})
|
|
96
|
+
} else {
|
|
97
|
+
draft['@restrict'] = undefined
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
if ('@Capabilities.DeleteRestrictions.Deletable' in draft)
|
|
101
|
+
draft['@Capabilities.DeleteRestrictions.Deletable'] = undefined
|
|
102
|
+
if ('@Capabilities.InsertRestrictions.Insertable' in draft)
|
|
103
|
+
draft['@Capabilities.InsertRestrictions.Insertable'] = undefined
|
|
104
|
+
if ('@Capabilities.UpdateRestrictions.Updatable' in draft)
|
|
105
|
+
draft['@Capabilities.UpdateRestrictions.Updatable'] = undefined
|
|
106
|
+
if ('@Capabilities.NavigationRestrictions.RestrictedProperties' in draft)
|
|
107
|
+
draft['@Capabilities.NavigationRestrictions.RestrictedProperties'] = undefined
|
|
80
108
|
|
|
81
109
|
// Recursively add drafts for compositions
|
|
82
110
|
for (const each in draft.elements) {
|
|
83
111
|
const e = draft.elements[each]
|
|
84
112
|
const newEl = Object.create(e)
|
|
85
|
-
if (
|
|
113
|
+
if (
|
|
114
|
+
e.isComposition ||
|
|
115
|
+
(e.isAssociation && e['@odata.draft.enclosed']) ||
|
|
116
|
+
((!active['@Common.DraftRoot.ActivationAction'] || e._target === active) && _isCompositionBacklink(e) && _isDraft(e._target))
|
|
117
|
+
) {
|
|
86
118
|
if (e._target['@odata.draft.enabled'] === false) continue // happens for texts if @fiori.draft.enabled is not set
|
|
87
119
|
_redirect(newEl, addDraftEntity(e._target, model))
|
|
88
120
|
}
|
package/lib/compile/resolve.js
CHANGED
|
@@ -14,7 +14,6 @@ const suffixes = [ '.csn', '.cds', sep+'index.csn', sep+'index.cds', sep+'csn.js
|
|
|
14
14
|
* @returns and array of absolute filenames
|
|
15
15
|
*/
|
|
16
16
|
module.exports = exports = function cds_resolve (model, o={}) { // NOSONAR
|
|
17
|
-
|
|
18
17
|
if (!model || model === '--') return
|
|
19
18
|
if (model._resolved) return model
|
|
20
19
|
if (model === '*') return _resolve_all(o,this)
|
|
@@ -25,7 +24,7 @@ module.exports = exports = function cds_resolve (model, o={}) { // NOSONAR
|
|
|
25
24
|
if (model.endsWith('/*')) return _resolve_subdirs_in(model,o,this)
|
|
26
25
|
|
|
27
26
|
const cwd = o.root || this.root, local = resolve (cwd,model)
|
|
28
|
-
const context = _paths(cwd,o), {cached} = context
|
|
27
|
+
const context = _paths(cwd,o,this), {cached} = context
|
|
29
28
|
let id = model.startsWith('.') ? local : model
|
|
30
29
|
if (id in cached) return cached[id]
|
|
31
30
|
|
|
@@ -93,11 +92,14 @@ function _resolve_subdirs_in (pattern='fts/*',o,cds) {
|
|
|
93
92
|
}
|
|
94
93
|
}
|
|
95
94
|
|
|
96
|
-
function _paths (dir,o) {
|
|
95
|
+
function _paths (dir,o,cds) {
|
|
97
96
|
const cache = o.cache || exports.cache
|
|
98
97
|
const cached = cache[dir]; if (cached) return cached
|
|
99
|
-
const a = dir.split(sep), n = a.length,
|
|
100
|
-
const
|
|
98
|
+
const a = dir.split(sep), n = a.length, paths = [ dir ]
|
|
99
|
+
const { cdsc: { moduleLookupDirectories }} = o.env ?? cds.env
|
|
100
|
+
for (const mld of moduleLookupDirectories) { // node_modules/ usually, more for Java
|
|
101
|
+
paths.push(...a.map ((_,i,a)=> a.slice(0,n-i).join(sep)+sep+mld))
|
|
102
|
+
}
|
|
101
103
|
return cache[dir] = { paths, cached:{} }
|
|
102
104
|
}
|
|
103
105
|
|
package/lib/compile/to/json.js
CHANGED
|
@@ -4,6 +4,7 @@ const path = require('path')
|
|
|
4
4
|
module.exports = (csn,o={}) => {
|
|
5
5
|
const relative = filename => (o.src !== o.cwd) ? path.relative(o.src, path.join(o.cwd, filename)) : filename
|
|
6
6
|
const relative_cds_home = RegExp ('^' + path.relative (o.src || o.cwd || cds.root, cds.home) + '/')
|
|
7
|
+
const { moduleLookupDirectories } = cds.env.cdsc
|
|
7
8
|
|
|
8
9
|
const resolver = (_,v) => {
|
|
9
10
|
|
|
@@ -20,9 +21,12 @@ module.exports = (csn,o={}) => {
|
|
|
20
21
|
// Preserve original sources for services so we can use them for finding
|
|
21
22
|
// sibling implementation files when reloaded from csn.json.
|
|
22
23
|
let file = relative(v.$location.file)
|
|
23
|
-
.replace(relative_cds_home,'@sap/cds/')
|
|
24
|
-
.replace('node_modules/','')
|
|
25
24
|
.replace(/\\/g,'/')
|
|
25
|
+
.replace(relative_cds_home,'@sap/cds/')
|
|
26
|
+
for (const mld of moduleLookupDirectories) { // node_modules/ usually, more for Java
|
|
27
|
+
file = file.replace(mld, '')
|
|
28
|
+
}
|
|
29
|
+
|
|
26
30
|
// If there is still a relative path pointing outside of cwd, convert it to a module path
|
|
27
31
|
// e.g. ../bookshop/srv/cat-service.cds -> @capire/bookshop/srv/cat-service.cds
|
|
28
32
|
if (file.startsWith('../')) {
|