@sap/cds 5.8.4 → 5.9.2

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 (248) hide show
  1. package/CHANGELOG.md +198 -77
  2. package/app/fiori/preview.js +16 -11
  3. package/app/fiori/routes.js +15 -8
  4. package/app/index.js +1 -1
  5. package/bin/build/buildTaskFactory.js +3 -3
  6. package/bin/build/buildTaskProviderFactory.js +1 -1
  7. package/bin/build/constants.js +1 -1
  8. package/bin/build/provider/buildTaskHandlerEdmx.js +12 -7
  9. package/bin/build/provider/buildTaskHandlerInternal.js +1 -1
  10. package/bin/build/provider/buildTaskProviderInternal.js +8 -2
  11. package/bin/build/provider/hana/2migration.js +27 -24
  12. package/bin/build/provider/hana/index.js +17 -18
  13. package/bin/build/provider/hana/migrationtable.js +9 -10
  14. package/bin/build/provider/java-cf/index.js +4 -5
  15. package/bin/build/provider/node-cf/index.js +99 -6
  16. package/bin/cds.js +17 -18
  17. package/bin/deploy/to-hana/cfUtil.js +16 -19
  18. package/bin/deploy/to-hana/hana.js +7 -24
  19. package/bin/deploy/to-hana/hdiDeployUtil.js +8 -4
  20. package/bin/mtx/in-cds.js +2 -2
  21. package/bin/serve.js +10 -3
  22. package/bin/utils/modules.js +7 -0
  23. package/bin/version.js +56 -3
  24. package/lib/compile/cdsc.js +7 -2
  25. package/lib/compile/etc/_localized.js +37 -25
  26. package/lib/compile/etc/csv.js +8 -8
  27. package/lib/compile/for/drafts.js +9 -0
  28. package/lib/compile/for/java.js +16 -0
  29. package/lib/compile/for/nodejs.js +12 -0
  30. package/lib/compile/index.js +3 -0
  31. package/lib/compile/minify.js +16 -2
  32. package/lib/compile/parse.js +2 -2
  33. package/lib/compile/resolve.js +35 -18
  34. package/lib/compile/to/json.js +3 -1
  35. package/lib/compile/to/sql.js +2 -2
  36. package/lib/compile/to/srvinfo.js +4 -2
  37. package/lib/connect/bindings.js +1 -1
  38. package/lib/connect/index.js +3 -4
  39. package/lib/core/entities.js +15 -14
  40. package/lib/core/index.js +39 -36
  41. package/lib/core/reflect.js +4 -2
  42. package/lib/deploy.js +114 -127
  43. package/lib/env/defaults.js +1 -0
  44. package/lib/env/index.js +165 -165
  45. package/lib/env/presets.js +1 -0
  46. package/lib/env/requires.js +121 -50
  47. package/lib/index.js +2 -0
  48. package/lib/log/format/kibana.js +2 -2
  49. package/lib/ql/SELECT.js +10 -0
  50. package/lib/ql/parse.js +1 -0
  51. package/lib/req/cds-context.js +4 -1
  52. package/lib/req/context.js +50 -56
  53. package/lib/req/event.js +1 -6
  54. package/lib/req/locale.js +6 -5
  55. package/lib/req/request.js +2 -0
  56. package/lib/req/user.js +7 -5
  57. package/lib/serve/Service-api.js +10 -7
  58. package/lib/serve/Service-dispatch.js +9 -11
  59. package/lib/serve/Service-methods.js +30 -41
  60. package/lib/serve/Transaction.js +10 -7
  61. package/lib/serve/adapters.js +11 -9
  62. package/lib/serve/factory.js +14 -9
  63. package/lib/serve/index.js +28 -15
  64. package/lib/utils/data.js +1 -1
  65. package/lib/utils/index.js +27 -30
  66. package/lib/utils/resources/index.js +101 -0
  67. package/lib/utils/resources/tar.js +71 -0
  68. package/lib/utils/resources/utils.js +11 -0
  69. package/libx/_runtime/audit/Service.js +36 -39
  70. package/libx/_runtime/audit/generic/personal/access.js +3 -4
  71. package/libx/_runtime/audit/generic/personal/modification.js +3 -4
  72. package/libx/_runtime/audit/utils/v2.js +1 -2
  73. package/libx/_runtime/auth/index.js +126 -84
  74. package/libx/_runtime/auth/strategies/JWT.js +12 -19
  75. package/libx/_runtime/auth/strategies/dummy.js +1 -5
  76. package/libx/_runtime/auth/strategies/dwc.js +11 -9
  77. package/libx/_runtime/auth/strategies/mock.js +0 -4
  78. package/libx/_runtime/auth/strategies/{utils/xssec.js → xssecUtils.js} +7 -4
  79. package/libx/_runtime/auth/strategies/xsuaa.js +12 -19
  80. package/libx/_runtime/auth/utils.js +22 -1
  81. package/libx/_runtime/cds-services/adapter/odata-v4/Dispatcher.js +104 -98
  82. package/libx/_runtime/cds-services/adapter/odata-v4/OData.js +8 -3
  83. package/libx/_runtime/cds-services/adapter/odata-v4/ODataRequest.js +1 -1
  84. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/action.js +1 -1
  85. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/language.js +2 -8
  86. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/metadata.js +4 -29
  87. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/read.js +2 -1
  88. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/request.js +3 -2
  89. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/update.js +2 -2
  90. package/libx/_runtime/cds-services/adapter/odata-v4/odata-to-cqn/ExpressionToCQN.js +4 -6
  91. package/libx/_runtime/cds-services/adapter/odata-v4/odata-to-cqn/expandToCQN.js +24 -21
  92. package/libx/_runtime/cds-services/adapter/odata-v4/odata-to-cqn/index.js +8 -2
  93. package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-server/deserializer/DeserializerFactory.js +2 -0
  94. package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-server/invocation/DispatcherCommand.js +2 -6
  95. package/libx/_runtime/cds-services/adapter/odata-v4/to.js +1 -12
  96. package/libx/_runtime/cds-services/adapter/odata-v4/utils/data.js +33 -9
  97. package/libx/_runtime/cds-services/adapter/odata-v4/utils/dispatcherUtils.js +56 -0
  98. package/libx/_runtime/cds-services/adapter/odata-v4/utils/readAfterWrite.js +2 -2
  99. package/libx/_runtime/cds-services/adapter/odata-v4/utils/request.js +10 -3
  100. package/libx/_runtime/cds-services/adapter/odata-v4/utils/result.js +9 -11
  101. package/libx/_runtime/cds-services/adapter/rest/RestRequest.js +6 -3
  102. package/libx/_runtime/cds-services/adapter/rest/handlers/operation.js +4 -2
  103. package/libx/_runtime/cds-services/adapter/rest/rest-to-cqn/utils.js +1 -1
  104. package/libx/_runtime/cds-services/adapter/rest/utils/binary.js +1 -1
  105. package/libx/_runtime/cds-services/adapter/rest/utils/key-value-utils.js +2 -3
  106. package/libx/_runtime/cds-services/adapter/rest/utils/parse-url.js +6 -4
  107. package/libx/_runtime/cds-services/adapter/rest/utils/result.js +1 -0
  108. package/libx/_runtime/cds-services/adapter/rest/utils/validation-checks.js +8 -5
  109. package/libx/_runtime/cds-services/services/Service.js +40 -0
  110. package/libx/_runtime/cds-services/services/utils/columns.js +4 -3
  111. package/libx/_runtime/cds-services/services/utils/compareJson.js +4 -4
  112. package/libx/_runtime/cds-services/services/utils/differ.js +3 -3
  113. package/libx/_runtime/cds-services/services/utils/handlerUtils.js +4 -4
  114. package/libx/_runtime/cds-services/util/assert.js +20 -14
  115. package/libx/_runtime/cds.js +9 -1
  116. package/libx/_runtime/common/aspects/any.js +5 -0
  117. package/libx/_runtime/common/aspects/entity.js +25 -7
  118. package/libx/_runtime/common/aspects/utils.js +2 -2
  119. package/libx/_runtime/common/composition/data.js +6 -0
  120. package/libx/_runtime/common/composition/insert.js +3 -2
  121. package/libx/_runtime/common/composition/tree.js +4 -10
  122. package/libx/_runtime/common/composition/update.js +4 -4
  123. package/libx/_runtime/common/constants/draft.js +29 -26
  124. package/libx/_runtime/common/error/constants.js +2 -2
  125. package/libx/_runtime/common/error/frontend.js +7 -15
  126. package/libx/_runtime/common/generic/auth/capabilities.js +59 -0
  127. package/libx/_runtime/common/generic/auth/constants.js +20 -0
  128. package/libx/_runtime/common/generic/auth/expand.js +54 -0
  129. package/libx/_runtime/common/generic/auth/index.js +32 -0
  130. package/libx/_runtime/common/generic/auth/insertOnly.js +15 -0
  131. package/libx/_runtime/common/generic/auth/readOnly.js +26 -0
  132. package/libx/_runtime/common/generic/auth/requires.js +34 -0
  133. package/libx/_runtime/common/generic/auth/restrict.js +298 -0
  134. package/libx/_runtime/common/generic/auth/restrictions.js +85 -0
  135. package/libx/_runtime/common/generic/auth/utils.js +213 -0
  136. package/libx/_runtime/common/generic/crud.js +8 -6
  137. package/libx/_runtime/common/generic/etag.js +1 -1
  138. package/libx/_runtime/common/generic/input.js +35 -35
  139. package/libx/_runtime/common/generic/sorting.js +2 -3
  140. package/libx/_runtime/common/generic/temporal.js +2 -2
  141. package/libx/_runtime/common/i18n/messages.properties +1 -1
  142. package/libx/_runtime/common/toggles/handler.js +21 -0
  143. package/libx/_runtime/common/utils/copy.js +10 -1
  144. package/libx/_runtime/common/utils/cqn2cqn4sql.js +111 -35
  145. package/libx/_runtime/common/utils/csn.js +63 -1
  146. package/libx/_runtime/common/utils/dollar.js +10 -1
  147. package/libx/_runtime/common/utils/draft.js +46 -7
  148. package/libx/_runtime/common/utils/entityFromCqn.js +13 -9
  149. package/libx/_runtime/common/utils/extensibilityUtils.js +18 -0
  150. package/libx/_runtime/common/utils/foreignKeyPropagations.js +88 -104
  151. package/libx/_runtime/common/utils/generateOnCond.js +4 -1
  152. package/libx/_runtime/common/utils/quotingStyles.js +2 -0
  153. package/libx/_runtime/common/utils/resolveStructured.js +25 -9
  154. package/libx/_runtime/common/utils/resolveView.js +4 -1
  155. package/libx/_runtime/common/utils/rewriteAsterisks.js +3 -16
  156. package/libx/_runtime/common/utils/structured.js +33 -37
  157. package/libx/_runtime/common/utils/template.js +17 -8
  158. package/libx/_runtime/common/utils/templateProcessor.js +28 -28
  159. package/libx/_runtime/db/data-conversion/post-processing.js +118 -412
  160. package/libx/_runtime/db/expand/expandCQNToJoin.js +45 -41
  161. package/libx/_runtime/db/expand/rawToExpanded.js +29 -8
  162. package/libx/_runtime/db/generic/index.js +1 -3
  163. package/libx/_runtime/db/generic/input.js +5 -10
  164. package/libx/_runtime/db/generic/rewrite.js +5 -2
  165. package/libx/_runtime/db/generic/structured.js +2 -2
  166. package/libx/_runtime/db/query/delete.js +2 -2
  167. package/libx/_runtime/db/query/insert.js +1 -1
  168. package/libx/_runtime/db/query/update.js +9 -14
  169. package/libx/_runtime/db/sql-builder/CreateBuilder.js +4 -3
  170. package/libx/_runtime/db/sql-builder/FunctionBuilder.js +8 -8
  171. package/libx/_runtime/db/sql-builder/InsertBuilder.js +14 -1
  172. package/libx/_runtime/db/sql-builder/SelectBuilder.js +3 -2
  173. package/libx/_runtime/db/sql-builder/dataTypes.js +3 -3
  174. package/libx/_runtime/db/utils/columns.js +3 -3
  175. package/libx/_runtime/db/utils/normalizeTimeData.js +2 -2
  176. package/libx/_runtime/db/utils/propagateForeignKeys.js +6 -2
  177. package/libx/_runtime/extensibility/mps/index.js +5 -0
  178. package/libx/_runtime/extensibility/mps/service.js +111 -0
  179. package/libx/_runtime/extensibility/mps/tar.js +42 -0
  180. package/libx/_runtime/extensibility/mps/utils.js +11 -0
  181. package/libx/_runtime/{fiori → extensibility}/uiflex/handler/transformREAD.js +0 -0
  182. package/libx/_runtime/{fiori → extensibility}/uiflex/handler/transformRESULT.js +17 -5
  183. package/libx/_runtime/{fiori → extensibility}/uiflex/handler/transformWRITE.js +1 -0
  184. package/libx/_runtime/extensibility/uiflex/index.js +54 -0
  185. package/libx/_runtime/extensibility/uiflex/service.js +276 -0
  186. package/libx/_runtime/{fiori → extensibility}/uiflex/utils.js +22 -7
  187. package/libx/_runtime/fiori/generic/activate.js +2 -2
  188. package/libx/_runtime/fiori/generic/before.js +4 -4
  189. package/libx/_runtime/fiori/generic/new.js +3 -3
  190. package/libx/_runtime/fiori/generic/patch.js +1 -1
  191. package/libx/_runtime/fiori/generic/read.js +58 -66
  192. package/libx/_runtime/fiori/generic/readOverDraft.js +74 -16
  193. package/libx/_runtime/fiori/utils/handler.js +6 -13
  194. package/libx/_runtime/fiori/utils/where.js +6 -5
  195. package/libx/_runtime/hana/Service.js +4 -10
  196. package/libx/_runtime/hana/customBuilder/CustomSelectBuilder.js +1 -1
  197. package/libx/_runtime/hana/driver.js +2 -2
  198. package/libx/_runtime/hana/execute.js +45 -75
  199. package/libx/_runtime/hana/pool.js +1 -1
  200. package/libx/_runtime/hana/streaming.js +2 -1
  201. package/libx/_runtime/index.js +6 -6
  202. package/libx/_runtime/messaging/AMQPWebhookMessaging.js +5 -21
  203. package/libx/_runtime/messaging/Outbox.js +2 -2
  204. package/libx/_runtime/messaging/common-utils/AMQPClient.js +4 -14
  205. package/libx/_runtime/messaging/common-utils/connections.js +5 -7
  206. package/libx/_runtime/messaging/common-utils/normalizeIncomingMessage.js +30 -0
  207. package/libx/_runtime/messaging/enterprise-messaging-shared.js +2 -1
  208. package/libx/_runtime/messaging/enterprise-messaging-utils/EMManagement.js +36 -30
  209. package/libx/_runtime/messaging/enterprise-messaging-utils/registerEndpoints.js +19 -12
  210. package/libx/_runtime/messaging/enterprise-messaging.js +8 -8
  211. package/libx/_runtime/messaging/file-based.js +5 -5
  212. package/libx/_runtime/messaging/message-queuing.js +14 -12
  213. package/libx/_runtime/messaging/outbox/utils.js +18 -19
  214. package/libx/_runtime/messaging/redis-messaging.js +91 -0
  215. package/libx/_runtime/messaging/service.js +8 -6
  216. package/libx/_runtime/remote/Service.js +44 -8
  217. package/libx/_runtime/remote/utils/client.js +24 -19
  218. package/libx/_runtime/remote/utils/data.js +11 -11
  219. package/libx/_runtime/sqlite/Service.js +6 -9
  220. package/libx/_runtime/sqlite/customBuilder/CustomFunctionBuilder.js +5 -2
  221. package/libx/_runtime/types/api.js +10 -2
  222. package/libx/common/utils/ucsn.js +109 -0
  223. package/libx/gql/resolvers/crud/update.js +5 -0
  224. package/libx/gql/resolvers/parse/ast2cqn/columns.js +3 -1
  225. package/libx/gql/schema/typeDefMap.js +2 -2
  226. package/libx/odata/afterburner.js +110 -16
  227. package/libx/odata/cqn2odata.js +24 -27
  228. package/libx/odata/grammar.pegjs +9 -1
  229. package/libx/odata/parseToCqn.js +39 -0
  230. package/libx/odata/parser.js +1 -1
  231. package/libx/rest/RestAdapter.js +9 -1
  232. package/libx/rest/middleware/input.js +54 -0
  233. package/libx/rest/middleware/operation.js +14 -1
  234. package/libx/rest/middleware/parse.js +11 -7
  235. package/package.json +2 -2
  236. package/server.js +34 -19
  237. package/srv/audit-log.cds +2 -2
  238. package/srv/flex.cds +8 -2
  239. package/srv/flex.js +1 -1
  240. package/srv/mps.cds +23 -0
  241. package/srv/mps.js +1 -0
  242. package/libx/_runtime/auth/strategies/utils/uaa.js +0 -21
  243. package/libx/_runtime/common/generic/auth.js +0 -874
  244. package/libx/_runtime/common/toggles/alpha.js +0 -43
  245. package/libx/_runtime/db/generic/arrayed.js +0 -33
  246. package/libx/_runtime/fiori/uiflex/index.js +0 -35
  247. package/libx/_runtime/fiori/uiflex/service.js +0 -150
  248. package/libx/rest/utils/data.js +0 -60
