@sap/cds 5.8.4 → 5.9.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 (244) hide show
  1. package/CHANGELOG.md +174 -77
  2. package/app/fiori/preview.js +16 -11
  3. package/app/index.js +1 -1
  4. package/bin/build/buildTaskFactory.js +3 -3
  5. package/bin/build/buildTaskProviderFactory.js +1 -1
  6. package/bin/build/constants.js +1 -1
  7. package/bin/build/provider/buildTaskHandlerEdmx.js +12 -7
  8. package/bin/build/provider/buildTaskHandlerInternal.js +1 -1
  9. package/bin/build/provider/buildTaskProviderInternal.js +8 -2
  10. package/bin/build/provider/hana/2migration.js +27 -24
  11. package/bin/build/provider/hana/index.js +17 -18
  12. package/bin/build/provider/hana/migrationtable.js +9 -10
  13. package/bin/build/provider/java-cf/index.js +4 -5
  14. package/bin/build/provider/node-cf/index.js +99 -6
  15. package/bin/cds.js +17 -18
  16. package/bin/deploy/to-hana/cfUtil.js +16 -19
  17. package/bin/deploy/to-hana/hana.js +7 -24
  18. package/bin/deploy/to-hana/hdiDeployUtil.js +8 -4
  19. package/bin/mtx/in-cds.js +2 -2
  20. package/bin/serve.js +10 -3
  21. package/bin/utils/modules.js +7 -0
  22. package/bin/version.js +56 -3
  23. package/lib/compile/cdsc.js +26 -3
  24. package/lib/compile/etc/_localized.js +36 -25
  25. package/lib/compile/etc/csv.js +8 -8
  26. package/lib/compile/for/drafts.js +9 -0
  27. package/lib/compile/for/java.js +16 -0
  28. package/lib/compile/for/nodejs.js +12 -0
  29. package/lib/compile/for/odata.js +1 -1
  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/index.js +1 -1
  38. package/lib/core/entities.js +15 -14
  39. package/lib/core/index.js +39 -36
  40. package/lib/core/reflect.js +4 -2
  41. package/lib/deploy.js +114 -127
  42. package/lib/env/defaults.js +1 -0
  43. package/lib/env/index.js +165 -165
  44. package/lib/env/presets.js +1 -0
  45. package/lib/env/requires.js +120 -49
  46. package/lib/index.js +1 -0
  47. package/lib/log/format/kibana.js +2 -2
  48. package/lib/ql/SELECT.js +10 -0
  49. package/lib/ql/parse.js +1 -0
  50. package/lib/req/cds-context.js +4 -1
  51. package/lib/req/context.js +50 -56
  52. package/lib/req/event.js +1 -6
  53. package/lib/req/locale.js +6 -5
  54. package/lib/req/request.js +2 -0
  55. package/lib/req/user.js +7 -5
  56. package/lib/serve/Service-api.js +10 -7
  57. package/lib/serve/Service-dispatch.js +9 -11
  58. package/lib/serve/Service-methods.js +30 -41
  59. package/lib/serve/Transaction.js +10 -7
  60. package/lib/serve/adapters.js +7 -5
  61. package/lib/serve/index.js +24 -12
  62. package/lib/utils/data.js +1 -1
  63. package/lib/utils/index.js +27 -30
  64. package/lib/utils/resources/index.js +101 -0
  65. package/lib/utils/resources/tar.js +71 -0
  66. package/lib/utils/resources/utils.js +11 -0
  67. package/libx/_runtime/audit/Service.js +36 -39
  68. package/libx/_runtime/audit/generic/personal/access.js +3 -4
  69. package/libx/_runtime/audit/generic/personal/modification.js +3 -4
  70. package/libx/_runtime/audit/utils/v2.js +1 -2
  71. package/libx/_runtime/auth/index.js +126 -84
  72. package/libx/_runtime/auth/strategies/JWT.js +12 -19
  73. package/libx/_runtime/auth/strategies/dummy.js +1 -5
  74. package/libx/_runtime/auth/strategies/dwc.js +11 -9
  75. package/libx/_runtime/auth/strategies/mock.js +0 -4
  76. package/libx/_runtime/auth/strategies/{utils/xssec.js → xssecUtils.js} +7 -4
  77. package/libx/_runtime/auth/strategies/xsuaa.js +12 -19
  78. package/libx/_runtime/auth/utils.js +22 -1
  79. package/libx/_runtime/cds-services/adapter/odata-v4/Dispatcher.js +104 -98
  80. package/libx/_runtime/cds-services/adapter/odata-v4/OData.js +2 -2
  81. package/libx/_runtime/cds-services/adapter/odata-v4/ODataRequest.js +1 -1
  82. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/action.js +1 -1
  83. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/language.js +2 -8
  84. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/metadata.js +4 -29
  85. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/read.js +2 -1
  86. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/request.js +3 -2
  87. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/update.js +2 -2
  88. package/libx/_runtime/cds-services/adapter/odata-v4/odata-to-cqn/ExpressionToCQN.js +4 -6
  89. package/libx/_runtime/cds-services/adapter/odata-v4/odata-to-cqn/expandToCQN.js +24 -21
  90. package/libx/_runtime/cds-services/adapter/odata-v4/odata-to-cqn/index.js +8 -2
  91. package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-server/deserializer/DeserializerFactory.js +2 -0
  92. package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-server/invocation/DispatcherCommand.js +2 -6
  93. package/libx/_runtime/cds-services/adapter/odata-v4/to.js +1 -12
  94. package/libx/_runtime/cds-services/adapter/odata-v4/utils/data.js +33 -9
  95. package/libx/_runtime/cds-services/adapter/odata-v4/utils/dispatcherUtils.js +50 -0
  96. package/libx/_runtime/cds-services/adapter/odata-v4/utils/readAfterWrite.js +2 -2
  97. package/libx/_runtime/cds-services/adapter/odata-v4/utils/request.js +10 -3
  98. package/libx/_runtime/cds-services/adapter/odata-v4/utils/result.js +9 -11
  99. package/libx/_runtime/cds-services/adapter/rest/RestRequest.js +6 -3
  100. package/libx/_runtime/cds-services/adapter/rest/handlers/operation.js +4 -2
  101. package/libx/_runtime/cds-services/adapter/rest/rest-to-cqn/utils.js +1 -1
  102. package/libx/_runtime/cds-services/adapter/rest/utils/binary.js +1 -1
  103. package/libx/_runtime/cds-services/adapter/rest/utils/key-value-utils.js +2 -3
  104. package/libx/_runtime/cds-services/adapter/rest/utils/parse-url.js +6 -4
  105. package/libx/_runtime/cds-services/adapter/rest/utils/result.js +1 -0
  106. package/libx/_runtime/cds-services/adapter/rest/utils/validation-checks.js +8 -5
  107. package/libx/_runtime/cds-services/services/Service.js +40 -0
  108. package/libx/_runtime/cds-services/services/utils/columns.js +4 -3
  109. package/libx/_runtime/cds-services/services/utils/compareJson.js +4 -4
  110. package/libx/_runtime/cds-services/services/utils/differ.js +3 -3
  111. package/libx/_runtime/cds-services/services/utils/handlerUtils.js +4 -4
  112. package/libx/_runtime/cds-services/services/utils/restrictions.js +78 -0
  113. package/libx/_runtime/cds-services/util/assert.js +20 -14
  114. package/libx/_runtime/cds.js +9 -1
  115. package/libx/_runtime/common/aspects/any.js +5 -0
  116. package/libx/_runtime/common/aspects/entity.js +25 -7
  117. package/libx/_runtime/common/aspects/utils.js +2 -2
  118. package/libx/_runtime/common/composition/data.js +6 -0
  119. package/libx/_runtime/common/composition/insert.js +3 -2
  120. package/libx/_runtime/common/composition/tree.js +4 -10
  121. package/libx/_runtime/common/composition/update.js +4 -4
  122. package/libx/_runtime/common/constants/draft.js +29 -26
  123. package/libx/_runtime/common/error/constants.js +2 -2
  124. package/libx/_runtime/common/error/frontend.js +7 -15
  125. package/libx/_runtime/common/generic/auth/capabilities.js +59 -0
  126. package/libx/_runtime/common/generic/auth/constants.js +20 -0
  127. package/libx/_runtime/common/generic/auth/expand.js +54 -0
  128. package/libx/_runtime/common/generic/auth/index.js +32 -0
  129. package/libx/_runtime/common/generic/auth/insertOnly.js +15 -0
  130. package/libx/_runtime/common/generic/auth/readOnly.js +26 -0
  131. package/libx/_runtime/common/generic/auth/requires.js +34 -0
  132. package/libx/_runtime/common/generic/auth/restrict.js +296 -0
  133. package/libx/_runtime/common/generic/auth/utils.js +213 -0
  134. package/libx/_runtime/common/generic/crud.js +8 -6
  135. package/libx/_runtime/common/generic/etag.js +1 -1
  136. package/libx/_runtime/common/generic/input.js +35 -35
  137. package/libx/_runtime/common/generic/sorting.js +2 -3
  138. package/libx/_runtime/common/generic/temporal.js +2 -2
  139. package/libx/_runtime/common/i18n/messages.properties +1 -1
  140. package/libx/_runtime/common/toggles/handler.js +21 -0
  141. package/libx/_runtime/common/utils/copy.js +10 -1
  142. package/libx/_runtime/common/utils/cqn2cqn4sql.js +100 -29
  143. package/libx/_runtime/common/utils/csn.js +63 -1
  144. package/libx/_runtime/common/utils/dollar.js +10 -1
  145. package/libx/_runtime/common/utils/draft.js +46 -7
  146. package/libx/_runtime/common/utils/entityFromCqn.js +13 -9
  147. package/libx/_runtime/common/utils/extensibilityUtils.js +18 -0
  148. package/libx/_runtime/common/utils/foreignKeyPropagations.js +88 -104
  149. package/libx/_runtime/common/utils/generateOnCond.js +4 -1
  150. package/libx/_runtime/common/utils/quotingStyles.js +2 -0
  151. package/libx/_runtime/common/utils/resolveStructured.js +25 -9
  152. package/libx/_runtime/common/utils/resolveView.js +4 -1
  153. package/libx/_runtime/common/utils/rewriteAsterisks.js +3 -16
  154. package/libx/_runtime/common/utils/structured.js +33 -37
  155. package/libx/_runtime/common/utils/template.js +17 -8
  156. package/libx/_runtime/common/utils/templateProcessor.js +28 -28
  157. package/libx/_runtime/db/data-conversion/post-processing.js +118 -417
  158. package/libx/_runtime/db/expand/expandCQNToJoin.js +45 -41
  159. package/libx/_runtime/db/expand/rawToExpanded.js +29 -8
  160. package/libx/_runtime/db/generic/index.js +1 -3
  161. package/libx/_runtime/db/generic/input.js +5 -10
  162. package/libx/_runtime/db/generic/rewrite.js +5 -2
  163. package/libx/_runtime/db/generic/structured.js +2 -2
  164. package/libx/_runtime/db/query/delete.js +2 -2
  165. package/libx/_runtime/db/query/insert.js +1 -1
  166. package/libx/_runtime/db/query/update.js +9 -14
  167. package/libx/_runtime/db/sql-builder/CreateBuilder.js +4 -3
  168. package/libx/_runtime/db/sql-builder/InsertBuilder.js +14 -1
  169. package/libx/_runtime/db/sql-builder/SelectBuilder.js +3 -2
  170. package/libx/_runtime/db/sql-builder/dataTypes.js +3 -3
  171. package/libx/_runtime/db/utils/columns.js +3 -3
  172. package/libx/_runtime/db/utils/normalizeTimeData.js +2 -2
  173. package/libx/_runtime/db/utils/propagateForeignKeys.js +6 -2
  174. package/libx/_runtime/extensibility/mps/index.js +5 -0
  175. package/libx/_runtime/extensibility/mps/service.js +111 -0
  176. package/libx/_runtime/extensibility/mps/tar.js +42 -0
  177. package/libx/_runtime/extensibility/mps/utils.js +11 -0
  178. package/libx/_runtime/{fiori → extensibility}/uiflex/handler/transformREAD.js +0 -0
  179. package/libx/_runtime/{fiori → extensibility}/uiflex/handler/transformRESULT.js +17 -5
  180. package/libx/_runtime/{fiori → extensibility}/uiflex/handler/transformWRITE.js +1 -0
  181. package/libx/_runtime/extensibility/uiflex/index.js +54 -0
  182. package/libx/_runtime/extensibility/uiflex/service.js +276 -0
  183. package/libx/_runtime/{fiori → extensibility}/uiflex/utils.js +22 -7
  184. package/libx/_runtime/fiori/generic/activate.js +2 -2
  185. package/libx/_runtime/fiori/generic/before.js +4 -4
  186. package/libx/_runtime/fiori/generic/new.js +3 -3
  187. package/libx/_runtime/fiori/generic/patch.js +1 -1
  188. package/libx/_runtime/fiori/generic/read.js +58 -66
  189. package/libx/_runtime/fiori/generic/readOverDraft.js +71 -16
  190. package/libx/_runtime/fiori/utils/handler.js +6 -13
  191. package/libx/_runtime/fiori/utils/where.js +6 -5
  192. package/libx/_runtime/hana/Service.js +4 -10
  193. package/libx/_runtime/hana/customBuilder/CustomSelectBuilder.js +1 -1
  194. package/libx/_runtime/hana/driver.js +2 -2
  195. package/libx/_runtime/hana/execute.js +27 -74
  196. package/libx/_runtime/hana/pool.js +1 -1
  197. package/libx/_runtime/hana/streaming.js +2 -1
  198. package/libx/_runtime/index.js +6 -6
  199. package/libx/_runtime/messaging/AMQPWebhookMessaging.js +5 -21
  200. package/libx/_runtime/messaging/Outbox.js +2 -2
  201. package/libx/_runtime/messaging/common-utils/AMQPClient.js +4 -14
  202. package/libx/_runtime/messaging/common-utils/connections.js +5 -7
  203. package/libx/_runtime/messaging/common-utils/normalizeIncomingMessage.js +30 -0
  204. package/libx/_runtime/messaging/enterprise-messaging-shared.js +2 -1
  205. package/libx/_runtime/messaging/enterprise-messaging-utils/EMManagement.js +36 -30
  206. package/libx/_runtime/messaging/enterprise-messaging-utils/registerEndpoints.js +19 -12
  207. package/libx/_runtime/messaging/enterprise-messaging.js +8 -8
  208. package/libx/_runtime/messaging/file-based.js +5 -5
  209. package/libx/_runtime/messaging/message-queuing.js +14 -12
  210. package/libx/_runtime/messaging/outbox/utils.js +18 -19
  211. package/libx/_runtime/messaging/redis-messaging.js +91 -0
  212. package/libx/_runtime/messaging/service.js +8 -6
  213. package/libx/_runtime/remote/Service.js +44 -8
  214. package/libx/_runtime/remote/utils/client.js +20 -13
  215. package/libx/_runtime/remote/utils/data.js +11 -11
  216. package/libx/_runtime/sqlite/Service.js +6 -9
  217. package/libx/_runtime/sqlite/customBuilder/CustomFunctionBuilder.js +5 -2
  218. package/libx/_runtime/types/api.js +10 -2
  219. package/libx/common/utils/ucsn.js +109 -0
  220. package/libx/gql/resolvers/crud/update.js +5 -0
  221. package/libx/gql/resolvers/parse/ast2cqn/columns.js +3 -1
  222. package/libx/gql/schema/typeDefMap.js +2 -2
  223. package/libx/odata/afterburner.js +110 -16
  224. package/libx/odata/grammar.pegjs +9 -1
  225. package/libx/odata/parseToCqn.js +39 -0
  226. package/libx/odata/parser.js +1 -1
  227. package/libx/rest/RestAdapter.js +9 -1
  228. package/libx/rest/middleware/input.js +54 -0
  229. package/libx/rest/middleware/operation.js +14 -1
  230. package/libx/rest/middleware/parse.js +11 -7
  231. package/package.json +1 -1
  232. package/server.js +34 -19
  233. package/srv/audit-log.cds +2 -2
  234. package/srv/flex.cds +8 -2
  235. package/srv/flex.js +1 -1
  236. package/srv/mps.cds +23 -0
  237. package/srv/mps.js +1 -0
  238. package/libx/_runtime/auth/strategies/utils/uaa.js +0 -21
  239. package/libx/_runtime/common/generic/auth.js +0 -874
  240. package/libx/_runtime/common/toggles/alpha.js +0 -43
  241. package/libx/_runtime/db/generic/arrayed.js +0 -33
  242. package/libx/_runtime/fiori/uiflex/index.js +0 -35
  243. package/libx/_runtime/fiori/uiflex/service.js +0 -150
  244. 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,296 @@
