@sap/cds 5.8.2 → 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 (252) hide show
  1. package/CHANGELOG.md +214 -78
  2. package/app/fiori/preview.js +16 -11
  3. package/app/fiori/routes.js +3 -0
  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 +20 -17
  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 +12 -5
  22. package/bin/utils/modules.js +7 -0
  23. package/bin/version.js +56 -3
  24. package/lib/compile/cdsc.js +26 -3
  25. package/lib/compile/etc/_localized.js +36 -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/for/odata.js +1 -1
  31. package/lib/compile/index.js +3 -0
  32. package/lib/compile/minify.js +16 -2
  33. package/lib/compile/parse.js +2 -2
  34. package/lib/compile/resolve.js +35 -18
  35. package/lib/compile/to/json.js +3 -1
  36. package/lib/compile/to/sql.js +2 -2
  37. package/lib/compile/to/srvinfo.js +4 -2
  38. package/lib/connect/index.js +1 -1
  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 +120 -49
  47. package/lib/index.js +1 -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 +7 -5
  62. package/lib/serve/index.js +24 -12
  63. package/lib/utils/data.js +1 -1
  64. package/lib/utils/index.js +27 -30
  65. package/lib/utils/resources/index.js +101 -0
  66. package/lib/utils/resources/tar.js +71 -0
  67. package/lib/utils/resources/utils.js +11 -0
  68. package/libx/_runtime/audit/Service.js +36 -39
  69. package/libx/_runtime/audit/generic/personal/access.js +3 -4
  70. package/libx/_runtime/audit/generic/personal/modification.js +3 -4
  71. package/libx/_runtime/audit/utils/v2.js +1 -2
  72. package/libx/_runtime/auth/index.js +126 -84
  73. package/libx/_runtime/auth/strategies/JWT.js +12 -19
  74. package/libx/_runtime/auth/strategies/dummy.js +1 -5
  75. package/libx/_runtime/auth/strategies/dwc.js +11 -9
  76. package/libx/_runtime/auth/strategies/mock.js +0 -4
  77. package/libx/_runtime/auth/strategies/{utils/xssec.js → xssecUtils.js} +7 -4
  78. package/libx/_runtime/auth/strategies/xsuaa.js +12 -19
  79. package/libx/_runtime/auth/utils.js +22 -1
  80. package/libx/_runtime/cds-services/adapter/odata-v4/Dispatcher.js +104 -98
  81. package/libx/_runtime/cds-services/adapter/odata-v4/OData.js +2 -2
  82. package/libx/_runtime/cds-services/adapter/odata-v4/ODataRequest.js +1 -1
  83. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/action.js +13 -0
  84. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/language.js +2 -8
  85. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/metadata.js +4 -29
  86. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/read.js +2 -1
  87. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/request.js +3 -2
  88. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/update.js +2 -2
  89. package/libx/_runtime/cds-services/adapter/odata-v4/odata-to-cqn/ExpressionToCQN.js +4 -6
  90. package/libx/_runtime/cds-services/adapter/odata-v4/odata-to-cqn/expandToCQN.js +24 -21
  91. package/libx/_runtime/cds-services/adapter/odata-v4/odata-to-cqn/index.js +8 -2
  92. package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-commons/uri/UriHelper.js +1 -1
  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/deserializer/ResourceJsonDeserializer.js +5 -6
  95. package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-server/invocation/DispatcherCommand.js +2 -6
  96. package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-server/utils/UriHelper.js +4 -1
  97. package/libx/_runtime/cds-services/adapter/odata-v4/to.js +1 -12
  98. package/libx/_runtime/cds-services/adapter/odata-v4/utils/data.js +33 -9
  99. package/libx/_runtime/cds-services/adapter/odata-v4/utils/dispatcherUtils.js +50 -0
  100. package/libx/_runtime/cds-services/adapter/odata-v4/utils/readAfterWrite.js +2 -2
  101. package/libx/_runtime/cds-services/adapter/odata-v4/utils/request.js +10 -3
  102. package/libx/_runtime/cds-services/adapter/odata-v4/utils/result.js +9 -11
  103. package/libx/_runtime/cds-services/adapter/rest/RestRequest.js +6 -3
  104. package/libx/_runtime/cds-services/adapter/rest/handlers/operation.js +4 -2
  105. package/libx/_runtime/cds-services/adapter/rest/rest-to-cqn/utils.js +1 -1
  106. package/libx/_runtime/cds-services/adapter/rest/utils/binary.js +1 -1
  107. package/libx/_runtime/cds-services/adapter/rest/utils/key-value-utils.js +2 -3
  108. package/libx/_runtime/cds-services/adapter/rest/utils/parse-url.js +6 -4
  109. package/libx/_runtime/cds-services/adapter/rest/utils/result.js +1 -0
  110. package/libx/_runtime/cds-services/adapter/rest/utils/validation-checks.js +8 -5
  111. package/libx/_runtime/cds-services/services/Service.js +40 -0
  112. package/libx/_runtime/cds-services/services/utils/columns.js +4 -3
  113. package/libx/_runtime/cds-services/services/utils/compareJson.js +4 -4
  114. package/libx/_runtime/cds-services/services/utils/differ.js +3 -3
  115. package/libx/_runtime/cds-services/services/utils/handlerUtils.js +4 -4
  116. package/libx/_runtime/cds-services/services/utils/restrictions.js +78 -0
  117. package/libx/_runtime/cds-services/util/assert.js +20 -14
  118. package/libx/_runtime/cds.js +9 -1
  119. package/libx/_runtime/common/aspects/any.js +5 -0
  120. package/libx/_runtime/common/aspects/entity.js +25 -7
  121. package/libx/_runtime/common/aspects/utils.js +2 -2
  122. package/libx/_runtime/common/composition/data.js +6 -0
  123. package/libx/_runtime/common/composition/insert.js +3 -2
  124. package/libx/_runtime/common/composition/tree.js +4 -10
  125. package/libx/_runtime/common/composition/update.js +4 -4
  126. package/libx/_runtime/common/constants/draft.js +29 -26
  127. package/libx/_runtime/common/error/constants.js +2 -2
  128. package/libx/_runtime/common/error/frontend.js +7 -15
  129. package/libx/_runtime/common/generic/auth/capabilities.js +59 -0
  130. package/libx/_runtime/common/generic/auth/constants.js +20 -0
  131. package/libx/_runtime/common/generic/auth/expand.js +54 -0
  132. package/libx/_runtime/common/generic/auth/index.js +32 -0
  133. package/libx/_runtime/common/generic/auth/insertOnly.js +15 -0
  134. package/libx/_runtime/common/generic/auth/readOnly.js +26 -0
  135. package/libx/_runtime/common/generic/auth/requires.js +34 -0
  136. package/libx/_runtime/common/generic/auth/restrict.js +296 -0
  137. package/libx/_runtime/common/generic/auth/utils.js +213 -0
  138. package/libx/_runtime/common/generic/crud.js +14 -10
  139. package/libx/_runtime/common/generic/etag.js +1 -1
  140. package/libx/_runtime/common/generic/input.js +35 -35
  141. package/libx/_runtime/common/generic/sorting.js +2 -3
  142. package/libx/_runtime/common/generic/temporal.js +2 -2
  143. package/libx/_runtime/common/i18n/index.js +2 -31
  144. package/libx/_runtime/common/i18n/messages.properties +1 -1
  145. package/libx/_runtime/common/toggles/handler.js +21 -0
  146. package/libx/_runtime/common/utils/copy.js +10 -1
  147. package/libx/_runtime/common/utils/cqn2cqn4sql.js +100 -29
  148. package/libx/_runtime/common/utils/csn.js +63 -1
  149. package/libx/_runtime/common/utils/dollar.js +10 -1
  150. package/libx/_runtime/common/utils/draft.js +46 -7
  151. package/libx/_runtime/common/utils/entityFromCqn.js +13 -9
  152. package/libx/_runtime/common/utils/extensibilityUtils.js +18 -0
  153. package/libx/_runtime/common/utils/foreignKeyPropagations.js +88 -104
  154. package/libx/_runtime/common/utils/generateOnCond.js +9 -6
  155. package/libx/_runtime/common/utils/quotingStyles.js +2 -0
  156. package/libx/_runtime/common/utils/resolveStructured.js +25 -9
  157. package/libx/_runtime/common/utils/resolveView.js +4 -1
  158. package/libx/_runtime/common/utils/rewriteAsterisks.js +3 -16
  159. package/libx/_runtime/common/utils/structured.js +33 -37
  160. package/libx/_runtime/common/utils/template.js +17 -8
  161. package/libx/_runtime/common/utils/templateProcessor.js +28 -28
  162. package/libx/_runtime/db/data-conversion/post-processing.js +118 -417
  163. package/libx/_runtime/db/expand/expandCQNToJoin.js +45 -41
  164. package/libx/_runtime/db/expand/rawToExpanded.js +29 -8
  165. package/libx/_runtime/db/generic/index.js +1 -3
  166. package/libx/_runtime/db/generic/input.js +5 -10
  167. package/libx/_runtime/db/generic/rewrite.js +5 -2
  168. package/libx/_runtime/db/generic/structured.js +2 -2
  169. package/libx/_runtime/db/query/delete.js +2 -2
  170. package/libx/_runtime/db/query/insert.js +1 -1
  171. package/libx/_runtime/db/query/update.js +9 -14
  172. package/libx/_runtime/db/sql-builder/CreateBuilder.js +4 -3
  173. package/libx/_runtime/db/sql-builder/InsertBuilder.js +14 -1
  174. package/libx/_runtime/db/sql-builder/SelectBuilder.js +3 -2
  175. package/libx/_runtime/db/sql-builder/dataTypes.js +3 -3
  176. package/libx/_runtime/db/utils/columns.js +3 -3
  177. package/libx/_runtime/db/utils/normalizeTimeData.js +2 -2
  178. package/libx/_runtime/db/utils/propagateForeignKeys.js +6 -2
  179. package/libx/_runtime/extensibility/mps/index.js +5 -0
  180. package/libx/_runtime/extensibility/mps/service.js +111 -0
  181. package/libx/_runtime/extensibility/mps/tar.js +42 -0
  182. package/libx/_runtime/extensibility/mps/utils.js +11 -0
  183. package/libx/_runtime/{fiori → extensibility}/uiflex/handler/transformREAD.js +0 -0
  184. package/libx/_runtime/{fiori → extensibility}/uiflex/handler/transformRESULT.js +17 -5
  185. package/libx/_runtime/{fiori → extensibility}/uiflex/handler/transformWRITE.js +1 -0
  186. package/libx/_runtime/extensibility/uiflex/index.js +54 -0
  187. package/libx/_runtime/extensibility/uiflex/service.js +276 -0
  188. package/libx/_runtime/{fiori → extensibility}/uiflex/utils.js +22 -7
  189. package/libx/_runtime/fiori/generic/activate.js +2 -2
  190. package/libx/_runtime/fiori/generic/before.js +4 -4
  191. package/libx/_runtime/fiori/generic/new.js +3 -3
  192. package/libx/_runtime/fiori/generic/patch.js +1 -1
  193. package/libx/_runtime/fiori/generic/read.js +58 -66
  194. package/libx/_runtime/fiori/generic/readOverDraft.js +71 -16
  195. package/libx/_runtime/fiori/utils/handler.js +6 -13
  196. package/libx/_runtime/fiori/utils/where.js +6 -5
  197. package/libx/_runtime/hana/Service.js +4 -10
  198. package/libx/_runtime/hana/customBuilder/CustomSelectBuilder.js +2 -2
  199. package/libx/_runtime/hana/driver.js +2 -2
  200. package/libx/_runtime/hana/execute.js +29 -75
  201. package/libx/_runtime/hana/pool.js +1 -1
  202. package/libx/_runtime/hana/streaming.js +2 -1
  203. package/libx/_runtime/index.js +6 -6
  204. package/libx/_runtime/messaging/AMQPWebhookMessaging.js +5 -21
  205. package/libx/_runtime/messaging/Outbox.js +2 -2
  206. package/libx/_runtime/messaging/common-utils/AMQPClient.js +4 -14
  207. package/libx/_runtime/messaging/common-utils/connections.js +5 -7
  208. package/libx/_runtime/messaging/common-utils/normalizeIncomingMessage.js +30 -0
  209. package/libx/_runtime/messaging/enterprise-messaging-shared.js +2 -1
  210. package/libx/_runtime/messaging/enterprise-messaging-utils/EMManagement.js +36 -30
  211. package/libx/_runtime/messaging/enterprise-messaging-utils/registerEndpoints.js +19 -12
  212. package/libx/_runtime/messaging/enterprise-messaging.js +8 -8
  213. package/libx/_runtime/messaging/file-based.js +5 -5
  214. package/libx/_runtime/messaging/message-queuing.js +14 -12
  215. package/libx/_runtime/messaging/outbox/utils.js +18 -19
  216. package/libx/_runtime/messaging/redis-messaging.js +91 -0
  217. package/libx/_runtime/messaging/service.js +8 -6
  218. package/libx/_runtime/remote/Service.js +44 -8
  219. package/libx/_runtime/remote/utils/client.js +25 -13
  220. package/libx/_runtime/remote/utils/data.js +11 -11
  221. package/libx/_runtime/sqlite/Service.js +6 -9
  222. package/libx/_runtime/sqlite/customBuilder/CustomFunctionBuilder.js +5 -2
  223. package/libx/_runtime/types/api.js +10 -2
  224. package/libx/common/utils/ucsn.js +109 -0
  225. package/libx/gql/resolvers/crud/create.js +6 -1
  226. package/libx/gql/resolvers/crud/delete.js +6 -1
  227. package/libx/gql/resolvers/crud/read.js +6 -1
  228. package/libx/gql/resolvers/crud/update.js +9 -1
  229. package/libx/gql/resolvers/parse/ast2cqn/columns.js +3 -1
  230. package/libx/gql/schema/typeDefMap.js +2 -2
  231. package/libx/odata/afterburner.js +110 -16
  232. package/libx/odata/grammar.pegjs +9 -1
  233. package/libx/odata/parseToCqn.js +39 -0
  234. package/libx/odata/parser.js +1 -1
  235. package/libx/rest/RestAdapter.js +9 -1
  236. package/libx/rest/middleware/input.js +54 -0
  237. package/libx/rest/middleware/operation.js +14 -1
  238. package/libx/rest/middleware/parse.js +11 -7
  239. package/package.json +1 -1
  240. package/server.js +34 -19
  241. package/srv/audit-log.cds +2 -2
  242. package/srv/flex.cds +8 -2
  243. package/srv/flex.js +1 -1
  244. package/srv/mps.cds +23 -0
  245. package/srv/mps.js +1 -0
  246. package/libx/_runtime/auth/strategies/utils/uaa.js +0 -21
  247. package/libx/_runtime/common/generic/auth.js +0 -874
  248. package/libx/_runtime/common/toggles/alpha.js +0 -43
  249. package/libx/_runtime/db/generic/arrayed.js +0 -33
  250. package/libx/_runtime/fiori/uiflex/index.js +0 -35
  251. package/libx/_runtime/fiori/uiflex/service.js +0 -150
  252. package/libx/rest/utils/data.js +0 -60
