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