1
+ const cds = require('../../../cds')
2
+
3
+ const { reject, getRejectReason, resolveUserAttrs, getAuthRelevantEntity } = require('./utils')
4
+ const { DRAFT_EVENTS, MOD_EVENTS } = require('./constants')
5
+
6
+ const { cqn2cqn4sql } = require('../../utils/cqn2cqn4sql')
7
+
8
+ const { isActiveEntityRequested, removeIsActiveEntityRecursively } = require('../../../fiori/utils/where')
9
+
10
+ const _getResolvedApplicables = (applicables, req) => {
11
+ const resolvedApplicables = []
12
+
13
+ // REVISIT: the static portion of "mixed wheres" could already grant access -> optimization potential
14
+ for (const restrict of applicables) {
15
+ // replace $user.x with respective values
16
+ const resolved = resolveUserAttrs({ grant: restrict.grant, target: restrict.target, where: restrict.where }, req)
17
+
18
+ // check for duplicates
19
+ if (
20
+ !resolvedApplicables.find(
21
+ restrict =>
22
+ resolved.grant === restrict.grant &&
23
+ (!resolved.target || resolved.target === restrict.target) &&
24
+ (!resolved.where || resolved.where === restrict.where)
25
+ )
26
+ ) {
27
+ if (resolved.where) resolved._xpr = cds.parse.expr(resolved.where).xpr
28
+ resolvedApplicables.push(resolved)
29
+ }
30
+ }
31
+
32
+ return resolvedApplicables
33
+ }
34
+
35
+ const _isStaticAuth = resolvedApplicables => {
36
+ return (
37
+ resolvedApplicables.length === 1 &&
38
+ resolvedApplicables[0]._xpr.length === 3 &&
39
+ resolvedApplicables[0]._xpr.every(ele => typeof ele !== 'object' || ele.val)
40
+ )
41
+ }
42
+
43
+ const _evalStatic = (op, vals) => {
44
+ vals[0] = Number.isNaN(Number(vals[0])) ? vals[0] : Number(vals[0])
45
+ vals[1] = Number.isNaN(Number(vals[1])) ? vals[1] : Number(vals[1])
46
+
47
+ switch (op) {
48
+ case '=':
49
+ return vals[0] === vals[1]
50
+ case '!=':
51
+ return vals[0] !== vals[1]
52
+ case '<':
53
+ return vals[0] < vals[1]
54
+ case '<=':
55
+ return vals[0] <= vals[1]
56
+ case '>':
57
+ return vals[0] > vals[1]
58
+ case '>=':
59
+ return vals[0] >= vals[1]
60
+ default:
61
+ throw new Error(`Operator "${op}" is not supported in @restrict.where`)
62
+ }
63
+ }
64
+
65
+ const _handleStaticAuth = (resolvedApplicables, req) => {
66
+ const op = resolvedApplicables[0]._xpr.find(ele => typeof ele === 'string')
67
+ const vals = resolvedApplicables[0]._xpr.filter(ele => typeof ele === 'object' && ele.val).map(ele => ele.val)
68
+
69
+ if (_evalStatic(op, vals)) {
70
+ // static clause grants access => done
71
+ return
72
+ }
73
+
74
+ // static clause forbids access => forbidden
75
+ return reject(req)
76
+ }
77
+
78
+ const _getMergedWhere = restricts => {
79
+ const xprs = []
80
+ restricts.forEach(ele => {
81
+ xprs.push('(', ...ele._xpr, ')', 'or')
82
+ })
83
+ xprs.pop()
84
+ return xprs
85
+ }
86
+
87
+ const _addWheresToRef = (ref, model, resolvedApplicables) => {
88
+ const newRef = []
89
+ let lastEntity = model.definitions[ref[0].id || ref[0]]
90
+
91
+ ref.forEach((identifier, idx) => {
92
+ if (idx === ref.length - 1) {
93
+ newRef.push(identifier)
94
+ return // determine last one separately
95
+ }
96
+
97
+ const entity = idx === 0 ? lastEntity : lastEntity.elements[identifier.id || identifier]._target
98
+ lastEntity = entity
99
+ const applicablesForEntity = resolvedApplicables.filter(
100
+ restrict => restrict.target && restrict.target.name === entity.name
101
+ )
102
+
103
+ let newIdentifier = identifier
104
+
105
+ if (applicablesForEntity.length) {
106
+ if (typeof newIdentifier === 'string') {
107
+ newIdentifier = { id: identifier, where: [] }
108
+ }
109
+
110
+ if (!newIdentifier.where) newIdentifier.where = []
111
+
112
+ if (newIdentifier.where && newIdentifier.where.length) {
113
+ newIdentifier.where.unshift('(')
114
+ newIdentifier.where.push(')')
115
+ newIdentifier.where.push('and')
116
+ }
117
+
118
+ newIdentifier.where.push(..._getMergedWhere(applicablesForEntity))
119
+ }
120
+
121
+ newRef.push(newIdentifier)
122
+ })
123
+
124
+ return newRef
125
+ }
126
+
127
+ const _getRestrictionForTarget = (resolvedApplicables, target) => {
128
+ const reqTarget = target && (target._isDraftEnabled ? target.name.replace(/_drafts$/, '') : target.name)
129
+ const applicablesForTarget = resolvedApplicables.filter(
130
+ restrict => restrict.target && restrict.target.name === reqTarget
131
+ )
132
+
133
+ if (applicablesForTarget.length) {
134
+ return _getMergedWhere(applicablesForTarget)
135
+ }
136
+ }
137
+
138
+ const _addRestrictionsToRead = async (req, model, resolvedApplicables) => {
139
+ if (req.target._isDraftEnabled) {
140
+ req.query._draftRestrictions = resolvedApplicables
141
+ return
142
+ }
143
+
144
+ if (typeof req.query.SELECT.from === 'object')
145
+ req.query.SELECT.from.ref = _addWheresToRef(req.query.SELECT.from.ref, model, resolvedApplicables)
146
+
147
+ const restrictionForTarget = _getRestrictionForTarget(resolvedApplicables, req.target)
148
+ if (!restrictionForTarget) return
149
+
150
+ // adjust free subselects, if necessary
151
+ if (resolvedApplicables.some(ra => ra.where.match(/\s*exists\s*\(\s*select\s*1\s*/i))) {
152
+ for (const ele of restrictionForTarget) {
153
+ if (typeof ele !== 'object' || !ele.SELECT || !ele.SELECT.where) continue
154
+
155
+ for (const w of ele.SELECT.where) {
156
+ if (w.ref && w.ref.length > 2) {
157
+ let path = w.ref[0]
158
+ if (!model.definitions[path]) continue
159
+ let i = 1
160
+
161
+ for (; i < w.ref.length; i++) {
162
+ if (model.definitions[`${path}.${w.ref[i]}`]) path += `.${w.ref[i]}`
163
+ else break
164
+ }
165
+
166
+ w.ref = [path, ...w.ref.slice(i)]
167
+ }
168
+ }
169
+ }
170
+ }
171
+
172
+ // apply restriction
173
+ req.query.where(restrictionForTarget)
174
+ }
175
+
176
+ const _getFromWithIsActiveEntityRemoved = from => {
177
+ for (const element of from.ref) {
178
+ if (element.where && isActiveEntityRequested(element.where)) {
179
+ element.where = removeIsActiveEntityRecursively(element.where)
180
+ }
181
+ }
182
+
183
+ return from
184
+ }
185
+
186
+ const _getUnrestrictedCount = async req => {
187
+ const dbtx = cds.tx(req)
188
+ const target =
189
+ (req.query.UPDATE && req.query.UPDATE.entity) ||
190
+ (req.query.DELETE && req.query.DELETE.from) ||
191
+ (req.query.SELECT && req.query.SELECT.from)
192
+ const selectUnrestricted = SELECT.one(['count(*) as n']).from(target)
193
+ const whereUnrestricted = (req.query.UPDATE && req.query.UPDATE.where) || (req.query.DELETE && req.query.DELETE.where)
194
+
195
+ if (whereUnrestricted) selectUnrestricted.where(whereUnrestricted)
196
+
197
+ // Because of side effects, the statements have to be fired sequentially.
198
+ const { n } = await dbtx.run(selectUnrestricted)
199
+ return n
200
+ }
201
+
202
+ const _getRestrictedCount = async (req, model, resolvedApplicables) => {
203
+ const dbtx = cds.tx(req)
204
+ const target =
205
+ (req.query.UPDATE && req.query.UPDATE.entity) ||
206
+ (req.query.DELETE && req.query.DELETE.from) ||
207
+ (req.query.SELECT && req.query.SELECT.from)
208
+ const selectRestricted = SELECT.one(['count(*) as n']).from(target)
209
+ const whereRestricted = (req.query.UPDATE && req.query.UPDATE.where) || (req.query.DELETE && req.query.DELETE.where)
210
+
211
+ if (whereRestricted) selectRestricted.where(whereRestricted)
212
+
213
+ if (typeof selectRestricted.SELECT === 'object') {
214
+ selectRestricted.SELECT.from.ref = _addWheresToRef(selectRestricted.SELECT.from.ref, model, resolvedApplicables)
215
+ }
216
+
217
+ const restrictionForTarget = _getRestrictionForTarget(resolvedApplicables, req.target)
218
+ if (restrictionForTarget) selectRestricted.where(restrictionForTarget)
219
+
220
+ const { n } = await dbtx.run(cqn2cqn4sql(selectRestricted, model, { suppressSearch: true }))
221
+ return n
222
+ }
223
+
224
+ // eslint-disable-next-line complexity
225
+ async function handler(req) {
226
+ if (req.user._is_privileged || DRAFT_EVENTS[req.event]) {
227
+ // > skip checks (events in DRAFT_EVENTS are checked in draft handlers via InProcessByUser)
228
+ return
229
+ }
230
+
231
+ const authRelevantEntity = getAuthRelevantEntity(req, this.model, ['@requires', '@restrict'])
232
+ const definition =
233
+ authRelevantEntity ||
234
+ (req.target && req.target.actions && req.target.actions[req.event]) ||
235
+ (this.operations && this.operations[req.event])
236
+
237
+ if (!definition) {
238
+ // > nothing to restrict
239
+ return
240
+ }
241
+
242
+ let restrictions = this.getRestrictions(definition, req.event, req.user)
243
+ if (restrictions instanceof Promise) restrictions = await restrictions
244
+ if (!restrictions) {
245
+ // > unrestricted
246
+ return
247
+ }
248
+
249
+ if (!restrictions.length) {
250
+ // > no applicable restrictions -> 403
251
+ reject(req, getRejectReason(req, '@restrict', definition))
252
+ }
253
+
254
+ // at least one if the user's roles grants unrestricted access => done
255
+ if (restrictions.some(restrict => !restrict.where)) return
256
+
257
+ const resolvedApplicables = _getResolvedApplicables(restrictions, req)
258
+
259
+ // REVISIT: support more complex statics
260
+ if (_isStaticAuth(resolvedApplicables)) {
261
+ return _handleStaticAuth(resolvedApplicables, req)
262
+ }
263
+
264
+ if (req.event === 'READ') {
265
+ _addRestrictionsToRead(req, this.model, resolvedApplicables)
266
+ return
267
+ }
268
+
269
+ // no modification -> nothing more to do
270
+ if (!MOD_EVENTS[req.event]) return
271
+
272
+ if (req.query.DELETE) req.query.DELETE.from = _getFromWithIsActiveEntityRemoved(req.query.DELETE.from)
273
+ if (req.query.SELECT) req.query.SELECT.from = _getFromWithIsActiveEntityRemoved(req.query.SELECT.from)
274
+
275
+ // REVISIT: selected data could be used for etag check, diff, etc.
276
+
277
+ /*
278
+ * Here we check if UPDATE/DELETE requests add additional restrictions
279
+ * Note: Needs to happen sequentially because of side effects
280
+ */
281
+ const unrestrictedCount = await _getUnrestrictedCount(req)
282
+ if (unrestrictedCount === 0) req.reject(404)
283
+
284
+ const restrictedCount = await _getRestrictedCount(req, this.model, resolvedApplicables)
285
+
286
+ if (restrictedCount < unrestrictedCount) {
287
+ reject(req, getRejectReason(req, '@restrict', definition, restrictedCount, unrestrictedCount))
288
+ }
289
+
290
+ // for minor optimization in generic crud handler
291
+ req._authChecked = true
292
+ }
293
+
294
+ handler._initial = true
295
+
296
+ module.exports = handler
@@ -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
  }