@@ -1,874 +0,0 @@
1
- /* istanbul ignore file */
2
- /* eslint-disable max-len */
3
- /* eslint-disable no-new-wrappers */
4
-
5
- const cds = require('../../cds')
6
- const { SELECT } = cds.ql
7
-
8
- const { getRequiresAsArray } = require('../../auth/utils')
9
- const { cqn2cqn4sql } = require('../utils/cqn2cqn4sql')
10
- const { isActiveEntityRequested, removeIsActiveEntityRecursively } = require('../../fiori/utils/where')
11
- const { ensureNoDraftsSuffix } = require('../../fiori/utils/handler')
12
- const { rewriteExpandAsterisk } = require('../../common/utils/rewriteAsterisks')
13
-
14
- const WRITE = ['CREATE', 'UPDATE', 'DELETE']
15
- const MOD = { UPDATE: 1, DELETE: 1, EDIT: 1 }
16
- const WRITE_EVENTS = { CREATE: 1, NEW: 1, UPDATE: 1, PATCH: 1, DELETE: 1, CANCEL: 1, EDIT: 1 }
17
- const DRAFT_EVENTS = { PATCH: 1, CANCEL: 1, draftActivate: 1, draftPrepare: 1 }
18
- const DRAFT2CRUD = { NEW: 'CREATE', EDIT: 'UPDATE' }
19
- const ODATA_DRAFT_ENABLED = '@odata.draft.enabled'
20
- const FIORI_DRAFT_ENABLED = '@fiori.draft.enabled'
21
-
22
- const RESTRICTIONS = {
23
- READABLE: 'ReadRestrictions.Readable',
24
- READABLE_BY_KEY: 'ReadRestrictions.ReadByKeyRestrictions.Readable',
25
- INSERTABLE: 'InsertRestrictions.Insertable',
26
- UPDATABLE: 'UpdateRestrictions.Updatable',
27
- DELETABLE: 'DeleteRestrictions.Deletable'
28
- }
29
-
30
- const _reject = req => {
31
- // unauthorized or forbidden?
32
- if (req.user._is_anonymous) {
33
- // REVISIT: improve `req._.req` check if this is an HTTP request
34
- if (req._.req && req.user._challenges && req.user._challenges.length > 0) {
35
- req._.res.set('WWW-Authenticate', req.user._challenges.join(';'))
36
- }
37
- // REVISIT: security log in else case?
38
- return req.reject(401)
39
- } else {
40
- // REVISIT: security log?
41
- return req.reject(403)
42
- }
43
- }
44
-
45
- const _getTarget = (ref, target, definitions) => {
46
- if (cds.env.effective.odata.proxies) {
47
- const target_ = target.elements[ref[0]]
48
-
49
- if (ref.length === 1) {
50
- return definitions[ensureNoDraftsSuffix(target_.target)]
51
- }
52
-
53
- return _getTarget(ref.slice(1), target_, definitions)
54
- }
55
-
56
- const target_ = target.elements[ref.join('_')]
57
- return definitions[ensureNoDraftsSuffix(target_.target)]
58
- }
59
-
60
- const _getRestrictedExpand = (columns, target, definitions) => {
61
- if (!columns || !target || columns === '*') return
62
-
63
- const annotation = target['@Capabilities.ExpandRestrictions.NonExpandableProperties']
64
- const restrictions = annotation && annotation.map(element => element['='])
65
-
66
- rewriteExpandAsterisk(columns, target)
67
-
68
- for (const col of columns) {
69
- if (col.expand) {
70
- if (restrictions && restrictions.length !== 0) {
71
- const ref = col.ref.join('_')
72
- const ref_ = restrictions.find(element => element.replace(/\./g, '_') === ref)
73
- if (ref_) return ref_
74
- }
75
-
76
- // expand: '**' or '*3' is only possible within custom handler, no check needed
77
- if (typeof col.expand === 'string' && /^\*{1}[\d|*]+/.test(col.expand)) {
78
- continue
79
- } else {
80
- const restricted = _getRestrictedExpand(col.expand, _getTarget(col.ref, target, definitions), definitions)
81
- if (restricted) return restricted
82
- }
83
- }
84
- }
85
- }
86
-
87
- const _getCurrentSubClause = (next, restrict) => {
88
- const escaped = next[0].replace(/\$/g, '\\$').replace(/\./g, '\\.')
89
- const re1 = new RegExp(`([\\w\\.']*)\\s*=\\s*(${escaped})|(${escaped})\\s*=\\s*([\\w\\.']*)`)
90
- const re2 = new RegExp(`([\\w\\.']*)\\s*in\\s*(${escaped})|(${escaped})\\s*in\\s*([\\w\\.']*)`)
91
- const clause = restrict.where.match(re1) || restrict.where.match(re2)
92
- if (!clause) {
93
- // NOTE: arrayed attr with "=" as operator is some kind of legacy case
94
- throw new Error('user attribute array must be used with operator "=" or "in"')
95
- }
96
- return clause
97
- }
98
-
99
- const _processUserAttr = (next, restrict, user, attr) => {
100
- const clause = _getCurrentSubClause(next, restrict)
101
- const valOrRef = clause[1] || clause[4]
102
- if (clause[0].match(/ in /)) {
103
- if (!user[attr] || user[attr].length === 0) {
104
- restrict.where = restrict.where.replace(clause[0], '1 = 2')
105
- } else if (user[attr].length === 1) {
106
- restrict.where = restrict.where.replace(clause[0], `${valOrRef} = '${user[attr][0]}'`)
107
- } else {
108
- restrict.where = restrict.where.replace(
109
- clause[0],
110
- `${valOrRef} in (${user[attr].map(ele => `'${ele}'`).join(', ')})`
111
- )
112
- }
113
- } else if (valOrRef.startsWith("'") && user[attr].includes(valOrRef.split("'")[1])) {
114
- restrict.where = restrict.where.replace(clause[0], `${valOrRef} = ${valOrRef}`)
115
- } else {
116
- restrict.where = restrict.where.replace(
117
- clause[0],
118
- `(${user[attr].map(ele => `${valOrRef} = '${ele}'`).join(' or ')})`
119
- )
120
- }
121
- }
122
-
123
- const _getShortcut = (attrs, attr) => {
124
- // undefined
125
- if (attrs[attr] === undefined) {
126
- return '1 = 2'
127
- }
128
-
129
- // $UNRESTRICTED
130
- if (
131
- (typeof attrs[attr] === 'string' && attrs[attr].match(/\$UNRESTRICTED/i)) ||
132
- (Array.isArray(attrs[attr]) && attrs[attr].some(a => a.match(/\$UNRESTRICTED/i)))
133
- ) {
134
- return '1 = 1'
135
- }
136
-
137
- return null
138
- }
139
-
140
- /*
141
- * for supporting xssec v3
142
- */
143
- const _getAttrsAsProxy = (attrs, additional = {}) => {
144
- return new Proxy(
145
- {},
146
- {
147
- get: function (_, attr) {
148
- if (attr in additional) return additional[attr]
149
- return attrs[attr]
150
- }
151
- }
152
- )
153
- }
154
-
155
- /*
156
- * resolves user attributes deeply, even though nested attributes are officially not supported
157
- */
158
- const _resolveUserAttrs = (restrict, req) => {
159
- const _getNext = where => where.match(/\$user\.([\w.]*)/)
160
-
161
- let next = _getNext(restrict.where)
162
- while (next !== null) {
163
- const parts = next[1].split('.')
164
-
165
- let skip
166
- let val
167
- let attrs = _getAttrsAsProxy(req.user.attr, { id: req.user.id })
168
- let attr = parts.shift()
169
- while (attr) {
170
- const shortcut = _getShortcut(attrs, attr)
171
- if (shortcut) {
172
- const clause = _getCurrentSubClause(next, restrict)
173
- restrict.where = restrict.where.replace(clause[0], shortcut)
174
- skip = true
175
- break
176
- }
177
-
178
- if (Array.isArray(attrs[attr])) {
179
- _processUserAttr(next, restrict, attrs, attr)
180
- skip = true
181
- break
182
- }
183
-
184
- val = !Number.isNaN(Number(attrs[attr])) && attr !== 'id' ? attrs[attr] : `'${attrs[attr]}'`
185
- if (val === null || val === undefined) break
186
-
187
- attrs = _getAttrsAsProxy(attrs[attr])
188
- attr = parts.shift()
189
- }
190
- if (!skip) restrict.where = restrict.where.replace(next[0], val === undefined ? null : val)
191
-
192
- next = _getNext(restrict.where)
193
- }
194
-
195
- return restrict
196
- }
197
-
198
- const _evalStatic = (op, vals) => {
199
- vals[0] = Number.isNaN(Number(vals[0])) ? vals[0] : Number(vals[0])
200
- vals[1] = Number.isNaN(Number(vals[1])) ? vals[1] : Number(vals[1])
201
-
202
- switch (op) {
203
- case '=':
204
- return vals[0] === vals[1]
205
- case '!=':
206
- return vals[0] !== vals[1]
207
- case '<':
208
- return vals[0] < vals[1]
209
- case '<=':
210
- return vals[0] <= vals[1]
211
- case '>':
212
- return vals[0] > vals[1]
213
- case '>=':
214
- return vals[0] >= vals[1]
215
- default:
216
- throw new Error(`Operator "${op}" is not supported in @restrict.where`)
217
- }
218
- }
219
-
220
- const _getMergedWhere = restricts => {
221
- const xprs = []
222
- restricts.forEach(ele => {
223
- xprs.push('(', ...ele._xpr, ')', 'or')
224
- })
225
- xprs.pop()
226
- return xprs
227
- }
228
-
229
- const _getApplicables = (restricts, req) => {
230
- return restricts.filter(restrict => {
231
- const event = DRAFT2CRUD[req.event] || req.event
232
- return (restrict.grant === '*' || restrict.grant === event) && restrict.to.some(role => req.user.is(role))
233
- })
234
- }
235
-
236
- const _getResolvedApplicables = (applicables, req) => {
237
- const resolvedApplicables = []
238
-
239
- // REVISIT: the static portion of "mixed wheres" could already grant access -> optimization potential
240
- for (const restrict of applicables) {
241
- // replace $user.x with respective values
242
- const resolved = _resolveUserAttrs({ grant: restrict.grant, target: restrict.target, where: restrict.where }, req)
243
-
244
- // check for duplicates
245
- if (
246
- !resolvedApplicables.find(
247
- restrict =>
248
- resolved.grant === restrict.grant &&
249
- (!resolved.target || resolved.target === restrict.target) &&
250
- (!resolved.where || resolved.where === restrict.where)
251
- )
252
- ) {
253
- if (resolved.where) resolved._xpr = cds.parse.expr(resolved.where).xpr
254
- resolvedApplicables.push(resolved)
255
- }
256
- }
257
-
258
- return resolvedApplicables
259
- }
260
-
261
- const _isStaticAuth = resolvedApplicables => {
262
- return (
263
- resolvedApplicables.length === 1 &&
264
- resolvedApplicables[0]._xpr.length === 3 &&
265
- resolvedApplicables[0]._xpr.every(ele => typeof ele !== 'object' || ele.val)
266
- )
267
- }
268
-
269
- const _handleStaticAuth = (resolvedApplicables, req) => {
270
- const op = resolvedApplicables[0]._xpr.find(ele => typeof ele === 'string')
271
- const vals = resolvedApplicables[0]._xpr.filter(ele => typeof ele === 'object' && ele.val).map(ele => ele.val)
272
- if (!_evalStatic(op, vals)) {
273
- // static clause forbids access => forbidden
274
- return _reject(req)
275
- }
276
- // static clause grants access => done
277
- }
278
-
279
- const _getFromWithIsActiveEntityRemoved = from => {
280
- for (const element of from.ref) {
281
- if (element.where && isActiveEntityRequested(element.where)) {
282
- element.where = removeIsActiveEntityRecursively(element.where)
283
- }
284
- }
285
- return from
286
- }
287
-
288
- const _addWheresToRef = (ref, model, resolvedApplicables) => {
289
- const newRef = []
290
- let lastEntity = model.definitions[ref[0].id || ref[0]]
291
- ref.forEach((identifier, idx) => {
292
- if (idx === ref.length - 1) {
293
- newRef.push(identifier)
294
- return // determine last one separately
295
- }
296
- const entity = idx === 0 ? lastEntity : lastEntity.elements[identifier.id || identifier]._target
297
- lastEntity = entity
298
- const applicablesForEntity = resolvedApplicables.filter(
299
- restrict => restrict.target && restrict.target.name === entity.name
300
- )
301
- let newIdentifier = identifier
302
- if (applicablesForEntity.length) {
303
- if (typeof newIdentifier === 'string') {
304
- newIdentifier = { id: identifier, where: [] }
305
- }
306
- if (!newIdentifier.where) newIdentifier.where = []
307
- if (newIdentifier.where && newIdentifier.where.length) {
308
- newIdentifier.where.unshift('(')
309
- newIdentifier.where.push(')')
310
- newIdentifier.where.push('and')
311
- }
312
- newIdentifier.where.push(..._getMergedWhere(applicablesForEntity))
313
- }
314
- newRef.push(newIdentifier)
315
- })
316
- return newRef
317
- }
318
-
319
- const _getRestrictionForTarget = (resolvedApplicables, target) => {
320
- const reqTarget = target && (target[ODATA_DRAFT_ENABLED] ? target.name.replace(/_drafts$/, '') : target.name)
321
- const applicablesForTarget = resolvedApplicables.filter(
322
- restrict => restrict.target && restrict.target.name === reqTarget
323
- )
324
- if (applicablesForTarget.length) {
325
- return _getMergedWhere(applicablesForTarget)
326
- }
327
- }
328
-
329
- const _addRestrictionsToRead = async (req, model, resolvedApplicables) => {
330
- if (req.target._isDraftEnabled) {
331
- req.query._draftRestrictions = resolvedApplicables
332
- return
333
- }
334
-
335
- if (typeof req.query.SELECT.from === 'object')
336
- req.query.SELECT.from.ref = _addWheresToRef(req.query.SELECT.from.ref, model, resolvedApplicables)
337
-
338
- const restrictionForTarget = _getRestrictionForTarget(resolvedApplicables, req.target)
339
- if (restrictionForTarget) {
340
- // adjust free subselects, if necessary
341
- if (resolvedApplicables.some(ra => ra.where.match(/\s*exists\s*\(\s*select\s*1\s*/i))) {
342
- for (const ele of restrictionForTarget) {
343
- if (typeof ele !== 'object' || !ele.SELECT || !ele.SELECT.where) continue
344
- for (const w of ele.SELECT.where) {
345
- if (w.ref && w.ref.length > 2) {
346
- let path = w.ref[0]
347
- if (!model.definitions[path]) continue
348
- let i = 1
349
- for (; i < w.ref.length; i++) {
350
- if (model.definitions[`${path}.${w.ref[i]}`]) path += `.${w.ref[i]}`
351
- else break
352
- }
353
- w.ref = [path, ...w.ref.slice(i)]
354
- }
355
- }
356
- }
357
- }
358
- // apply restriction
359
- req.query.where(restrictionForTarget)
360
- }
361
- }
362
-
363
- const _getUnrestrictedCount = async req => {
364
- const dbtx = cds.tx(req)
365
-
366
- const target =
367
- (req.query.UPDATE && req.query.UPDATE.entity) ||
368
- (req.query.DELETE && req.query.DELETE.from) ||
369
- (req.query.SELECT && req.query.SELECT.from)
370
- const selectUnrestricted = SELECT.one(['count(*) as n']).from(target)
371
-
372
- const whereUnrestricted = (req.query.UPDATE && req.query.UPDATE.where) || (req.query.DELETE && req.query.DELETE.where)
373
- if (whereUnrestricted) selectUnrestricted.where(whereUnrestricted)
374
-
375
- // Because of side effects, the statements have to be fired sequentially.
376
- const { n } = await dbtx.run(selectUnrestricted)
377
- return n
378
- }
379
-
380
- const _getRestrictedCount = async (req, model, resolvedApplicables) => {
381
- const dbtx = cds.tx(req)
382
-
383
- const target =
384
- (req.query.UPDATE && req.query.UPDATE.entity) ||
385
- (req.query.DELETE && req.query.DELETE.from) ||
386
- (req.query.SELECT && req.query.SELECT.from)
387
-
388
- const selectRestricted = SELECT.one(['count(*) as n']).from(target)
389
-
390
- const whereRestricted = (req.query.UPDATE && req.query.UPDATE.where) || (req.query.DELETE && req.query.DELETE.where)
391
- if (whereRestricted) selectRestricted.where(whereRestricted)
392
-
393
- if (typeof selectRestricted.SELECT === 'object')
394
- selectRestricted.SELECT.from.ref = _addWheresToRef(selectRestricted.SELECT.from.ref, model, resolvedApplicables)
395
-
396
- const restrictionForTarget = _getRestrictionForTarget(resolvedApplicables, req.target)
397
- if (restrictionForTarget) selectRestricted.where(restrictionForTarget)
398
-
399
- const { n } = await dbtx.run(cqn2cqn4sql(selectRestricted, model, { suppressSearch: true }))
400
- return n
401
- }
402
-
403
- const _getRestrictsHandler = (restricts, definition, model) => {
404
- const bounds = Object.keys(definition.actions || {})
405
- const onlyBoundsAreRestricted = restricts.every(restrict => bounds.includes(restrict.grant))
406
-
407
- const handler = async function (req) {
408
- if (req.user._is_privileged || DRAFT_EVENTS[req.event]) {
409
- // > skip checks (events in DRAFT_EVENTS are checked in draft handlers via InProcessByUser)
410
- return
411
- }
412
-
413
- if (!bounds.includes(req.event) && onlyBoundsAreRestricted) {
414
- // no @restrict on entity level => done
415
- return
416
- }
417
-
418
- const applicables = _getApplicables(restricts, req)
419
-
420
- if (applicables.length === 0) {
421
- // no @restrict for req.event with the user's roles => forbidden
422
- return _reject(req)
423
- }
424
-
425
- if (applicables.some(restrict => !restrict.where)) {
426
- // at least one if the user's roles grants unrestricted access => done
427
- return
428
- }
429
-
430
- const resolvedApplicables = _getResolvedApplicables(applicables, req)
431
-
432
- // REVISIT: support more complex statics
433
- if (_isStaticAuth(resolvedApplicables)) {
434
- return _handleStaticAuth(resolvedApplicables, req)
435
- }
436
-
437
- // REVISIT: remove feature flag skip_restrict_where after grace period of at least two months (> April release)
438
- if (cds.env.features.skip_restrict_where === false) {
439
- if (req.event !== 'READ' && !MOD[req.event]) {
440
- // REVISIT: security log?
441
- req.reject({
442
- code: 403,
443
- internal: {
444
- reason: `Only static @restrict.where allowed for event "${req.event}"`,
445
- source: `@restrict.where of ${definition.name}`
446
- }
447
- })
448
- }
449
- }
450
-
451
- if (req.event === 'READ') {
452
- _addRestrictionsToRead(req, model, resolvedApplicables)
453
- return
454
- }
455
-
456
- if (!MOD[req.event]) {
457
- // no modification -> nothing more to do
458
- return
459
- }
460
-
461
- if (req.query.DELETE) req.query.DELETE.from = _getFromWithIsActiveEntityRemoved(req.query.DELETE.from)
462
- if (req.query.SELECT) req.query.SELECT.from = _getFromWithIsActiveEntityRemoved(req.query.SELECT.from)
463
-
464
- // REVISIT: selected data could be used for etag check, diff, etc.
465
-
466
- /*
467
- * Here we check if UPDATE/DELETE requests add additional restrictions
468
- * Note: Needs to happen sequentially because of side effects
469
- */
470
- const unrestrictedCount = await _getUnrestrictedCount(req)
471
- if (unrestrictedCount === 0) req.reject(404)
472
-
473
- const restrictedCount = await _getRestrictedCount(req, model, resolvedApplicables)
474
- if (restrictedCount < unrestrictedCount) {
475
- // REVISIT: security log?
476
- req.reject({
477
- code: 403,
478
- internal: {
479
- reason: `@restrict results in ${restrictedCount} affected rows out of ${unrestrictedCount}`,
480
- source: `@restrict.where of ${definition.name}`
481
- }
482
- })
483
- }
484
-
485
- // for minor optimization in generic crud handler
486
- req._authChecked = true
487
- }
488
-
489
- handler._initial = true
490
-
491
- return handler
492
- }
493
-
494
- const _getLocalName = definition => {
495
- return definition._service ? definition.name.replace(`${definition._service.name}.`, '') : definition.name
496
- }
497
-
498
- const _getRestrictWithEventRewrite = (grant, to, where, target) => {
499
- // REVISIT: req.event should be 'SAVE' and 'PREPARE'
500
- if (grant === 'SAVE') grant = 'draftActivate'
501
- else if (grant === 'PREPARE') grant = 'draftPrepare'
502
- return { grant, to, where, target }
503
- }
504
-
505
- const _addNormalizedRestrictPerGrant = (grant, where, restrict, restricts, definition) => {
506
- const to = restrict.to ? (Array.isArray(restrict.to) ? restrict.to : [restrict.to]) : ['any']
507
- if (definition.kind === 'entity') {
508
- if (grant === 'WRITE') {
509
- WRITE.forEach(g => {
510
- restricts.push(_getRestrictWithEventRewrite(g, to, where, definition))
511
- })
512
- } else {
513
- restricts.push(_getRestrictWithEventRewrite(grant, to, where, definition))
514
- }
515
- } else {
516
- restricts.push({ grant: _getLocalName(definition), to, where, target: definition.parent })
517
- }
518
- }
519
-
520
- const _addNormalizedRestrict = (restrict, restricts, definition, definitions) => {
521
- const where = restrict.where
522
- ? restrict.where.replace(/\$user/g, '$user.id').replace(/\$user\.id\./g, '$user.')
523
- : undefined
524
- restrict.grant = Array.isArray(restrict.grant) ? restrict.grant : [restrict.grant || '*']
525
- restrict.grant.forEach(grant => _addNormalizedRestrictPerGrant(grant, where, restrict, restricts, definition))
526
- }
527
-
528
- const _getNormalizedRestricts = (definition, definitions) => {
529
- const restricts = []
530
-
531
- // own
532
- definition['@restrict'] &&
533
- definition['@restrict'].forEach(restrict => _addNormalizedRestrict(restrict, restricts, definition, definitions))
534
-
535
- // bounds
536
- if (definition.actions && Object.keys(definition.actions).some(k => definition.actions[k]['@restrict'])) {
537
- for (const k in definition.actions) {
538
- const action = definition.actions[k]
539
- if (action['@restrict']) {
540
- restricts.push(..._getNormalizedRestricts(action, definitions))
541
- } else if (!definition['@restrict']) {
542
- // > no entity-level restrictions => unrestricted action
543
- restricts.push({ grant: action.name, to: ['any'], target: action.parent })
544
- }
545
- }
546
- }
547
-
548
- return restricts
549
- }
550
-
551
- const _cqnFrom = req => {
552
- const { query } = req
553
- if (!query) return
554
- if (query.SELECT) return query.SELECT.from
555
- if (query.INSERT) return query.INSERT.into
556
- if (query.UPDATE) return query.UPDATE.entity
557
- if (query.DELETE) return query.DELETE.from
558
- }
559
-
560
- const _forPath = ({ model }, mainEntity, intermediateEntities, handler) => {
561
- // eslint-disable-next-line complexity
562
- const _isEntityRequested = cqn => {
563
- if (!cqn) return
564
- if (!cqn.ref || !Array.isArray(cqn.ref) || cqn.name)
565
- return cqn.ref === mainEntity || cqn.name === mainEntity || cqn === mainEntity
566
- // Special case for drafts, as compositions are directly accessed
567
- if (cqn.ref.length === 1) return true
568
- let targetName = cqn.ref[0].id || cqn.ref[0]
569
- if (targetName === mainEntity) return true
570
- let element
571
- // no need to look at first and last segments
572
- for (const seg of cqn.ref.slice(1, -1)) {
573
- const csn = targetName ? model.definitions[targetName] : element && (element.items || element)
574
- if (csn) {
575
- element = csn.elements && csn.elements[seg.id || seg]
576
- targetName = element && (element.target || element.type || (element.items && element.items.type))
577
- if (!targetName && !element) return
578
- if (targetName === mainEntity) return true
579
- if (!intermediateEntities.includes(targetName)) return
580
- }
581
- }
582
- }
583
- return req => _isEntityRequested(_cqnFrom(req)) && handler(req)
584
- }
585
-
586
- const _getRequiresHandler = requires => {
587
- const handler = function (req) {
588
- return !requires.some(role => req.user.is(role)) && _reject(req)
589
- }
590
- handler._initial = true
591
- return handler
592
- }
593
-
594
- const _registerEntityRequiresHandlers = (entity, srv, { dependentEntity, intermediateEntities } = {}) => {
595
- // own
596
- const requires = getRequiresAsArray(entity)
597
- if (requires.length > 0) {
598
- if (dependentEntity)
599
- srv.before('*', dependentEntity, _forPath(srv, entity.name, intermediateEntities, _getRequiresHandler(requires)))
600
- else srv.before('*', entity, _getRequiresHandler(requires))
601
- }
602
-
603
- // bounds
604
- if (!dependentEntity && entity.actions && Object.keys(entity.actions).some(k => entity.actions[k]['@requires'])) {
605
- for (const k in entity.actions) {
606
- const requires = getRequiresAsArray(entity.actions[k])
607
- if (requires.length > 0) {
608
- srv.before(k, entity, _getRequiresHandler(requires))
609
- }
610
- }
611
- }
612
- }
613
-
614
- const _registerEntityRestrictHandlers = (entity, srv, { dependentEntity, intermediateEntities } = {}) => {
615
- if (entity['@restrict'] || entity.actions) {
616
- const restricts = _getNormalizedRestricts(entity, srv.model.definitions)
617
- if (restricts.length > 0) {
618
- if (dependentEntity)
619
- srv.before(
620
- '*',
621
- dependentEntity,
622
- _forPath(srv, entity.name, intermediateEntities, _getRestrictsHandler(restricts, entity, srv.model))
623
- )
624
- else srv.before('*', entity, _getRestrictsHandler(restricts, entity, srv.model))
625
- }
626
- }
627
- }
628
-
629
- const _registerOperationRequiresHandlers = (operation, srv) => {
630
- const requires = getRequiresAsArray(operation)
631
- if (requires.length > 0) {
632
- srv.before(_getLocalName(operation), _getRequiresHandler(requires))
633
- }
634
- }
635
-
636
- const _registerOperationRestrictHandlers = (operation, srv) => {
637
- if (operation['@restrict']) {
638
- const restricts = _getNormalizedRestricts(operation, srv.model.definitions)
639
- if (restricts.length > 0) {
640
- srv.before(_getLocalName(operation), _getRestrictsHandler(restricts, operation, srv.model))
641
- }
642
- }
643
- }
644
-
645
- const _registerRejectsForReadonly = (entity, srv, { dependentEntity, intermediateEntities } = {}) => {
646
- const handler = function (req) {
647
- // @read-only (-> C_UD events not allowed but actions and functions are)
648
- if (entity._isReadOnly) {
649
- if (WRITE_EVENTS[req.event]) req.reject(405, 'ENTITY_IS_READ_ONLY', [entity.name])
650
- return
651
- }
652
- // autoexposed
653
- if (req.event !== 'READ') req.reject(405, 'ENTITY_IS_AUTOEXPOSED', [entity.name])
654
- }
655
- handler._initial = true
656
-
657
- // According to documentation, @cds.autoexposed + @cds.autoexpose entities are readonly.
658
- if (
659
- entity._isReadOnly ||
660
- (entity['@cds.autoexpose'] && entity['@cds.autoexposed']) ||
661
- entity.name.match(/\.DraftAdministrativeData$/)
662
- ) {
663
- // registering check for '*' makes the check future proof
664
- if (dependentEntity) srv.before('*', dependentEntity, _forPath(srv, entity.name, intermediateEntities, handler))
665
- else srv.before('*', entity, handler)
666
- }
667
- }
668
-
669
- const _registerRejectsForInsertonly = (entity, srv, { dependentEntity, intermediateEntities } = {}) => {
670
- const allowed = entity[ODATA_DRAFT_ENABLED] ? ['NEW', 'PATCH'] : ['CREATE']
671
- const handler = function (req) {
672
- return !allowed.includes(req.event) && req.reject(405, 'ENTITY_IS_INSERT_ONLY', [entity.name])
673
- }
674
- handler._initial = true
675
-
676
- if (entity['@insertonly']) {
677
- // registering check for '*' makes the check future proof
678
- if (dependentEntity) srv.before('*', dependentEntity, _forPath(srv, entity.name, intermediateEntities, handler))
679
- else srv.before('*', entity, handler)
680
- }
681
- }
682
-
683
- const _getCapabilitiesHandler = (entity, annotation, srv) => {
684
- const action = annotation.split('.').pop().toUpperCase()
685
- const _localName = entity => entity.name.replace(entity._service.name + '.', '')
686
-
687
- const _isRestricted = (req, capability, capabilityReadByKey) => {
688
- if (capabilityReadByKey !== undefined && req.query.SELECT.one) {
689
- return capabilityReadByKey === false
690
- }
691
- return capability === false
692
- }
693
-
694
- const _isNavigationRestricted = (target, path, req) => {
695
- if (!target) return
696
- const parts = annotation.split('.')
697
- if (target && Array.isArray(target['@Capabilities.NavigationRestrictions.RestrictedProperties'])) {
698
- for (const r of target['@Capabilities.NavigationRestrictions.RestrictedProperties']) {
699
- if (r.NavigationProperty['='] === path && r[parts[0]]) {
700
- return _isRestricted(
701
- req,
702
- r[parts[0]][parts[1]],
703
- r.ReadRestrictions && r.ReadRestrictions['ReadByKeyRestrictions.Readable']
704
- )
705
- }
706
- }
707
- }
708
- }
709
-
710
- const handler = function (req) {
711
- const from = _cqnFrom(req)
712
- const nav = (from && from.ref && from.ref.map(el => el.id || el)) || []
713
-
714
- if (nav.length > 1) {
715
- const path = nav.slice(1).join('.')
716
- const target = srv.model.definitions[nav[0]]
717
- if (_isNavigationRestricted(target, path, req)) {
718
- // REVISIT: rework exception with using target
719
- const trgt = `${_localName(target)}.${path}`
720
- req.reject(405, 'ENTITY_IS_NOT_CRUD_VIA_NAVIGATION', [_localName(entity), action, trgt])
721
- }
722
- } else if (
723
- _isRestricted(req, entity['@Capabilities.' + annotation], entity['@Capabilities.' + RESTRICTIONS.READABLE_BY_KEY])
724
- ) {
725
- req.reject(405, 'ENTITY_IS_NOT_CRUD', [_localName(entity), action])
726
- }
727
- }
728
-
729
- handler._initial = true
730
-
731
- return handler
732
- }
733
-
734
- const _authDependsOnParents = entity => {
735
- return entity['@cds.autoexposed'] && !entity['@cds.autoexpose']
736
- }
737
-
738
- const _traverseChildren = (srv, parentEntityDef, traversedEntities = []) => {
739
- if (traversedEntities.includes(parentEntityDef.name)) return // recursive compositions are handled in path filter
740
- traversedEntities.push(parentEntityDef.name)
741
-
742
- // We only need to look at compositions as only those can be autoexposed (without autoexpose)
743
- const children = Object.keys(parentEntityDef.compositions || {}).map(c => parentEntityDef.compositions[c])
744
-
745
- children
746
- .map(c => srv.model.definitions[c.target])
747
- .filter(t => _authDependsOnParents(t))
748
- .forEach(t => _traverseChildren(srv, t, traversedEntities))
749
-
750
- return traversedEntities
751
- }
752
-
753
- const _registerRejectsForCapabilities = (entity, srv, { dependentEntity, intermediateEntities } = {}) => {
754
- if (dependentEntity) {
755
- srv.before(
756
- 'CREATE',
757
- dependentEntity,
758
- _forPath(srv, entity.name, intermediateEntities, _getCapabilitiesHandler(entity, RESTRICTIONS.INSERTABLE, srv))
759
- )
760
- srv.before(
761
- 'READ',
762
- dependentEntity,
763
- _forPath(srv, entity.name, intermediateEntities, _getCapabilitiesHandler(entity, RESTRICTIONS.READABLE, srv))
764
- )
765
- srv.before(
766
- 'UPDATE',
767
- dependentEntity,
768
- _forPath(srv, entity.name, intermediateEntities, _getCapabilitiesHandler(entity, RESTRICTIONS.UPDATABLE, srv))
769
- )
770
- srv.before(
771
- 'DELETE',
772
- dependentEntity,
773
- _forPath(srv, entity.name, intermediateEntities, _getCapabilitiesHandler(entity, RESTRICTIONS.DELETABLE, srv))
774
- )
775
- } else {
776
- srv.before('CREATE', entity, _getCapabilitiesHandler(entity, RESTRICTIONS.INSERTABLE, srv))
777
- srv.before('READ', entity, _getCapabilitiesHandler(entity, RESTRICTIONS.READABLE, srv))
778
- srv.before('UPDATE', entity, _getCapabilitiesHandler(entity, RESTRICTIONS.UPDATABLE, srv))
779
- srv.before('DELETE', entity, _getCapabilitiesHandler(entity, RESTRICTIONS.DELETABLE, srv))
780
- }
781
- }
782
-
783
- const _registerAuthHandlers = (entity, srv, opts) => {
784
- // REVISIT: switch order? access control checks should be cheaper than authorization checks...
785
-
786
- // @requires (own and bounds)
787
- _registerEntityRequiresHandlers(entity, srv, opts)
788
-
789
- // @restrict (own and bounds)
790
- _registerEntityRestrictHandlers(entity, srv, opts)
791
-
792
- // @readonly (incl. DraftAdministrativeData by default)
793
- _registerRejectsForReadonly(entity, srv, opts)
794
-
795
- // @insertonly
796
- _registerRejectsForInsertonly(entity, srv, opts)
797
-
798
- // @Capabilities
799
- _registerRejectsForCapabilities(entity, srv, opts)
800
- }
801
-
802
- // REVISIT: What's missing here is the special draft case.
803
- // Fiori Elements accesses draft compositions via top level access, e.g.
804
- // PATCH SalesOrdersHeaders(ID=....,IsActiveEntity=false)
805
- // as opposed to
806
- // PATCH SalesOrders(ID=...,IsActiveEntity=false)/SalesOrdersHeaders
807
- // therefore we must make sure to restrict direct access.
808
- // Note: As the parent information is lost, we cannot support
809
- // authorization checks in case one entity has several parents (in draft)
810
-
811
- /*
812
- * Algorithm as follows:
813
- * 1) Determine traversedEntities, these are the auth-dependent entities which
814
- * can be accessed through the auth root (without having auth entities in-between).
815
- * Example: Foo/bar/baz -> traversedEntities = [bar, baz]
816
- * ^^^
817
- * auth root
818
- * 2) Register auth handlers for every traversed entity (with settings from auth root)
819
- * Each of those auth handlers has an additional path restriction (`_forPath`).
820
- * The path restriction checks that the segments always either include the
821
- * traversed entities or the auth root, beginning from the last segment until the auth root
822
- * (or only the traversed entities in case auf direct access).
823
- */
824
- const _secureDependentEntities = srv => {
825
- const entities = Object.keys(srv.model.definitions)
826
- .map(n => srv.model.definitions[n])
827
- .filter(d => d.kind === 'entity' && !_authDependsOnParents(d))
828
-
829
- for (const e of entities) {
830
- const traversedEntities = _traverseChildren(srv, e)
831
- const [, ...intermediateEntities] = traversedEntities
832
- for (const entity of traversedEntities) {
833
- if (entity === e.name) continue // no need to secure auth root
834
- _registerAuthHandlers(e, srv, { dependentEntity: entity, intermediateEntities })
835
- }
836
- if (e[ODATA_DRAFT_ENABLED] || e[FIORI_DRAFT_ENABLED])
837
- _registerAuthHandlers(e, srv, { dependentEntity: 'DraftAdministrativeData', intermediateEntities })
838
- }
839
- }
840
-
841
- const _restrictExpand = service => {
842
- service.on('READ', '*', (req, next) => {
843
- const restricted = _getRestrictedExpand(
844
- req.query.SELECT && req.query.SELECT.columns,
845
- req.target,
846
- service.model.definitions
847
- )
848
- if (restricted) {
849
- return req.reject(400, 'EXPAND_IS_RESTRICTED', [restricted])
850
- }
851
- return next()
852
- })
853
- }
854
-
855
- module.exports = cds.service.impl(function () {
856
- // @restrict, @requires, @readonly, @insertonly, and @Capabilities for entities
857
- _secureDependentEntities(this)
858
- _restrictExpand(this)
859
- for (const k in this.entities) {
860
- const entity = this.entities[k]
861
- if (!_authDependsOnParents(entity)) _registerAuthHandlers(entity, this)
862
- }
863
-
864
- // @restrict and @requires for operations
865
- for (const k in this.operations) {
866
- const operation = this.operations[k]
867
-
868
- // @requires
869
- _registerOperationRequiresHandlers(operation, this)
870
-
871
- // @restrict
872
- _registerOperationRestrictHandlers(operation, this)
873
- }
874
- })