@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
@@ -0,0 +1,26 @@
1
+ const { getAuthRelevantEntity } = require('./utils')
2
+ const { WRITE_EVENTS } = require('./constants')
3
+
4
+ const _isAutoexposed = entity => {
5
+ if (!entity) return false
6
+ return (entity['@cds.autoexpose'] && entity['@cds.autoexposed']) || entity.name.match(/\.DraftAdministrativeData$/)
7
+ }
8
+
9
+ function handler(req) {
10
+ // autoexposed
11
+ const isAutoexposed = _isAutoexposed(req.target)
12
+ if (isAutoexposed && req.event !== 'READ') {
13
+ req.reject(405, 'ENTITY_IS_AUTOEXPOSED', [req.target.name])
14
+ }
15
+
16
+ // @read-only
17
+ const entity = getAuthRelevantEntity(req, this.model, ['@readonly'])
18
+ if (!entity || !entity['@readonly']) return
19
+ if (entity['@readonly'] && req.event in WRITE_EVENTS) {
20
+ req.reject(405, 'ENTITY_IS_READ_ONLY', [entity.name])
21
+ }
22
+ }
23
+
24
+ handler._initial = true
25
+
26
+ module.exports = handler
@@ -0,0 +1,34 @@
1
+ const { reject, getRejectReason, getAuthRelevantEntity } = require('./utils')
2
+ const { CRUD_EVENTS } = require('./constants')
3
+
4
+ const { getRequiresAsArray } = require('../../../auth/utils')
5
+
6
+ function handler(req) {
7
+ if (req.user._is_privileged) {
8
+ // > skip checks
9
+ return
10
+ }
11
+
12
+ let definition
13
+ if (req.event in CRUD_EVENTS) {
14
+ // > CRUD
15
+ definition = getAuthRelevantEntity(req, this.model, ['@requires', '@restrict'])
16
+ } else if (req.target && req.target.actions) {
17
+ // > bound
18
+ definition = req.target.actions[req.event]
19
+ } else {
20
+ // > unbound
21
+ definition = this.operations[req.event]
22
+ }
23
+
24
+ if (!definition) return
25
+
26
+ const requires = getRequiresAsArray(definition)
27
+ if (!requires.length || requires.some(role => req.user.is(role))) return
28
+
29
+ reject(req, getRejectReason(req, '@requires', definition))
30
+ }
31
+
32
+ handler._initial = true
33
+
34
+ module.exports = handler
@@ -0,0 +1,298 @@
1
+ const cds = require('../../../cds')
2
+
3
+ const { reject, getRejectReason, resolveUserAttrs, getAuthRelevantEntity } = require('./utils')
4
+ const { DRAFT_EVENTS, MOD_EVENTS } = require('./constants')
5
+ const { getNormalizedPlainRestrictions } = require('./restrictions')
6
+
7
+ const { cqn2cqn4sql } = require('../../utils/cqn2cqn4sql')
8
+
9
+ const { isActiveEntityRequested, removeIsActiveEntityRecursively } = require('../../../fiori/utils/where')
10
+
11
+ const _getResolvedApplicables = (applicables, req) => {
12
+ const resolvedApplicables = []
13
+
14
+ // REVISIT: the static portion of "mixed wheres" could already grant access -> optimization potential
15
+ for (const restrict of applicables) {
16
+ // replace $user.x with respective values
17
+ const resolved = resolveUserAttrs({ grant: restrict.grant, target: restrict.target, where: restrict.where }, req)
18
+
19
+ // check for duplicates
20
+ if (
21
+ !resolvedApplicables.find(
22
+ restrict =>
23
+ resolved.grant === restrict.grant &&
24
+ (!resolved.target || resolved.target === restrict.target) &&
25
+ (!resolved.where || resolved.where === restrict.where)
26
+ )
27
+ ) {
28
+ if (resolved.where) resolved._xpr = cds.parse.expr(resolved.where).xpr
29
+ resolvedApplicables.push(resolved)
30
+ }
31
+ }
32
+
33
+ return resolvedApplicables
34
+ }
35
+
36
+ const _isStaticAuth = resolvedApplicables => {
37
+ return (
38
+ resolvedApplicables.length === 1 &&
39
+ resolvedApplicables[0]._xpr.length === 3 &&
40
+ resolvedApplicables[0]._xpr.every(ele => typeof ele !== 'object' || ele.val)
41
+ )
42
+ }
43
+
44
+ const _evalStatic = (op, vals) => {
45
+ vals[0] = Number.isNaN(Number(vals[0])) ? vals[0] : Number(vals[0])
46
+ vals[1] = Number.isNaN(Number(vals[1])) ? vals[1] : Number(vals[1])
47
+
48
+ switch (op) {
49
+ case '=':
50
+ return vals[0] === vals[1]
51
+ case '!=':
52
+ return vals[0] !== vals[1]
53
+ case '<':
54
+ return vals[0] < vals[1]
55
+ case '<=':
56
+ return vals[0] <= vals[1]
57
+ case '>':
58
+ return vals[0] > vals[1]
59
+ case '>=':
60
+ return vals[0] >= vals[1]
61
+ default:
62
+ throw new Error(`Operator "${op}" is not supported in @restrict.where`)
63
+ }
64
+ }
65
+
66
+ const _handleStaticAuth = (resolvedApplicables, req) => {
67
+ const op = resolvedApplicables[0]._xpr.find(ele => typeof ele === 'string')
68
+ const vals = resolvedApplicables[0]._xpr.filter(ele => typeof ele === 'object' && ele.val).map(ele => ele.val)
69
+
70
+ if (_evalStatic(op, vals)) {
71
+ // static clause grants access => done
72
+ return
73
+ }
74
+
75
+ // static clause forbids access => forbidden
76
+ return reject(req)
77
+ }
78
+
79
+ const _getMergedWhere = restricts => {
80
+ const xprs = []
81
+ restricts.forEach(ele => {
82
+ xprs.push('(', ...ele._xpr, ')', 'or')
83
+ })
84
+ xprs.pop()
85
+ return xprs
86
+ }
87
+
88
+ const _addWheresToRef = (ref, model, resolvedApplicables) => {
89
+ const newRef = []
90
+ let lastEntity = model.definitions[ref[0].id || ref[0]]
91
+
92
+ ref.forEach((identifier, idx) => {
93
+ if (idx === ref.length - 1) {
94
+ newRef.push(identifier)
95
+ return // determine last one separately
96
+ }
97
+
98
+ const entity = idx === 0 ? lastEntity : lastEntity.elements[identifier.id || identifier]._target
99
+ lastEntity = entity
100
+ const applicablesForEntity = resolvedApplicables.filter(
101
+ restrict => restrict.target && restrict.target.name === entity.name
102
+ )
103
+
104
+ let newIdentifier = identifier
105
+
106
+ if (applicablesForEntity.length) {
107
+ if (typeof newIdentifier === 'string') {
108
+ newIdentifier = { id: identifier, where: [] }
109
+ }
110
+
111
+ if (!newIdentifier.where) newIdentifier.where = []
112
+
113
+ if (newIdentifier.where && newIdentifier.where.length) {
114
+ newIdentifier.where.unshift('(')
115
+ newIdentifier.where.push(')')
116
+ newIdentifier.where.push('and')
117
+ }
118
+
119
+ newIdentifier.where.push(..._getMergedWhere(applicablesForEntity))
120
+ }
121
+
122
+ newRef.push(newIdentifier)
123
+ })
124
+
125
+ return newRef
126
+ }
127
+
128
+ const _getRestrictionForTarget = (resolvedApplicables, target) => {
129
+ const reqTarget = target && (target._isDraftEnabled ? target.name.replace(/_drafts$/, '') : target.name)
130
+ const applicablesForTarget = resolvedApplicables.filter(
131
+ restrict => restrict.target && restrict.target.name === reqTarget
132
+ )
133
+
134
+ if (applicablesForTarget.length) {
135
+ return _getMergedWhere(applicablesForTarget)
136
+ }
137
+ }
138
+
139
+ const _addRestrictionsToRead = async (req, model, resolvedApplicables) => {
140
+ if (req.target._isDraftEnabled) {
141
+ req.query._draftRestrictions = resolvedApplicables
142
+ return
143
+ }
144
+
145
+ if (typeof req.query.SELECT.from === 'object')
146
+ req.query.SELECT.from.ref = _addWheresToRef(req.query.SELECT.from.ref, model, resolvedApplicables)
147
+
148
+ const restrictionForTarget = _getRestrictionForTarget(resolvedApplicables, req.target)
149
+ if (!restrictionForTarget) return
150
+
151
+ // adjust free subselects, if necessary
152
+ if (resolvedApplicables.some(ra => ra.where.match(/\s*exists\s*\(\s*select\s*1\s*/i))) {
153
+ for (const ele of restrictionForTarget) {
154
+ if (typeof ele !== 'object' || !ele.SELECT || !ele.SELECT.where) continue
155
+
156
+ for (const w of ele.SELECT.where) {
157
+ if (w.ref && w.ref.length > 2) {
158
+ let path = w.ref[0]
159
+ if (!model.definitions[path]) continue
160
+ let i = 1
161
+
162
+ for (; i < w.ref.length; i++) {
163
+ if (model.definitions[`${path}.${w.ref[i]}`]) path += `.${w.ref[i]}`
164
+ else break
165
+ }
166
+
167
+ w.ref = [path, ...w.ref.slice(i)]
168
+ }
169
+ }
170
+ }
171
+ }
172
+
173
+ // apply restriction
174
+ req.query.where(restrictionForTarget)
175
+ }
176
+
177
+ const _getFromWithIsActiveEntityRemoved = from => {
178
+ for (const element of from.ref) {
179
+ if (element.where && isActiveEntityRequested(element.where)) {
180
+ element.where = removeIsActiveEntityRecursively(element.where)
181
+ }
182
+ }
183
+
184
+ return from
185
+ }
186
+
187
+ const _getUnrestrictedCount = async req => {
188
+ const dbtx = cds.tx(req)
189
+ const target =
190
+ (req.query.UPDATE && req.query.UPDATE.entity) ||
191
+ (req.query.DELETE && req.query.DELETE.from) ||
192
+ (req.query.SELECT && req.query.SELECT.from)
193
+ const selectUnrestricted = SELECT.one(['count(*) as n']).from(target)
194
+ const whereUnrestricted = (req.query.UPDATE && req.query.UPDATE.where) || (req.query.DELETE && req.query.DELETE.where)
195
+
196
+ if (whereUnrestricted) selectUnrestricted.where(whereUnrestricted)
197
+
198
+ // Because of side effects, the statements have to be fired sequentially.
199
+ const { n } = await dbtx.run(selectUnrestricted)
200
+ return n
201
+ }
202
+
203
+ const _getRestrictedCount = async (req, model, resolvedApplicables) => {
204
+ const dbtx = cds.tx(req)
205
+ const target =
206
+ (req.query.UPDATE && req.query.UPDATE.entity) ||
207
+ (req.query.DELETE && req.query.DELETE.from) ||
208
+ (req.query.SELECT && req.query.SELECT.from)
209
+ const selectRestricted = SELECT.one(['count(*) as n']).from(target)
210
+ const whereRestricted = (req.query.UPDATE && req.query.UPDATE.where) || (req.query.DELETE && req.query.DELETE.where)
211
+
212
+ if (whereRestricted) selectRestricted.where(whereRestricted)
213
+
214
+ if (typeof selectRestricted.SELECT === 'object') {
215
+ selectRestricted.SELECT.from.ref = _addWheresToRef(selectRestricted.SELECT.from.ref, model, resolvedApplicables)
216
+ }
217
+
218
+ const restrictionForTarget = _getRestrictionForTarget(resolvedApplicables, req.target)
219
+ if (restrictionForTarget) selectRestricted.where(restrictionForTarget)
220
+
221
+ const { n } = await dbtx.run(cqn2cqn4sql(selectRestricted, model, { suppressSearch: true }))
222
+ return n
223
+ }
224
+
225
+ // eslint-disable-next-line complexity
226
+ async function handler(req) {
227
+ if (req.user._is_privileged || DRAFT_EVENTS[req.event]) {
228
+ // > skip checks (events in DRAFT_EVENTS are checked in draft handlers via InProcessByUser)
229
+ return
230
+ }
231
+
232
+ const authRelevantEntity = getAuthRelevantEntity(req, this.model, ['@requires', '@restrict'])
233
+ const definition =
234
+ authRelevantEntity ||
235
+ (req.target && req.target.actions && req.target.actions[req.event]) ||
236
+ (this.operations && this.operations[req.event])
237
+
238
+ if (!definition) {
239
+ // > nothing to restrict
240
+ return
241
+ }
242
+
243
+ let restrictions = this.getRestrictions(definition, req.event, req.user)
244
+ if (restrictions instanceof Promise) restrictions = await restrictions
245
+ if (!restrictions) {
246
+ // > unrestricted
247
+ return
248
+ }
249
+
250
+ if (!restrictions.length) {
251
+ // > no applicable restrictions -> 403
252
+ reject(req, getRejectReason(req, '@restrict', definition))
253
+ }
254
+ // normalize
255
+ restrictions = getNormalizedPlainRestrictions(restrictions, definition)
256
+ // at least one if the user's roles grants unrestricted access => done
257
+ if (restrictions.some(restrict => !restrict.where)) return
258
+
259
+ const resolvedApplicables = _getResolvedApplicables(restrictions, req)
260
+
261
+ // REVISIT: support more complex statics
262
+ if (_isStaticAuth(resolvedApplicables)) {
263
+ return _handleStaticAuth(resolvedApplicables, req)
264
+ }
265
+
266
+ if (req.event === 'READ') {
267
+ _addRestrictionsToRead(req, this.model, resolvedApplicables)
268
+ return
269
+ }
270
+
271
+ // no modification -> nothing more to do
272
+ if (!MOD_EVENTS[req.event]) return
273
+
274
+ if (req.query.DELETE) req.query.DELETE.from = _getFromWithIsActiveEntityRemoved(req.query.DELETE.from)
275
+ if (req.query.SELECT) req.query.SELECT.from = _getFromWithIsActiveEntityRemoved(req.query.SELECT.from)
276
+
277
+ // REVISIT: selected data could be used for etag check, diff, etc.
278
+
279
+ /*
280
+ * Here we check if UPDATE/DELETE requests add additional restrictions
281
+ * Note: Needs to happen sequentially because of side effects
282
+ */
283
+ const unrestrictedCount = await _getUnrestrictedCount(req)
284
+ if (unrestrictedCount === 0) req.reject(404)
285
+
286
+ const restrictedCount = await _getRestrictedCount(req, this.model, resolvedApplicables)
287
+
288
+ if (restrictedCount < unrestrictedCount) {
289
+ reject(req, getRejectReason(req, '@restrict', definition, restrictedCount, unrestrictedCount))
290
+ }
291
+
292
+ // for minor optimization in generic crud handler
293
+ req._authChecked = true
294
+ }
295
+
296
+ handler._initial = true
297
+
298
+ module.exports = handler
@@ -0,0 +1,85 @@
1
+ const _getLocalName = definition => {
2
+ return definition._service ? definition.name.replace(`${definition._service.name}.`, '') : definition.name
3
+ }
4
+
5
+ const _getRestrictWithEventRewrite = (grant, to, where, target) => {
6
+ // REVISIT: req.event should be 'SAVE' and 'PREPARE'
7
+ if (grant === 'SAVE') grant = 'draftActivate'
8
+ else if (grant === 'PREPARE') grant = 'draftPrepare'
9
+ return { grant, to, where, target }
10
+ }
11
+
12
+ const WRITE = ['CREATE', 'UPDATE', 'DELETE']
13
+
14
+ const _addNormalizedRestrictPerGrant = (grant, where, restrict, restricts, definition) => {
15
+ const to = restrict.to ? (Array.isArray(restrict.to) ? restrict.to : [restrict.to]) : ['any']
16
+
17
+ if (definition.kind === 'entity') {
18
+ if (grant === 'WRITE') {
19
+ WRITE.forEach(g => {
20
+ restricts.push(_getRestrictWithEventRewrite(g, to, where, definition))
21
+ })
22
+ } else {
23
+ restricts.push(_getRestrictWithEventRewrite(grant, to, where, definition))
24
+ }
25
+ } else {
26
+ restricts.push({ grant: _getLocalName(definition), to, where, target: definition.parent })
27
+ }
28
+ }
29
+
30
+ const _addNormalizedRestrict = (restrict, restricts, definition) => {
31
+ const where = restrict.where
32
+ ? restrict.where.replace(/\$user/g, '$user.id').replace(/\$user\.id\./g, '$user.')
33
+ : undefined
34
+
35
+ restrict.grant = Array.isArray(restrict.grant) ? restrict.grant : [restrict.grant || '*']
36
+ restrict.grant.forEach(grant => _addNormalizedRestrictPerGrant(grant, where, restrict, restricts, definition))
37
+ }
38
+
39
+ const getNormalizedRestrictions = (definition, definitions, event) => {
40
+ const restricts = []
41
+
42
+ // own
43
+ definition['@restrict'] &&
44
+ definition['@restrict'].forEach(restrict => _addNormalizedRestrict(restrict, restricts, definition))
45
+
46
+ // bounds
47
+ const actions = definition.actions
48
+
49
+ if (actions && Object.keys(actions).some(k => actions[k]['@restrict'])) {
50
+ for (const k in actions) {
51
+ const action = actions[k]
52
+
53
+ if (action['@restrict']) {
54
+ restricts.push(...getNormalizedRestrictions(action, definitions))
55
+ } else if (!definition['@restrict']) {
56
+ // > no entity-level restrictions => unrestricted action
57
+ restricts.push({ grant: action.name, to: ['any'], target: action.parent })
58
+ }
59
+ }
60
+ }
61
+
62
+ return restricts
63
+ }
64
+
65
+ const _isGrantAccessAllowed = (eventName, restrict) => restrict.grant === '*' || restrict.grant === eventName
66
+ const _isToAccessAllowed = (user, restrict) => restrict.to.some(role => user.is(role))
67
+
68
+ const getApplicableRestrictions = (restrictions, event, user) => {
69
+ return restrictions.filter(restrict => {
70
+ const eventName = { NEW: 'CREATE', EDIT: 'UPDATE' }[event] || event
71
+ return _isGrantAccessAllowed(eventName, restrict) && _isToAccessAllowed(user, restrict)
72
+ })
73
+ }
74
+
75
+ const getNormalizedPlainRestrictions = (restrictions, definition) => {
76
+ const result = []
77
+ for (const restriction of restrictions) _addNormalizedRestrict(restriction, result, definition)
78
+ return result
79
+ }
80
+
81
+ module.exports = {
82
+ getNormalizedRestrictions,
83
+ getApplicableRestrictions,
84
+ getNormalizedPlainRestrictions
85
+ }
@@ -0,0 +1,213 @@
1
+ const cds = require('../../../cds')
2
+ const LOG = cds.log('app')
3
+
4
+ const { CRUD_EVENTS } = require('./constants')
5
+
6
+ const { getDraftTreeRoot } = require('../../utils/csn')
7
+
8
+ const reject = (req, reason = null) => {
9
+ // unauthorized or forbidden?
10
+ if (req.user._is_anonymous) {
11
+ // REVISIT: challenges handling should be done in protocol adapter (i.e., express error middleware)
12
+ // REVISIT: improve `req._.req` check if this is an HTTP request
13
+ if (req._.req && req.user._challenges && req.user._challenges.length > 0) {
14
+ req._.res.set('WWW-Authenticate', req.user._challenges.join(';'))
15
+ }
16
+
17
+ // REVISIT: security log in else case?
18
+ return req.reject(401)
19
+ }
20
+
21
+ return req.reject({
22
+ code: 403,
23
+ internal: reason
24
+ })
25
+ }
26
+
27
+ const getRejectReason = (req, annotation, definition, restrictedCount, unrestrictedCount) => {
28
+ if (!LOG._debug) return
29
+ // it is not possible to specify the reason further than the source as there are multiple factors
30
+ const appendix = unrestrictedCount
31
+ ? ` (${unrestrictedCount - restrictedCount} out of ${unrestrictedCount} instances are restricted)`
32
+ : ''
33
+ return {
34
+ reason: `Access denied to user "${req.user.id}" for event "${req.event}"${appendix}`,
35
+ source: `${annotation} of "${definition.name}"`
36
+ }
37
+ }
38
+
39
+ const _getCurrentSubClause = (next, restrict) => {
40
+ const escaped = next[0].replace(/\$/g, '\\$').replace(/\./g, '\\.')
41
+ const re1 = new RegExp(`([\\w\\.']*)\\s*=\\s*(${escaped})|(${escaped})\\s*=\\s*([\\w\\.']*)`)
42
+ const re2 = new RegExp(`([\\w\\.']*)\\s*in\\s*(${escaped})|(${escaped})\\s*in\\s*([\\w\\.']*)`)
43
+ const clause = restrict.where.match(re1) || restrict.where.match(re2)
44
+
45
+ if (clause) return clause
46
+
47
+ // NOTE: arrayed attr with "=" as operator is some kind of legacy case
48
+ throw new Error('user attribute array must be used with operator "=" or "in"')
49
+ }
50
+
51
+ const _processUserAttr = (next, restrict, user, attr) => {
52
+ const clause = _getCurrentSubClause(next, restrict)
53
+ const valOrRef = clause[1] || clause[4]
54
+
55
+ if (clause[0].match(/ in /)) {
56
+ if (!user[attr] || user[attr].length === 0) {
57
+ restrict.where = restrict.where.replace(clause[0], '1 = 2')
58
+ } else if (user[attr].length === 1) {
59
+ restrict.where = restrict.where.replace(clause[0], `${valOrRef} = '${user[attr][0]}'`)
60
+ } else {
61
+ restrict.where = restrict.where.replace(
62
+ clause[0],
63
+ `${valOrRef} in (${user[attr].map(ele => `'${ele}'`).join(', ')})`
64
+ )
65
+ }
66
+ } else if (valOrRef.startsWith("'") && user[attr].includes(valOrRef.split("'")[1])) {
67
+ restrict.where = restrict.where.replace(clause[0], `${valOrRef} = ${valOrRef}`)
68
+ } else {
69
+ restrict.where = restrict.where.replace(
70
+ clause[0],
71
+ `(${user[attr].map(ele => `${valOrRef} = '${ele}'`).join(' or ')})`
72
+ )
73
+ }
74
+ }
75
+
76
+ const _getShortcut = (attrs, attr) => {
77
+ // undefined
78
+ if (attrs[attr] === undefined) {
79
+ return '1 = 2'
80
+ }
81
+
82
+ // $UNRESTRICTED
83
+ if (
84
+ (typeof attrs[attr] === 'string' && attrs[attr].match(/\$UNRESTRICTED/i)) ||
85
+ (Array.isArray(attrs[attr]) && attrs[attr].some(a => a.match(/\$UNRESTRICTED/i)))
86
+ ) {
87
+ return '1 = 1'
88
+ }
89
+
90
+ return null
91
+ }
92
+
93
+ /*
94
+ * for supporting xssec v3
95
+ */
96
+ const _getAttrsAsProxy = (attrs, additional = {}) => {
97
+ return new Proxy(
98
+ {},
99
+ {
100
+ get: function (_, attr) {
101
+ if (attr in additional) return additional[attr]
102
+ return attrs[attr]
103
+ }
104
+ }
105
+ )
106
+ }
107
+
108
+ /*
109
+ * resolves user attributes deeply, even though nested attributes are officially not supported
110
+ */
111
+ const resolveUserAttrs = (restrict, req) => {
112
+ const _getNext = where => where.match(/\$user\.([\w.]*)/)
113
+ let next = _getNext(restrict.where)
114
+
115
+ while (next !== null) {
116
+ const parts = next[1].split('.')
117
+ let skip
118
+ let val
119
+ let attrs = _getAttrsAsProxy(req.user.attr, { id: req.user.id })
120
+ let attr = parts.shift()
121
+
122
+ while (attr) {
123
+ const shortcut = _getShortcut(attrs, attr)
124
+ if (shortcut) {
125
+ const clause = _getCurrentSubClause(next, restrict)
126
+ restrict.where = restrict.where.replace(clause[0], shortcut)
127
+ skip = true
128
+ break
129
+ }
130
+
131
+ if (Array.isArray(attrs[attr])) {
132
+ _processUserAttr(next, restrict, attrs, attr)
133
+ skip = true
134
+ break
135
+ }
136
+
137
+ val = !Number.isNaN(Number(attrs[attr])) && attr !== 'id' ? attrs[attr] : `'${attrs[attr]}'`
138
+ if (val === null || val === undefined) break
139
+
140
+ attrs = _getAttrsAsProxy(attrs[attr])
141
+ attr = parts.shift()
142
+ }
143
+
144
+ if (!skip) restrict.where = restrict.where.replace(next[0], val === undefined ? null : val)
145
+ next = _getNext(restrict.where)
146
+ }
147
+
148
+ return restrict
149
+ }
150
+
151
+ const _authDependsOnParent = (entity, annotations) => {
152
+ // @cds.autoexposed and not @cds.autoexpose -> not explicitly exposed by modeling
153
+ return (
154
+ entity.name.match(/\.DraftAdministrativeData$/) ||
155
+ (entity['@cds.autoexposed'] && !entity['@cds.autoexpose'] && !annotations.some(a => a in entity))
156
+ )
157
+ }
158
+
159
+ const cqnFrom = req => {
160
+ const { query } = req
161
+ if (!query) return
162
+ if (query.SELECT) return query.SELECT.from
163
+ if (query.INSERT) return query.INSERT.into
164
+ if (query.UPDATE) return query.UPDATE.entity
165
+ if (query.DELETE) return query.DELETE.from
166
+ }
167
+
168
+ const getAuthRelevantEntity = (req, model, annotations) => {
169
+ if (!req.target || !(req.event in CRUD_EVENTS)) return
170
+ if (!_authDependsOnParent(req.target, annotations)) return req.target
171
+
172
+ let cqn = cqnFrom(req)
173
+
174
+ // REVISIT: needed in draft for some reason
175
+ if (typeof cqn === 'string') cqn = { ref: [cqn] }
176
+
177
+ if (cqn.ref.length === 1 && req.target._isDraftEnabled) {
178
+ // > direct access to children in draft
179
+ const root = getDraftTreeRoot(req.target, model)
180
+ if (!root)
181
+ reject(
182
+ req,
183
+ LOG._debug ? { reason: `Unable to determine single draft tree root for entity "${req.target.name}"` } : null
184
+ )
185
+ return root
186
+ }
187
+
188
+ // find and return the restrictions (may be none!) of the right-most entity that is non-autoexposed or explicitly restricted
189
+ const segments = []
190
+ let current = { elements: model.definitions }
191
+ for (let i = 0; i < cqn.ref.length; i++) {
192
+ current = current.elements[cqn.ref[i].id || cqn.ref[i]]
193
+ if (current && current.target) current = model.definitions[current.target]
194
+ segments.push(current)
195
+ }
196
+ let authRelevantEntity
197
+ for (let i = segments.length - 1; i >= 0; i--) {
198
+ const segment = segments[i]
199
+ if (segment.kind === 'entity' && !_authDependsOnParent(segment, annotations)) {
200
+ authRelevantEntity = segment
201
+ break
202
+ }
203
+ }
204
+ return authRelevantEntity
205
+ }
206
+
207
+ module.exports = {
208
+ reject,
209
+ getRejectReason,
210
+ resolveUserAttrs,
211
+ cqnFrom,
212
+ getAuthRelevantEntity
213
+ }
@@ -30,12 +30,13 @@ const _processorFn = req => {
30
30
  const { event, user, timestamp } = req
31
31
  const ts = new Date(timestamp).toISOString()
32
32
 
33
- return ({ row, key, plain }) => {
34
- const categories = plain.categories
35
-
36
- for (const category of categories) {
37
- if (category === '@cds.on.update' || (event === 'CREATE' && category === '@cds.on.insert')) {
33
+ return ({ row, key, plain, isRoot }) => {
34
+ for (const category of plain.categories) {
35
+ if (event === 'CREATE' && category === '@cds.on.insert') {
38
36
  replaceManagedData(row, key, user, ts)
37
+ } else if (category === '@cds.on.update') {
38
+ if (isRoot) replaceManagedData(row, key, user, ts)
39
+ else if (row[key] === '$user' || row[key] === '$now') delete row[key]
39
40
  }
40
41
  }
41
42
  }
@@ -93,8 +94,9 @@ module.exports = cds.service.impl(function () {
93
94
  if (res.length === 0) req.reject(404)
94
95
  }
95
96
 
97
+ // REVISIT: remove block with cds^6 (i.e., update_managed_properties is always true)
96
98
  // no changes, no op (otherwise, @cds.on.update gets new values), but we need to check existence
97
- if (req.event === 'UPDATE' && onlyKeysRemain(req)) {
99
+ if (cds.env.features.update_managed_properties === false && req.event === 'UPDATE' && onlyKeysRemain(req)) {
98
100
  if (await _targetEntityDoesNotExist(req)) req.reject(404)
99
101
  result = req.data
100
102
  }
@@ -55,7 +55,7 @@ const _handler = async function (req) {
55
55
  }
56
56
 
57
57
  // generate new etag, if UUID
58
- if (C_U_[req.event] && etagElement.type === 'cds.UUID') {
58
+ if (C_U_[req.event] && etagElement.isUUID) {
59
59
  req.data[etagElement.name] = cds.utils.uuid()
60
60
  }
61
61
  }