@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.
- package/CHANGELOG.md +198 -77
- package/app/fiori/preview.js +16 -11
- package/app/fiori/routes.js +15 -8
- package/app/index.js +1 -1
- package/bin/build/buildTaskFactory.js +3 -3
- package/bin/build/buildTaskProviderFactory.js +1 -1
- package/bin/build/constants.js +1 -1
- package/bin/build/provider/buildTaskHandlerEdmx.js +12 -7
- package/bin/build/provider/buildTaskHandlerInternal.js +1 -1
- package/bin/build/provider/buildTaskProviderInternal.js +8 -2
- package/bin/build/provider/hana/2migration.js +27 -24
- package/bin/build/provider/hana/index.js +17 -18
- package/bin/build/provider/hana/migrationtable.js +9 -10
- package/bin/build/provider/java-cf/index.js +4 -5
- package/bin/build/provider/node-cf/index.js +99 -6
- package/bin/cds.js +17 -18
- package/bin/deploy/to-hana/cfUtil.js +16 -19
- package/bin/deploy/to-hana/hana.js +7 -24
- package/bin/deploy/to-hana/hdiDeployUtil.js +8 -4
- package/bin/mtx/in-cds.js +2 -2
- package/bin/serve.js +10 -3
- package/bin/utils/modules.js +7 -0
- package/bin/version.js +56 -3
- package/lib/compile/cdsc.js +7 -2
- package/lib/compile/etc/_localized.js +37 -25
- package/lib/compile/etc/csv.js +8 -8
- package/lib/compile/for/drafts.js +9 -0
- package/lib/compile/for/java.js +16 -0
- package/lib/compile/for/nodejs.js +12 -0
- package/lib/compile/index.js +3 -0
- package/lib/compile/minify.js +16 -2
- package/lib/compile/parse.js +2 -2
- package/lib/compile/resolve.js +35 -18
- package/lib/compile/to/json.js +3 -1
- package/lib/compile/to/sql.js +2 -2
- package/lib/compile/to/srvinfo.js +4 -2
- package/lib/connect/bindings.js +1 -1
- package/lib/connect/index.js +3 -4
- package/lib/core/entities.js +15 -14
- package/lib/core/index.js +39 -36
- package/lib/core/reflect.js +4 -2
- package/lib/deploy.js +114 -127
- package/lib/env/defaults.js +1 -0
- package/lib/env/index.js +165 -165
- package/lib/env/presets.js +1 -0
- package/lib/env/requires.js +121 -50
- package/lib/index.js +2 -0
- package/lib/log/format/kibana.js +2 -2
- package/lib/ql/SELECT.js +10 -0
- package/lib/ql/parse.js +1 -0
- package/lib/req/cds-context.js +4 -1
- package/lib/req/context.js +50 -56
- package/lib/req/event.js +1 -6
- package/lib/req/locale.js +6 -5
- package/lib/req/request.js +2 -0
- package/lib/req/user.js +7 -5
- package/lib/serve/Service-api.js +10 -7
- package/lib/serve/Service-dispatch.js +9 -11
- package/lib/serve/Service-methods.js +30 -41
- package/lib/serve/Transaction.js +10 -7
- package/lib/serve/adapters.js +11 -9
- package/lib/serve/factory.js +14 -9
- package/lib/serve/index.js +28 -15
- package/lib/utils/data.js +1 -1
- package/lib/utils/index.js +27 -30
- package/lib/utils/resources/index.js +101 -0
- package/lib/utils/resources/tar.js +71 -0
- package/lib/utils/resources/utils.js +11 -0
- package/libx/_runtime/audit/Service.js +36 -39
- package/libx/_runtime/audit/generic/personal/access.js +3 -4
- package/libx/_runtime/audit/generic/personal/modification.js +3 -4
- package/libx/_runtime/audit/utils/v2.js +1 -2
- package/libx/_runtime/auth/index.js +126 -84
- package/libx/_runtime/auth/strategies/JWT.js +12 -19
- package/libx/_runtime/auth/strategies/dummy.js +1 -5
- package/libx/_runtime/auth/strategies/dwc.js +11 -9
- package/libx/_runtime/auth/strategies/mock.js +0 -4
- package/libx/_runtime/auth/strategies/{utils/xssec.js → xssecUtils.js} +7 -4
- package/libx/_runtime/auth/strategies/xsuaa.js +12 -19
- package/libx/_runtime/auth/utils.js +22 -1
- package/libx/_runtime/cds-services/adapter/odata-v4/Dispatcher.js +104 -98
- package/libx/_runtime/cds-services/adapter/odata-v4/OData.js +8 -3
- package/libx/_runtime/cds-services/adapter/odata-v4/ODataRequest.js +1 -1
- package/libx/_runtime/cds-services/adapter/odata-v4/handlers/action.js +1 -1
- package/libx/_runtime/cds-services/adapter/odata-v4/handlers/language.js +2 -8
- package/libx/_runtime/cds-services/adapter/odata-v4/handlers/metadata.js +4 -29
- package/libx/_runtime/cds-services/adapter/odata-v4/handlers/read.js +2 -1
- package/libx/_runtime/cds-services/adapter/odata-v4/handlers/request.js +3 -2
- package/libx/_runtime/cds-services/adapter/odata-v4/handlers/update.js +2 -2
- package/libx/_runtime/cds-services/adapter/odata-v4/odata-to-cqn/ExpressionToCQN.js +4 -6
- package/libx/_runtime/cds-services/adapter/odata-v4/odata-to-cqn/expandToCQN.js +24 -21
- package/libx/_runtime/cds-services/adapter/odata-v4/odata-to-cqn/index.js +8 -2
- package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-server/deserializer/DeserializerFactory.js +2 -0
- package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-server/invocation/DispatcherCommand.js +2 -6
- package/libx/_runtime/cds-services/adapter/odata-v4/to.js +1 -12
- package/libx/_runtime/cds-services/adapter/odata-v4/utils/data.js +33 -9
- package/libx/_runtime/cds-services/adapter/odata-v4/utils/dispatcherUtils.js +56 -0
- package/libx/_runtime/cds-services/adapter/odata-v4/utils/readAfterWrite.js +2 -2
- package/libx/_runtime/cds-services/adapter/odata-v4/utils/request.js +10 -3
- package/libx/_runtime/cds-services/adapter/odata-v4/utils/result.js +9 -11
- package/libx/_runtime/cds-services/adapter/rest/RestRequest.js +6 -3
- package/libx/_runtime/cds-services/adapter/rest/handlers/operation.js +4 -2
- package/libx/_runtime/cds-services/adapter/rest/rest-to-cqn/utils.js +1 -1
- package/libx/_runtime/cds-services/adapter/rest/utils/binary.js +1 -1
- package/libx/_runtime/cds-services/adapter/rest/utils/key-value-utils.js +2 -3
- package/libx/_runtime/cds-services/adapter/rest/utils/parse-url.js +6 -4
- package/libx/_runtime/cds-services/adapter/rest/utils/result.js +1 -0
- package/libx/_runtime/cds-services/adapter/rest/utils/validation-checks.js +8 -5
- package/libx/_runtime/cds-services/services/Service.js +40 -0
- package/libx/_runtime/cds-services/services/utils/columns.js +4 -3
- package/libx/_runtime/cds-services/services/utils/compareJson.js +4 -4
- package/libx/_runtime/cds-services/services/utils/differ.js +3 -3
- package/libx/_runtime/cds-services/services/utils/handlerUtils.js +4 -4
- package/libx/_runtime/cds-services/util/assert.js +20 -14
- package/libx/_runtime/cds.js +9 -1
- package/libx/_runtime/common/aspects/any.js +5 -0
- package/libx/_runtime/common/aspects/entity.js +25 -7
- package/libx/_runtime/common/aspects/utils.js +2 -2
- package/libx/_runtime/common/composition/data.js +6 -0
- package/libx/_runtime/common/composition/insert.js +3 -2
- package/libx/_runtime/common/composition/tree.js +4 -10
- package/libx/_runtime/common/composition/update.js +4 -4
- package/libx/_runtime/common/constants/draft.js +29 -26
- package/libx/_runtime/common/error/constants.js +2 -2
- package/libx/_runtime/common/error/frontend.js +7 -15
- package/libx/_runtime/common/generic/auth/capabilities.js +59 -0
- package/libx/_runtime/common/generic/auth/constants.js +20 -0
- package/libx/_runtime/common/generic/auth/expand.js +54 -0
- package/libx/_runtime/common/generic/auth/index.js +32 -0
- package/libx/_runtime/common/generic/auth/insertOnly.js +15 -0
- package/libx/_runtime/common/generic/auth/readOnly.js +26 -0
- package/libx/_runtime/common/generic/auth/requires.js +34 -0
- package/libx/_runtime/common/generic/auth/restrict.js +298 -0
- package/libx/_runtime/common/generic/auth/restrictions.js +85 -0
- package/libx/_runtime/common/generic/auth/utils.js +213 -0
- package/libx/_runtime/common/generic/crud.js +8 -6
- package/libx/_runtime/common/generic/etag.js +1 -1
- package/libx/_runtime/common/generic/input.js +35 -35
- package/libx/_runtime/common/generic/sorting.js +2 -3
- package/libx/_runtime/common/generic/temporal.js +2 -2
- package/libx/_runtime/common/i18n/messages.properties +1 -1
- package/libx/_runtime/common/toggles/handler.js +21 -0
- package/libx/_runtime/common/utils/copy.js +10 -1
- package/libx/_runtime/common/utils/cqn2cqn4sql.js +111 -35
- package/libx/_runtime/common/utils/csn.js +63 -1
- package/libx/_runtime/common/utils/dollar.js +10 -1
- package/libx/_runtime/common/utils/draft.js +46 -7
- package/libx/_runtime/common/utils/entityFromCqn.js +13 -9
- package/libx/_runtime/common/utils/extensibilityUtils.js +18 -0
- package/libx/_runtime/common/utils/foreignKeyPropagations.js +88 -104
- package/libx/_runtime/common/utils/generateOnCond.js +4 -1
- package/libx/_runtime/common/utils/quotingStyles.js +2 -0
- package/libx/_runtime/common/utils/resolveStructured.js +25 -9
- package/libx/_runtime/common/utils/resolveView.js +4 -1
- package/libx/_runtime/common/utils/rewriteAsterisks.js +3 -16
- package/libx/_runtime/common/utils/structured.js +33 -37
- package/libx/_runtime/common/utils/template.js +17 -8
- package/libx/_runtime/common/utils/templateProcessor.js +28 -28
- package/libx/_runtime/db/data-conversion/post-processing.js +118 -412
- package/libx/_runtime/db/expand/expandCQNToJoin.js +45 -41
- package/libx/_runtime/db/expand/rawToExpanded.js +29 -8
- package/libx/_runtime/db/generic/index.js +1 -3
- package/libx/_runtime/db/generic/input.js +5 -10
- package/libx/_runtime/db/generic/rewrite.js +5 -2
- package/libx/_runtime/db/generic/structured.js +2 -2
- package/libx/_runtime/db/query/delete.js +2 -2
- package/libx/_runtime/db/query/insert.js +1 -1
- package/libx/_runtime/db/query/update.js +9 -14
- package/libx/_runtime/db/sql-builder/CreateBuilder.js +4 -3
- package/libx/_runtime/db/sql-builder/FunctionBuilder.js +8 -8
- package/libx/_runtime/db/sql-builder/InsertBuilder.js +14 -1
- package/libx/_runtime/db/sql-builder/SelectBuilder.js +3 -2
- package/libx/_runtime/db/sql-builder/dataTypes.js +3 -3
- package/libx/_runtime/db/utils/columns.js +3 -3
- package/libx/_runtime/db/utils/normalizeTimeData.js +2 -2
- package/libx/_runtime/db/utils/propagateForeignKeys.js +6 -2
- package/libx/_runtime/extensibility/mps/index.js +5 -0
- package/libx/_runtime/extensibility/mps/service.js +111 -0
- package/libx/_runtime/extensibility/mps/tar.js +42 -0
- package/libx/_runtime/extensibility/mps/utils.js +11 -0
- package/libx/_runtime/{fiori → extensibility}/uiflex/handler/transformREAD.js +0 -0
- package/libx/_runtime/{fiori → extensibility}/uiflex/handler/transformRESULT.js +17 -5
- package/libx/_runtime/{fiori → extensibility}/uiflex/handler/transformWRITE.js +1 -0
- package/libx/_runtime/extensibility/uiflex/index.js +54 -0
- package/libx/_runtime/extensibility/uiflex/service.js +276 -0
- package/libx/_runtime/{fiori → extensibility}/uiflex/utils.js +22 -7
- package/libx/_runtime/fiori/generic/activate.js +2 -2
- package/libx/_runtime/fiori/generic/before.js +4 -4
- package/libx/_runtime/fiori/generic/new.js +3 -3
- package/libx/_runtime/fiori/generic/patch.js +1 -1
- package/libx/_runtime/fiori/generic/read.js +58 -66
- package/libx/_runtime/fiori/generic/readOverDraft.js +74 -16
- package/libx/_runtime/fiori/utils/handler.js +6 -13
- package/libx/_runtime/fiori/utils/where.js +6 -5
- package/libx/_runtime/hana/Service.js +4 -10
- package/libx/_runtime/hana/customBuilder/CustomSelectBuilder.js +1 -1
- package/libx/_runtime/hana/driver.js +2 -2
- package/libx/_runtime/hana/execute.js +45 -75
- package/libx/_runtime/hana/pool.js +1 -1
- package/libx/_runtime/hana/streaming.js +2 -1
- package/libx/_runtime/index.js +6 -6
- package/libx/_runtime/messaging/AMQPWebhookMessaging.js +5 -21
- package/libx/_runtime/messaging/Outbox.js +2 -2
- package/libx/_runtime/messaging/common-utils/AMQPClient.js +4 -14
- package/libx/_runtime/messaging/common-utils/connections.js +5 -7
- package/libx/_runtime/messaging/common-utils/normalizeIncomingMessage.js +30 -0
- package/libx/_runtime/messaging/enterprise-messaging-shared.js +2 -1
- package/libx/_runtime/messaging/enterprise-messaging-utils/EMManagement.js +36 -30
- package/libx/_runtime/messaging/enterprise-messaging-utils/registerEndpoints.js +19 -12
- package/libx/_runtime/messaging/enterprise-messaging.js +8 -8
- package/libx/_runtime/messaging/file-based.js +5 -5
- package/libx/_runtime/messaging/message-queuing.js +14 -12
- package/libx/_runtime/messaging/outbox/utils.js +18 -19
- package/libx/_runtime/messaging/redis-messaging.js +91 -0
- package/libx/_runtime/messaging/service.js +8 -6
- package/libx/_runtime/remote/Service.js +44 -8
- package/libx/_runtime/remote/utils/client.js +24 -19
- package/libx/_runtime/remote/utils/data.js +11 -11
- package/libx/_runtime/sqlite/Service.js +6 -9
- package/libx/_runtime/sqlite/customBuilder/CustomFunctionBuilder.js +5 -2
- package/libx/_runtime/types/api.js +10 -2
- package/libx/common/utils/ucsn.js +109 -0
- package/libx/gql/resolvers/crud/update.js +5 -0
- package/libx/gql/resolvers/parse/ast2cqn/columns.js +3 -1
- package/libx/gql/schema/typeDefMap.js +2 -2
- package/libx/odata/afterburner.js +110 -16
- package/libx/odata/cqn2odata.js +24 -27
- package/libx/odata/grammar.pegjs +9 -1
- package/libx/odata/parseToCqn.js +39 -0
- package/libx/odata/parser.js +1 -1
- package/libx/rest/RestAdapter.js +9 -1
- package/libx/rest/middleware/input.js +54 -0
- package/libx/rest/middleware/operation.js +14 -1
- package/libx/rest/middleware/parse.js +11 -7
- package/package.json +2 -2
- package/server.js +34 -19
- package/srv/audit-log.cds +2 -2
- package/srv/flex.cds +8 -2
- package/srv/flex.js +1 -1
- package/srv/mps.cds +23 -0
- package/srv/mps.js +1 -0
- package/libx/_runtime/auth/strategies/utils/uaa.js +0 -21
- package/libx/_runtime/common/generic/auth.js +0 -874
- package/libx/_runtime/common/toggles/alpha.js +0 -43
- package/libx/_runtime/db/generic/arrayed.js +0 -33
- package/libx/_runtime/fiori/uiflex/index.js +0 -35
- package/libx/_runtime/fiori/uiflex/service.js +0 -150
- 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
|
|
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
|
}
|