@sap/cds 7.1.1 → 7.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (84) hide show
  1. package/CHANGELOG.md +68 -4
  2. package/apis/cds.d.ts +10 -6
  3. package/apis/connect.d.ts +0 -1
  4. package/apis/core.d.ts +54 -5
  5. package/apis/log.d.ts +19 -6
  6. package/apis/models.d.ts +0 -18
  7. package/apis/ql.d.ts +23 -23
  8. package/apis/serve.d.ts +17 -14
  9. package/apis/services.d.ts +40 -29
  10. package/apis/test.d.ts +1 -2
  11. package/bin/serve.js +4 -4
  12. package/lib/auth/basic-auth.js +1 -1
  13. package/lib/auth/dummy-auth.js +2 -1
  14. package/lib/auth/ias-auth.js +68 -2
  15. package/lib/auth/index.js +5 -5
  16. package/lib/auth/jwt-auth.js +40 -24
  17. package/lib/auth/mocked-users.js +0 -13
  18. package/lib/auth/passport-basic.js +2 -0
  19. package/lib/auth/passport-digest.js +2 -0
  20. package/lib/compile/etc/_localized.js +0 -1
  21. package/lib/compile/extend.js +16 -0
  22. package/lib/compile/for/lean_drafts.js +38 -6
  23. package/lib/compile/resolve.js +7 -5
  24. package/lib/compile/to/json.js +6 -2
  25. package/lib/dbs/cds-deploy.js +4 -4
  26. package/lib/env/cds-env.js +3 -3
  27. package/lib/env/cds-requires.js +1 -0
  28. package/lib/env/defaults.js +8 -1
  29. package/lib/env/schemas/cds-rc.json +27 -3
  30. package/lib/i18n/localize.js +3 -3
  31. package/lib/index.js +4 -0
  32. package/lib/log/cds-log.js +10 -1
  33. package/lib/ql/Whereable.js +7 -3
  34. package/lib/req/user.js +18 -16
  35. package/lib/srv/middlewares/sap-statistics.js +3 -3
  36. package/lib/srv/middlewares/trace.js +5 -4
  37. package/lib/srv/srv-dispatch.js +10 -9
  38. package/lib/utils/axios.js +3 -0
  39. package/lib/utils/cds-test.js +3 -0
  40. package/lib/utils/cds-utils.js +2 -0
  41. package/libx/_runtime/auth/index.js +8 -32
  42. package/libx/_runtime/auth/strategies/ias-auth.js +1 -77
  43. package/libx/_runtime/auth/strategies/mock.js +1 -12
  44. package/libx/_runtime/auth/strategies/xssecUtils.js +2 -2
  45. package/libx/_runtime/cds-services/adapter/odata-v4/ODataRequest.js +3 -1
  46. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/request.js +11 -9
  47. package/libx/_runtime/cds-services/adapter/odata-v4/odata-to-cqn/ExpressionToCQN.js +5 -0
  48. package/libx/_runtime/cds-services/adapter/odata-v4/odata-to-cqn/applyToCQN.js +5 -2
  49. package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-commons/uri/UriParser.js +4 -0
  50. package/libx/_runtime/cds-services/services/utils/compareJson.js +0 -9
  51. package/libx/_runtime/cds-services/services/utils/differ.js +8 -10
  52. package/libx/_runtime/common/composition/data.js +10 -7
  53. package/libx/_runtime/common/composition/insert.js +9 -5
  54. package/libx/_runtime/common/composition/update.js +18 -12
  55. package/libx/_runtime/common/error/constants.js +6 -1
  56. package/libx/_runtime/common/generic/auth/requires.js +11 -3
  57. package/libx/_runtime/common/generic/auth/restrict.js +22 -16
  58. package/libx/_runtime/common/generic/auth/restrictions.js +5 -2
  59. package/libx/_runtime/common/generic/crud.js +6 -0
  60. package/libx/_runtime/common/generic/paging.js +2 -1
  61. package/libx/_runtime/common/utils/cqn2cqn4sql.js +3 -5
  62. package/libx/_runtime/common/utils/resolveView.js +3 -1
  63. package/libx/_runtime/common/utils/restrictions.js +47 -0
  64. package/libx/_runtime/db/data-conversion/post-processing.js +3 -3
  65. package/libx/_runtime/db/generic/input.js +1 -1
  66. package/libx/_runtime/db/sql-builder/ExpressionBuilder.js +0 -17
  67. package/libx/_runtime/db/utils/coloredTxCommands.js +5 -3
  68. package/libx/_runtime/fiori/lean-draft.js +24 -19
  69. package/libx/_runtime/hana/driver.js +2 -4
  70. package/libx/_runtime/hana/pool.js +53 -57
  71. package/libx/_runtime/messaging/enterprise-messaging-utils/registerEndpoints.js +2 -2
  72. package/libx/_runtime/messaging/outbox/utils.js +1 -2
  73. package/libx/_runtime/remote/utils/client.js +1 -1
  74. package/libx/_runtime/sqlite/Service.js +0 -4
  75. package/libx/_runtime/sqlite/customBuilder/CustomFunctionBuilder.js +2 -1
  76. package/libx/odata/afterburner.js +6 -4
  77. package/libx/odata/cqn2odata.js +7 -7
  78. package/libx/odata/utils.js +4 -1
  79. package/libx/rest/RestAdapter.js +15 -16
  80. package/package.json +1 -1
  81. package/lib/auth/xsuaa-auth.js +0 -2
  82. package/libx/_runtime/auth/utils.js +0 -32
  83. package/libx/audit-log/client.cds +0 -0
  84. package/libx/audit-log/client.js +0 -0