@@ -0,0 +1,54 @@
1
+ const cds = require('../../../cds')
2
+
3
+ const { rewriteExpandAsterisk } = require('../../utils/rewriteAsterisks')
4
+
5
+ const { ensureNoDraftsSuffix } = require('../../../fiori/utils/handler')
6
+
7
+ const _getTarget = (ref, target, definitions) => {
8
+ if (cds.env.effective.odata.proxies) {
9
+ const target_ = target.elements[ref[0]]
10
+ if (ref.length === 1) return definitions[ensureNoDraftsSuffix(target_.target)]
11
+ return _getTarget(ref.slice(1), target_, definitions)
12
+ }
13
+ const target_ = target.elements[ref.join('_')]
14
+ return definitions[ensureNoDraftsSuffix(target_.target)]
15
+ }
16
+
17
+ const _getRestrictedExpand = (columns, target, definitions) => {
18
+ if (!columns || !target || columns === '*') return
19
+
20
+ const annotation = target['@Capabilities.ExpandRestrictions.NonExpandableProperties']
21
+ const restrictions = annotation && annotation.map(element => element['='])
22
+
23
+ rewriteExpandAsterisk(columns, target)
24
+
25
+ for (const col of columns) {
26
+ if (col.expand) {
27
+ if (restrictions && restrictions.length !== 0) {
28
+ const ref = col.ref.join('_')
29
+ const ref_ = restrictions.find(element => element.replace(/\./g, '_') === ref)
30
+ if (ref_) return ref_
31
+ }
32
+ // expand: '**' or '*3' is only possible within custom handler, no check needed
33
+ if (typeof col.expand === 'string' && /^\*{1}[\d|*]+/.test(col.expand)) {
34
+ continue
35
+ } else {
36
+ const restricted = _getRestrictedExpand(col.expand, _getTarget(col.ref, target, definitions), definitions)
37
+ if (restricted) return restricted
38
+ }
39
+ }
40
+ }
41
+ }
42
+
43
+ function handler(req) {
44
+ const restricted = _getRestrictedExpand(
45
+ req.query.SELECT && req.query.SELECT.columns,
46
+ req.target,
47
+ this.model.definitions
48
+ )
49
+ if (restricted) req.reject(400, 'EXPAND_IS_RESTRICTED', [restricted])
50
+ }
51
+
52
+ handler._initial = true
53
+
54
+ module.exports = handler
@@ -0,0 +1,32 @@
1
+ const cds = require('../../../cds')
2
+
3
+ const requiresHandler = require('./requires')
4
+ const readOnlyHandler = require('./readOnly')
5
+ const insertOnlyHandler = require('./insertOnly')
6
+ const capabilitiesHandler = require('./capabilities')
7
+ const restrictHandler = require('./restrict')
8
+ const restrictExpandHandler = require('./expand')
9
+
10
+ module.exports = cds.service.impl(function authorization() {
11
+ /*
12
+ * @requires
13
+ */
14
+ this.before('*', requiresHandler)
15
+
16
+ /*
17
+ * access control (cheaper than @restrict -> do first)
18
+ */
19
+ this.before('*', readOnlyHandler)
20
+ this.before('*', insertOnlyHandler)
21
+ this.before('*', capabilitiesHandler)
22
+
23
+ /*
24
+ * @restrict
25
+ */
26
+ this.before('*', restrictHandler)
27
+
28
+ /*
29
+ * expand restrictions
30
+ */
31
+ this.before('READ', '*', restrictExpandHandler)
32
+ })
@@ -0,0 +1,15 @@
1
+ const { getAuthRelevantEntity } = require('./utils')
2
+
3
+ function handler(req) {
4
+ const entity = getAuthRelevantEntity(req, this.model, ['@insertonly'])
5
+ if (!entity || !entity['@insertonly']) return
6
+
7
+ const allowed = entity._isDraftEnabled ? { NEW: 1, PATCH: 1 } : { CREATE: 1 }
8
+ if (!(req.event in allowed)) {
9
+ req.reject(405, 'ENTITY_IS_INSERT_ONLY', [entity.name])
10
+ }
11
+ }
12
+
13
+ handler._initial = true
14
+
15
+ module.exports = handler
@@ -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
+ }