@@ -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: Events; data?: object; headers?: object }): Promise<T>
170
- <T = any>(event: Events, data?: object, headers?: object): Promise<T>
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: Events, path: string, data?: object, headers?: object): Promise<T>
179
- <T = any>(event: Events, data?: object, headers?: object): Promise<T>
180
- <T = any>(details: { event: Events; data?: object; headers?: object }): Promise<T>
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: Event; path: string; data?: object; headers?: object }): Promise<T>
183
- <T = any>(details: { event: Event; entity: Definition | string; data?: object; params?: object }): Promise<T>
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: EventMessage): Promise<any>
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: Events, entity: T, handler: CRUDEventHandler.On<InstanceType<T>, InstanceType<T> | void | Error>): this
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: Events, entity: Target, handler: OnEventHandler): this
224
- on(eve: Events, handler: OnEventHandler): this
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: Events, entity: T, handler: CRUDEventHandler.Before<InstanceType<T>, InstanceType<T> | void | Error>): this
232
- before(eve: Events, entity: Target, handler: EventHandler): this
233
- before(eve: Events, handler: EventHandler): this
234
- after<T extends Constructable>(eve: Events, entity: T, handler: CRUDEventHandler.After<InstanceType<T>, InstanceType<T> | void | Error>): this
235
- after(eve: Events, entity: Target, handler: ResultsHandler): this
236
- after(eve: Events, handler: ResultsHandler): this
237
- reject(eves: Events, ...entity: Target[]): this
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
- export class cds {
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 Service
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 Service
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 Service
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
- interface EventContext {
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
- interface EventMessage extends EventContext {
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
- interface Request extends EventMessage {
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
- type Events = Event | Event[]
418
- type Event = (CRUD | TX | HTTP | DRAFT) | (CustomOp & {})
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 main_cds;
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 sqlite driver to be installed. For example: _npm i sqlite3_.
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 flagse...
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('cds').error('❗️Uncaught',e)))
210
- process.on('uncaughtException', e => _shutdown (e, cds.log('cds').error('❗️Uncaught',e)))
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)
@@ -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
@@ -1,4 +1,5 @@
1
- const { User: { privileged }} = require ('../index')
1
+ const { User: { privileged } } = require ('../index')
2
+
2
3
  module.exports = function dummy_auth() {
3
4
  return function dummy_auth (req, res, next) {
4
5
  req.user = privileged
@@ -1,2 +1,68 @@
1
- module.exports = require('../../libx/_runtime/auth/strategies/ias-auth')
2
- // TODO: could move that implementation over here and link from libx/_runtime
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
- cds.log().info ('using auth strategy:', { kind }, '\n')
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 auth strategy:', { kind, impl: local(impl) }, '\n')
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)) {
@@ -1,41 +1,64 @@
1
1
  const cds = require('../')
2
- const _require = require('../../libx/_runtime/common/utils/require')
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
- LOG._warn &&
13
- LOG.warn(`Authentication kind "${config.kind}" configured, but no XSUAA instance bound to application.
14
- This is NOT recommended in production!`)
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(config.kind, new JWTStrategy(config.credentials))
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
- let roles = payload.scope.map(s => s.replace(new RegExp(`^(${config.credentials.xsappname + '.'})`), ''))
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
- const CLIENT = { client_credentials: 1, client_x509: 1 }
35
- if (payload.grant_type in CLIENT) {
58
+ if (payload.grant_type in { client_credentials: 1, client_x509: 1 }) {
36
59
  id = 'system'
37
- _is_system = true
38
- if (req.tokenInfo.getClientId() === config.credentials.clientid) _is_internal = true
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, _is_system, _is_internal })
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
  }
@@ -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, })
@@ -2,7 +2,6 @@ const cds = require('../..'), {env} = cds
2
2
  const DEBUG = cds.debug('alpha|_localized')
3
3
  const _locales_4sql = {
4
4
  sqlite : env.i18n.for_sqlite || env.i18n.for_sql || [],
5
- h2 : env.i18n.for_sql || [],
6
5
  plain : env.i18n.for_sql || [],
7
6
  }
8
7
 
@@ -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']) draft['@restrict'] = undefined
76
- if ('@Capabilities.DeleteRestrictions.Deletable' in draft) draft['@Capabilities.DeleteRestrictions.Deletable'] = undefined
77
- if ('@Capabilities.InsertRestrictions.Insertable' in draft) draft['@Capabilities.InsertRestrictions.Insertable'] = undefined
78
- if ('@Capabilities.UpdateRestrictions.Updatable' in draft) draft['@Capabilities.UpdateRestrictions.Updatable'] = undefined
79
- if ('@Capabilities.NavigationRestrictions.RestrictedProperties' in draft) draft['@Capabilities.NavigationRestrictions.RestrictedProperties'] = undefined
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 (e.isComposition || (e.isAssociation && e['@odata.draft.enclosed']) || ((!active['@Common.DraftRoot.ActivationAction'] || e._target === active) && _isCompositionBacklink(e))) {
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
  }
@@ -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, nm = sep+'node_modules'
100
- const paths = [ dir, ...a.map ((_,i,a)=> a.slice(0,n-i).join(sep)+nm) ]
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
 
@@ -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('../')) {