@contember/engine-content-api 2.1.0-beta.1 → 2.1.0-beta.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/dist/development/index.cjs +1 -0
- package/dist/development/index.cjs.map +1 -1
- package/dist/development/index.js +2 -1
- package/dist/development/src/ExecutionContainer.cjs +40 -4
- package/dist/development/src/ExecutionContainer.cjs.map +1 -1
- package/dist/development/src/ExecutionContainer.js +41 -5
- package/dist/development/src/ExecutionContainer.js.map +1 -1
- package/dist/development/src/acl/PermissionFactory.cjs +29 -15
- package/dist/development/src/acl/PermissionFactory.cjs.map +1 -1
- package/dist/development/src/acl/PermissionFactory.js +29 -15
- package/dist/development/src/acl/PermissionFactory.js.map +1 -1
- package/dist/development/src/acl/PredicateFactory.cjs +33 -13
- package/dist/development/src/acl/PredicateFactory.cjs.map +1 -1
- package/dist/development/src/acl/PredicateFactory.js +33 -13
- package/dist/development/src/acl/PredicateFactory.js.map +1 -1
- package/dist/development/src/acl/PredicatesInjector.cjs +21 -18
- package/dist/development/src/acl/PredicatesInjector.cjs.map +1 -1
- package/dist/development/src/acl/PredicatesInjector.js +21 -18
- package/dist/development/src/acl/PredicatesInjector.js.map +1 -1
- package/dist/development/src/mapper/select/handlers/FieldsVisitor.cjs +2 -1
- package/dist/development/src/mapper/select/handlers/FieldsVisitor.cjs.map +1 -1
- package/dist/development/src/mapper/select/handlers/FieldsVisitor.js +2 -1
- package/dist/development/src/mapper/select/handlers/FieldsVisitor.js.map +1 -1
- package/dist/production/index.cjs +1 -0
- package/dist/production/index.cjs.map +1 -1
- package/dist/production/index.js +2 -1
- package/dist/production/src/ExecutionContainer.cjs +40 -4
- package/dist/production/src/ExecutionContainer.cjs.map +1 -1
- package/dist/production/src/ExecutionContainer.js +41 -5
- package/dist/production/src/ExecutionContainer.js.map +1 -1
- package/dist/production/src/acl/PermissionFactory.cjs +29 -15
- package/dist/production/src/acl/PermissionFactory.cjs.map +1 -1
- package/dist/production/src/acl/PermissionFactory.js +29 -15
- package/dist/production/src/acl/PermissionFactory.js.map +1 -1
- package/dist/production/src/acl/PredicateFactory.cjs +33 -13
- package/dist/production/src/acl/PredicateFactory.cjs.map +1 -1
- package/dist/production/src/acl/PredicateFactory.js +33 -13
- package/dist/production/src/acl/PredicateFactory.js.map +1 -1
- package/dist/production/src/acl/PredicatesInjector.cjs +21 -18
- package/dist/production/src/acl/PredicatesInjector.cjs.map +1 -1
- package/dist/production/src/acl/PredicatesInjector.js +21 -18
- package/dist/production/src/acl/PredicatesInjector.js.map +1 -1
- package/dist/production/src/mapper/select/handlers/FieldsVisitor.cjs +2 -1
- package/dist/production/src/mapper/select/handlers/FieldsVisitor.cjs.map +1 -1
- package/dist/production/src/mapper/select/handlers/FieldsVisitor.js +2 -1
- package/dist/production/src/mapper/select/handlers/FieldsVisitor.js.map +1 -1
- package/dist/tests/tsconfig.tsbuildinfo +1 -1
- package/dist/types/ExecutionContainer.d.ts +20 -1
- package/dist/types/ExecutionContainer.d.ts.map +1 -1
- package/dist/types/acl/PermissionFactory.d.ts +6 -0
- package/dist/types/acl/PermissionFactory.d.ts.map +1 -1
- package/dist/types/acl/PredicateFactory.d.ts +17 -6
- package/dist/types/acl/PredicateFactory.d.ts.map +1 -1
- package/dist/types/acl/PredicatesInjector.d.ts.map +1 -1
- package/dist/types/mapper/select/handlers/FieldsVisitor.d.ts.map +1 -1
- package/dist/types/tsconfig.tsbuildinfo +1 -1
- package/package.json +1 -2
- package/src/ExecutionContainer.ts +57 -4
- package/src/acl/PermissionFactory.ts +48 -15
- package/src/acl/PredicateFactory.ts +38 -9
- package/src/acl/PredicatesInjector.ts +19 -13
- package/src/mapper/select/handlers/FieldsVisitor.ts +2 -1
- package/tests/cases/unit/permissionMerger.test.ts +162 -0
- package/tests/cases/unit/predicatesFactory.test.ts +75 -0
- package/vitest.config.ts +0 -3
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@contember/engine-content-api",
|
|
3
|
-
"version": "2.1.0-beta.
|
|
3
|
+
"version": "2.1.0-beta.2",
|
|
4
4
|
"license": "Apache-2.0",
|
|
5
5
|
"main": "./dist/production/index.js",
|
|
6
6
|
"typings": "./dist/types/index.d.ts",
|
|
@@ -37,7 +37,6 @@
|
|
|
37
37
|
},
|
|
38
38
|
"devDependencies": {
|
|
39
39
|
"@contember/database-tester": "2.1.0-beta.1",
|
|
40
|
-
"@contember/schema-definition": "2.1.0-beta.1",
|
|
41
40
|
"@types/node": "^24.12.0",
|
|
42
41
|
"graphql": "^16.13.1",
|
|
43
42
|
"pg": "^8.20.0"
|
|
@@ -36,6 +36,32 @@ import {
|
|
|
36
36
|
} from './input-validation'
|
|
37
37
|
import { RefreshViewResolver } from './resolvers/RefreshViewResolver'
|
|
38
38
|
|
|
39
|
+
export interface TriggeredActionEvent {
|
|
40
|
+
id: string
|
|
41
|
+
trigger: string
|
|
42
|
+
target: string
|
|
43
|
+
transactionId: string
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export interface TriggeredActionsCollector {
|
|
47
|
+
add(events: readonly TriggeredActionEvent[]): void
|
|
48
|
+
getEvents(): readonly TriggeredActionEvent[]
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export class InMemoryTriggeredActionsCollector implements TriggeredActionsCollector {
|
|
52
|
+
private events: TriggeredActionEvent[] = []
|
|
53
|
+
|
|
54
|
+
add(events: readonly TriggeredActionEvent[]): void {
|
|
55
|
+
for (const event of events) {
|
|
56
|
+
this.events.push(event)
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
getEvents(): readonly TriggeredActionEvent[] {
|
|
61
|
+
return this.events
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
39
65
|
export type ExecutionContainerArgs = {
|
|
40
66
|
schema: Schema
|
|
41
67
|
schemaMeta: { id?: number }
|
|
@@ -44,6 +70,7 @@ export type ExecutionContainerArgs = {
|
|
|
44
70
|
identityId: string
|
|
45
71
|
identityVariables: Acl.VariablesMap
|
|
46
72
|
permissions: Acl.Permissions
|
|
73
|
+
allPermissions?: Acl.Permissions
|
|
47
74
|
systemSchema: string
|
|
48
75
|
project: { slug: string }
|
|
49
76
|
stage: { id: string; slug: string }
|
|
@@ -55,6 +82,7 @@ export interface ExecutionContainer {
|
|
|
55
82
|
mutationResolver: MutationResolver
|
|
56
83
|
validationResolver: ValidationResolver
|
|
57
84
|
refreshViewResolver: RefreshViewResolver
|
|
85
|
+
triggeredActionsCollector: TriggeredActionsCollector | undefined
|
|
58
86
|
}
|
|
59
87
|
|
|
60
88
|
export type ExecutionContainerBuilder = ReturnType<ExecutionContainerFactory['createBuilderInternal']>
|
|
@@ -75,21 +103,40 @@ export class ExecutionContainerFactory {
|
|
|
75
103
|
}
|
|
76
104
|
|
|
77
105
|
createBuilderInternal(
|
|
78
|
-
{
|
|
79
|
-
|
|
106
|
+
{
|
|
107
|
+
permissions,
|
|
108
|
+
allPermissions,
|
|
109
|
+
identityVariables,
|
|
110
|
+
identityId,
|
|
111
|
+
db,
|
|
112
|
+
schema,
|
|
113
|
+
schemaMeta,
|
|
114
|
+
systemSchema,
|
|
115
|
+
stage,
|
|
116
|
+
project,
|
|
117
|
+
schemaDatabaseMetadata,
|
|
118
|
+
userInfo,
|
|
119
|
+
}: ExecutionContainerArgs,
|
|
80
120
|
) {
|
|
81
121
|
return new Builder({})
|
|
82
122
|
.addService('systemSchema', () => systemSchema)
|
|
83
123
|
.addService('db', () => db)
|
|
84
124
|
.addService('project', () => project)
|
|
85
125
|
.addService('stage', () => stage)
|
|
126
|
+
.addService('triggeredActionsCollector', (): TriggeredActionsCollector | undefined =>
|
|
127
|
+
schema.settings.actions?.returnTriggeredActions === true
|
|
128
|
+
? new InMemoryTriggeredActionsCollector()
|
|
129
|
+
: undefined)
|
|
86
130
|
.addService('schema', () => schema)
|
|
87
131
|
.addService('schemaMeta', () => schemaMeta)
|
|
88
132
|
.addService('schemaDatabaseMetadata', () => schemaDatabaseMetadata)
|
|
89
133
|
.addService('userInfo', () => userInfo)
|
|
90
134
|
.addService('providers', () => this.providers)
|
|
91
135
|
.addService('variableInjector', ({ schema }) => new VariableInjector(schema.model, identityVariables))
|
|
92
|
-
.addService(
|
|
136
|
+
.addService(
|
|
137
|
+
'predicateFactory',
|
|
138
|
+
({ variableInjector, schema }) => new PredicateFactory(permissions, schema.model, variableInjector, allPermissions),
|
|
139
|
+
)
|
|
93
140
|
.addService('predicatesInjector', ({ predicateFactory, schema }) => new PredicatesInjector(schema.model, predicateFactory))
|
|
94
141
|
.addService('joinBuilder', ({ schema }) => new JoinBuilder(schema.model))
|
|
95
142
|
.addService('conditionBuilder', () => new ConditionBuilder())
|
|
@@ -232,6 +279,12 @@ export class ExecutionContainerFactory {
|
|
|
232
279
|
}
|
|
233
280
|
|
|
234
281
|
public create(args: ExecutionContainerArgs): ExecutionContainer {
|
|
235
|
-
return this.createBuilder(args).build().pick(
|
|
282
|
+
return this.createBuilder(args).build().pick(
|
|
283
|
+
'validationResolver',
|
|
284
|
+
'readResolver',
|
|
285
|
+
'mutationResolver',
|
|
286
|
+
'refreshViewResolver',
|
|
287
|
+
'triggeredActionsCollector',
|
|
288
|
+
)
|
|
236
289
|
}
|
|
237
290
|
}
|
|
@@ -7,17 +7,33 @@ export interface Identity {
|
|
|
7
7
|
projectRoles: readonly string[]
|
|
8
8
|
}
|
|
9
9
|
|
|
10
|
+
export interface ContextualPermissions {
|
|
11
|
+
root: Acl.Permissions
|
|
12
|
+
all: Acl.Permissions
|
|
13
|
+
}
|
|
14
|
+
|
|
10
15
|
export class PermissionFactory {
|
|
11
16
|
public create(schema: Schema, roles: readonly string[], prefix?: string): Acl.Permissions {
|
|
17
|
+
return this.createInternal(schema, roles, false, prefix)
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
public createContextual(schema: Schema, roles: readonly string[]): ContextualPermissions {
|
|
21
|
+
return {
|
|
22
|
+
root: this.createInternal(schema, roles, false),
|
|
23
|
+
all: this.createInternal(schema, roles, true),
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
private createInternal(schema: Schema, roles: readonly string[], includeThrough: boolean, prefix?: string): Acl.Permissions {
|
|
12
28
|
let result: Acl.Permissions = {}
|
|
13
29
|
for (let role of roles) {
|
|
14
30
|
const roleDefinition = schema.acl.roles[role] || { entities: {} }
|
|
15
31
|
let rolePermissions: Acl.Permissions = this.prefixPredicatesWithRole(schema.model, roleDefinition.entities, prefix || role)
|
|
16
32
|
if (roleDefinition.inherits) {
|
|
17
|
-
const inheritedPermissions = this.
|
|
18
|
-
rolePermissions = this.mergePermissions(inheritedPermissions, rolePermissions)
|
|
33
|
+
const inheritedPermissions = this.createInternal(schema, roleDefinition.inherits, includeThrough, prefix || role)
|
|
34
|
+
rolePermissions = this.mergePermissions(inheritedPermissions, rolePermissions, includeThrough)
|
|
19
35
|
}
|
|
20
|
-
result = this.mergePermissions(result, rolePermissions)
|
|
36
|
+
result = this.mergePermissions(result, rolePermissions, includeThrough)
|
|
21
37
|
}
|
|
22
38
|
result = this.makePrimaryPredicatesUnionOfAllFields(schema.model, result)
|
|
23
39
|
|
|
@@ -98,11 +114,11 @@ export class PermissionFactory {
|
|
|
98
114
|
})
|
|
99
115
|
}
|
|
100
116
|
|
|
101
|
-
private mergePermissions(left: Acl.Permissions, right: Acl.Permissions): Acl.Permissions {
|
|
117
|
+
private mergePermissions(left: Acl.Permissions, right: Acl.Permissions, includeThrough = false): Acl.Permissions {
|
|
102
118
|
const result = { ...left }
|
|
103
119
|
for (let entityName in right) {
|
|
104
120
|
if (result[entityName] !== undefined) {
|
|
105
|
-
result[entityName] = this.mergeEntityPermissions(result[entityName], right[entityName])
|
|
121
|
+
result[entityName] = this.mergeEntityPermissions(result[entityName], right[entityName], includeThrough)
|
|
106
122
|
} else {
|
|
107
123
|
result[entityName] = right[entityName]
|
|
108
124
|
}
|
|
@@ -110,7 +126,7 @@ export class PermissionFactory {
|
|
|
110
126
|
return result
|
|
111
127
|
}
|
|
112
128
|
|
|
113
|
-
private mergeEntityPermissions(left: Acl.EntityPermissions, right: Acl.EntityPermissions): Acl.EntityPermissions {
|
|
129
|
+
private mergeEntityPermissions(left: Acl.EntityPermissions, right: Acl.EntityPermissions, includeThrough = false): Acl.EntityPermissions {
|
|
114
130
|
let predicates: Writable<Acl.PredicateMap> = {}
|
|
115
131
|
const operations: Writable<Acl.EntityOperations> = {}
|
|
116
132
|
if (left.operations.customPrimary || right.operations.customPrimary) {
|
|
@@ -125,7 +141,12 @@ export class PermissionFactory {
|
|
|
125
141
|
'update',
|
|
126
142
|
] as const
|
|
127
143
|
) {
|
|
128
|
-
const { predicates: opPredicates, permissions: fieldPermissions, noRoot: opNoRoot } = this.mergeOperationPermissions(
|
|
144
|
+
const { predicates: opPredicates, permissions: fieldPermissions, noRoot: opNoRoot } = this.mergeOperationPermissions(
|
|
145
|
+
left,
|
|
146
|
+
right,
|
|
147
|
+
operation,
|
|
148
|
+
includeThrough,
|
|
149
|
+
)
|
|
129
150
|
|
|
130
151
|
if (opNoRoot) {
|
|
131
152
|
noRoot.push(operation)
|
|
@@ -137,7 +158,7 @@ export class PermissionFactory {
|
|
|
137
158
|
}
|
|
138
159
|
}
|
|
139
160
|
|
|
140
|
-
const { predicateDefinition, predicate, noRoot: opNoRoot } = this.mergeDeletePermissions(left, right)
|
|
161
|
+
const { predicateDefinition, predicate, noRoot: opNoRoot } = this.mergeDeletePermissions(left, right, includeThrough)
|
|
141
162
|
if (opNoRoot) {
|
|
142
163
|
noRoot.push('delete')
|
|
143
164
|
}
|
|
@@ -158,12 +179,17 @@ export class PermissionFactory {
|
|
|
158
179
|
}
|
|
159
180
|
}
|
|
160
181
|
|
|
161
|
-
private mergeOperationPermissions(
|
|
182
|
+
private mergeOperationPermissions(
|
|
183
|
+
left: Acl.EntityPermissions,
|
|
184
|
+
right: Acl.EntityPermissions,
|
|
185
|
+
operation: 'create' | 'read' | 'update',
|
|
186
|
+
includeThrough = false,
|
|
187
|
+
): {
|
|
162
188
|
noRoot: boolean
|
|
163
189
|
predicates: Acl.PredicateMap
|
|
164
190
|
permissions: Acl.FieldPermissions
|
|
165
191
|
} {
|
|
166
|
-
const { noRoot, leftPermissions, rightPermissions } = this.resolveNoRoot(left, right, operation)
|
|
192
|
+
const { noRoot, leftPermissions, rightPermissions } = this.resolveNoRoot(left, right, operation, includeThrough)
|
|
167
193
|
return {
|
|
168
194
|
noRoot,
|
|
169
195
|
...this.mergeFieldPermissions(
|
|
@@ -175,12 +201,12 @@ export class PermissionFactory {
|
|
|
175
201
|
}
|
|
176
202
|
}
|
|
177
203
|
|
|
178
|
-
private mergeDeletePermissions(left: Acl.EntityPermissions, right: Acl.EntityPermissions): {
|
|
204
|
+
private mergeDeletePermissions(left: Acl.EntityPermissions, right: Acl.EntityPermissions, includeThrough = false): {
|
|
179
205
|
noRoot: boolean
|
|
180
206
|
predicate: Acl.PredicateReference | boolean
|
|
181
207
|
predicateDefinition: Acl.PredicateDefinition | undefined
|
|
182
208
|
} {
|
|
183
|
-
const { noRoot, leftPermissions, rightPermissions } = this.resolveNoRoot(left, right, 'delete')
|
|
209
|
+
const { noRoot, leftPermissions, rightPermissions } = this.resolveNoRoot(left, right, 'delete', includeThrough)
|
|
184
210
|
|
|
185
211
|
return {
|
|
186
212
|
noRoot,
|
|
@@ -193,7 +219,12 @@ export class PermissionFactory {
|
|
|
193
219
|
}
|
|
194
220
|
}
|
|
195
221
|
|
|
196
|
-
private resolveNoRoot<const Op extends `${Acl.Operation}`>(
|
|
222
|
+
private resolveNoRoot<const Op extends `${Acl.Operation}`>(
|
|
223
|
+
left: Acl.EntityPermissions,
|
|
224
|
+
right: Acl.EntityPermissions,
|
|
225
|
+
operation: Op,
|
|
226
|
+
includeThrough = false,
|
|
227
|
+
): {
|
|
197
228
|
noRoot: boolean
|
|
198
229
|
leftPermissions: Acl.EntityPermissions['operations'][Op] | undefined
|
|
199
230
|
rightPermissions: Acl.EntityPermissions['operations'][Op] | undefined
|
|
@@ -207,8 +238,10 @@ export class PermissionFactory {
|
|
|
207
238
|
|| (leftRootForbidden && !rightPermissions)
|
|
208
239
|
|| (rightRootForbidden && !leftPermissions)
|
|
209
240
|
|
|
210
|
-
|
|
211
|
-
|
|
241
|
+
// When includeThrough is true, keep all permissions regardless of noRoot
|
|
242
|
+
// (used for "all" permissions that apply when accessing through relations)
|
|
243
|
+
const resolvedLeftPermissions = !includeThrough && !rootForbidden && leftRootForbidden ? undefined : leftPermissions
|
|
244
|
+
const resolvedRightPermissions = !includeThrough && !rootForbidden && rightRootForbidden ? undefined : rightPermissions
|
|
212
245
|
|
|
213
246
|
return {
|
|
214
247
|
noRoot: rootForbidden,
|
|
@@ -14,14 +14,32 @@ export class PredicateFactory {
|
|
|
14
14
|
private readonly permissions: Acl.Permissions,
|
|
15
15
|
private readonly model: Model.Schema,
|
|
16
16
|
private readonly variableInjector: VariableInjector,
|
|
17
|
+
private readonly allPermissions?: Acl.Permissions,
|
|
17
18
|
) {}
|
|
18
19
|
|
|
20
|
+
/**
|
|
21
|
+
* Selects the appropriate permission set based on query context:
|
|
22
|
+
* - `isRoot === false` (nested/relation context): uses allPermissions (includes through-only permissions)
|
|
23
|
+
* - `isRoot === true` or `isRoot === undefined` (root or unknown context): uses root-only permissions
|
|
24
|
+
*
|
|
25
|
+
* The `undefined` vs `true` distinction matters: `undefined` is the default for callers
|
|
26
|
+
* that don't track root context, and must fall through to root permissions for safety.
|
|
27
|
+
*/
|
|
28
|
+
private getPermissionsForContext(isRoot?: boolean): Acl.Permissions {
|
|
29
|
+
if (isRoot === false && this.allPermissions) {
|
|
30
|
+
return this.allPermissions
|
|
31
|
+
}
|
|
32
|
+
return this.permissions
|
|
33
|
+
}
|
|
34
|
+
|
|
19
35
|
public getFieldPredicate(
|
|
20
36
|
entity: Model.Entity,
|
|
21
37
|
operation: Acl.Operation.update | Acl.Operation.read | Acl.Operation.create,
|
|
22
38
|
fieldName: string,
|
|
39
|
+
isRoot?: boolean,
|
|
23
40
|
): FieldRequiredPredicate {
|
|
24
|
-
const
|
|
41
|
+
const perms = this.getPermissionsForContext(isRoot)
|
|
42
|
+
const permissions = perms[entity.name]?.operations?.[operation]
|
|
25
43
|
const predicate = permissions?.[fieldName] ?? false
|
|
26
44
|
const rowLevelField = getRowLevelPredicatePseudoField(entity)
|
|
27
45
|
|
|
@@ -38,12 +56,15 @@ export class PredicateFactory {
|
|
|
38
56
|
entity: Model.Entity,
|
|
39
57
|
operation: Acl.Operation.read,
|
|
40
58
|
fieldName: string,
|
|
59
|
+
isRoot?: boolean,
|
|
41
60
|
): boolean {
|
|
61
|
+
const perms = this.getPermissionsForContext(isRoot)
|
|
42
62
|
const rowLevelField = getRowLevelPredicatePseudoField(entity)
|
|
43
|
-
const permissions =
|
|
63
|
+
const permissions = perms[entity.name]?.operations?.[operation]
|
|
44
64
|
return permissions?.[fieldName] !== permissions?.[rowLevelField]
|
|
45
65
|
}
|
|
46
66
|
|
|
67
|
+
/** Delete predicates are not context-aware — through-permission support is scoped to read operations only. */
|
|
47
68
|
public createDeletePredicate(entity: Model.Entity) {
|
|
48
69
|
const neverCondition: Input.Where = { [entity.primary]: { never: true } }
|
|
49
70
|
const entityPermissions = this.permissions[entity.name]
|
|
@@ -65,8 +86,10 @@ export class PredicateFactory {
|
|
|
65
86
|
operation: Acl.Operation.update | Acl.Operation.read | Acl.Operation.create,
|
|
66
87
|
fieldNames: string[] = [getRowLevelPredicatePseudoField(entity)],
|
|
67
88
|
relationContext?: Model.AnyRelationContext,
|
|
89
|
+
isRoot?: boolean,
|
|
68
90
|
): Input.OptionalWhere {
|
|
69
|
-
const
|
|
91
|
+
const perms = this.getPermissionsForContext(isRoot)
|
|
92
|
+
const entityPermissions: Acl.EntityPermissions = perms[entity.name]
|
|
70
93
|
const neverCondition: Input.Where = { [entity.primary]: { never: true } }
|
|
71
94
|
|
|
72
95
|
if (!entityPermissions) {
|
|
@@ -85,11 +108,17 @@ export class PredicateFactory {
|
|
|
85
108
|
return neverCondition
|
|
86
109
|
}
|
|
87
110
|
|
|
88
|
-
return this.buildPredicates(entity, operationPredicates, relationContext)
|
|
111
|
+
return this.buildPredicates(entity, operationPredicates, relationContext, isRoot)
|
|
89
112
|
}
|
|
90
113
|
|
|
91
|
-
public buildPredicates(
|
|
92
|
-
|
|
114
|
+
public buildPredicates(
|
|
115
|
+
entity: Model.Entity,
|
|
116
|
+
predicates: Acl.PredicateReference[],
|
|
117
|
+
relationContext?: Model.AnyRelationContext,
|
|
118
|
+
isRoot?: boolean,
|
|
119
|
+
): Input.OptionalWhere {
|
|
120
|
+
const perms = this.getPermissionsForContext(isRoot)
|
|
121
|
+
const entityPermissions: Acl.EntityPermissions = perms[entity.name] ?? {}
|
|
93
122
|
|
|
94
123
|
const predicatesWhere: Input.Where[] = predicates.reduce(
|
|
95
124
|
(result: Input.Where[], name: Acl.PredicateReference): Input.Where[] => {
|
|
@@ -105,7 +134,7 @@ export class PredicateFactory {
|
|
|
105
134
|
return {}
|
|
106
135
|
}
|
|
107
136
|
const where: Input.Where = predicatesWhere.length === 1 ? predicatesWhere[0] : { and: predicatesWhere }
|
|
108
|
-
return this.optimizePredicates(where, relationContext)
|
|
137
|
+
return this.optimizePredicates(where, relationContext, isRoot)
|
|
109
138
|
}
|
|
110
139
|
|
|
111
140
|
private getRequiredPredicates(
|
|
@@ -128,11 +157,11 @@ export class PredicateFactory {
|
|
|
128
157
|
return predicates
|
|
129
158
|
}
|
|
130
159
|
|
|
131
|
-
public optimizePredicates(where: Input.OptionalWhere, relationContext?: Model.AnyRelationContext) {
|
|
160
|
+
public optimizePredicates(where: Input.OptionalWhere, relationContext?: Model.AnyRelationContext, isRoot?: boolean) {
|
|
132
161
|
if (!relationContext || !relationContext.targetRelation) {
|
|
133
162
|
return where
|
|
134
163
|
}
|
|
135
|
-
const sourcePredicate = this.create(relationContext.entity, Acl.Operation.read, [relationContext.relation.name])
|
|
164
|
+
const sourcePredicate = this.create(relationContext.entity, Acl.Operation.read, [relationContext.relation.name], undefined, isRoot)
|
|
136
165
|
if (Object.keys(sourcePredicate).length === 0) {
|
|
137
166
|
return where
|
|
138
167
|
}
|
|
@@ -11,8 +11,9 @@ export class PredicatesInjector {
|
|
|
11
11
|
relationContext?: Model.AnyRelationContext,
|
|
12
12
|
ancestorPath?: readonly Model.AnyRelationContext[],
|
|
13
13
|
): Input.OptionalWhere {
|
|
14
|
-
const
|
|
15
|
-
|
|
14
|
+
const isQueryRoot = !relationContext && (!ancestorPath || ancestorPath.length === 0)
|
|
15
|
+
const restrictedWhere = this.injectToWhere(where, entity, true, relationContext, false, ancestorPath ?? [], isQueryRoot)
|
|
16
|
+
return this.createWhere(entity, undefined, restrictedWhere, relationContext, false, ancestorPath ?? [], isQueryRoot)
|
|
16
17
|
}
|
|
17
18
|
|
|
18
19
|
/**
|
|
@@ -40,6 +41,7 @@ export class PredicatesInjector {
|
|
|
40
41
|
relationContext?: Model.AnyRelationContext,
|
|
41
42
|
isBackReferenceContext?: boolean,
|
|
42
43
|
ancestorPath?: readonly Model.AnyRelationContext[],
|
|
44
|
+
isQueryRoot?: boolean,
|
|
43
45
|
): Input.OptionalWhere {
|
|
44
46
|
// Simplify predicates when:
|
|
45
47
|
// 1. We're in a back-reference context (inside a filter that traverses back)
|
|
@@ -55,13 +57,14 @@ export class PredicatesInjector {
|
|
|
55
57
|
if (shouldSimplify) {
|
|
56
58
|
predicatesWhere = { [entity.primary]: { always: true } }
|
|
57
59
|
} else {
|
|
58
|
-
const rawPredicate = this.predicateFactory.create(entity, Acl.Operation.read, fieldNames, relationContext)
|
|
60
|
+
const rawPredicate = this.predicateFactory.create(entity, Acl.Operation.read, fieldNames, relationContext, isQueryRoot)
|
|
59
61
|
// Process the predicate to inject nested entity predicates
|
|
60
62
|
predicatesWhere = this.injectPredicatesToPredicate(
|
|
61
63
|
rawPredicate,
|
|
62
64
|
entity,
|
|
63
65
|
isBackReferenceContext ?? false,
|
|
64
66
|
ancestorPath ?? [],
|
|
67
|
+
isQueryRoot,
|
|
65
68
|
)
|
|
66
69
|
}
|
|
67
70
|
|
|
@@ -85,21 +88,22 @@ export class PredicatesInjector {
|
|
|
85
88
|
entity: Model.Entity,
|
|
86
89
|
isBackReferenceContext: boolean,
|
|
87
90
|
ancestorPath: readonly Model.AnyRelationContext[],
|
|
91
|
+
isQueryRoot?: boolean,
|
|
88
92
|
): Input.OptionalWhere {
|
|
89
93
|
const resultWhere: Writable<Input.OptionalWhere> = {}
|
|
90
94
|
|
|
91
95
|
if (where.and) {
|
|
92
96
|
resultWhere.and = where.and
|
|
93
97
|
.filter((it): it is Input.Where => !!it)
|
|
94
|
-
.map(it => this.injectPredicatesToPredicate(it, entity, isBackReferenceContext, ancestorPath))
|
|
98
|
+
.map(it => this.injectPredicatesToPredicate(it, entity, isBackReferenceContext, ancestorPath, isQueryRoot))
|
|
95
99
|
}
|
|
96
100
|
if (where.or) {
|
|
97
101
|
resultWhere.or = where.or
|
|
98
102
|
.filter((it): it is Input.Where => !!it)
|
|
99
|
-
.map(it => this.injectPredicatesToPredicate(it, entity, isBackReferenceContext, ancestorPath))
|
|
103
|
+
.map(it => this.injectPredicatesToPredicate(it, entity, isBackReferenceContext, ancestorPath, isQueryRoot))
|
|
100
104
|
}
|
|
101
105
|
if (where.not) {
|
|
102
|
-
resultWhere.not = this.injectPredicatesToPredicate(where.not, entity, isBackReferenceContext, ancestorPath)
|
|
106
|
+
resultWhere.not = this.injectPredicatesToPredicate(where.not, entity, isBackReferenceContext, ancestorPath, isQueryRoot)
|
|
103
107
|
}
|
|
104
108
|
|
|
105
109
|
const fields = Object.keys(where).filter(it => !['and', 'or', 'not'].includes(it))
|
|
@@ -128,6 +132,7 @@ export class PredicatesInjector {
|
|
|
128
132
|
context.targetEntity,
|
|
129
133
|
nestedIsBackReferenceContext,
|
|
130
134
|
nestedAncestorPath,
|
|
135
|
+
isQueryRoot,
|
|
131
136
|
)
|
|
132
137
|
|
|
133
138
|
// Check if we should simplify the target entity's predicate
|
|
@@ -137,7 +142,7 @@ export class PredicatesInjector {
|
|
|
137
142
|
// Get target entity's predicate (simplified if back-reference)
|
|
138
143
|
const targetPredicate = shouldSimplifyNested
|
|
139
144
|
? { [context.targetEntity.primary]: { always: true } }
|
|
140
|
-
: this.predicateFactory.create(context.targetEntity, Acl.Operation.read, undefined, context)
|
|
145
|
+
: this.predicateFactory.create(context.targetEntity, Acl.Operation.read, undefined, context, isQueryRoot)
|
|
141
146
|
|
|
142
147
|
// Optimization: avoid duplicate { id: always } when both are simplified
|
|
143
148
|
const primaryKey = context.targetEntity.primary
|
|
@@ -175,20 +180,21 @@ export class PredicatesInjector {
|
|
|
175
180
|
relationContext: Model.AnyRelationContext | undefined,
|
|
176
181
|
isBackReferenceContext: boolean,
|
|
177
182
|
ancestorPath: readonly Model.AnyRelationContext[],
|
|
183
|
+
isQueryRoot?: boolean,
|
|
178
184
|
): Input.OptionalWhere {
|
|
179
185
|
const resultWhere: Writable<Input.OptionalWhere> = {}
|
|
180
186
|
if (where.and) {
|
|
181
187
|
resultWhere.and = where.and.filter((it): it is Input.Where => !!it).map(it =>
|
|
182
|
-
this.injectToWhere(it, entity, isRoot, relationContext, isBackReferenceContext, ancestorPath)
|
|
188
|
+
this.injectToWhere(it, entity, isRoot, relationContext, isBackReferenceContext, ancestorPath, isQueryRoot)
|
|
183
189
|
)
|
|
184
190
|
}
|
|
185
191
|
if (where.or) {
|
|
186
192
|
resultWhere.or = where.or.filter((it): it is Input.Where => !!it).map(it =>
|
|
187
|
-
this.injectToWhere(it, entity, isRoot, relationContext, isBackReferenceContext, ancestorPath)
|
|
193
|
+
this.injectToWhere(it, entity, isRoot, relationContext, isBackReferenceContext, ancestorPath, isQueryRoot)
|
|
188
194
|
)
|
|
189
195
|
}
|
|
190
196
|
if (where.not) {
|
|
191
|
-
resultWhere.not = this.injectToWhere(where.not, entity, isRoot, relationContext, isBackReferenceContext, ancestorPath)
|
|
197
|
+
resultWhere.not = this.injectToWhere(where.not, entity, isRoot, relationContext, isBackReferenceContext, ancestorPath, isQueryRoot)
|
|
192
198
|
}
|
|
193
199
|
|
|
194
200
|
const fields = Object.keys(where).filter(it => !['and', 'or', 'not'].includes(it))
|
|
@@ -210,14 +216,14 @@ export class PredicatesInjector {
|
|
|
210
216
|
const nestedIsBackReferenceContext = isBackReference || isBackReferenceContext
|
|
211
217
|
// Build extended ancestor path for nested traversal
|
|
212
218
|
const nestedAncestorPath: Model.AnyRelationContext[] = [...ancestorPath, context]
|
|
213
|
-
return this.injectToWhere(relationWhere, context.targetEntity, false, context, nestedIsBackReferenceContext, nestedAncestorPath)
|
|
219
|
+
return this.injectToWhere(relationWhere, context.targetEntity, false, context, nestedIsBackReferenceContext, nestedAncestorPath, isQueryRoot)
|
|
214
220
|
},
|
|
215
221
|
})
|
|
216
222
|
}
|
|
217
223
|
const fieldsForPredicate = !isRoot
|
|
218
224
|
? fields
|
|
219
|
-
: fields.filter(it => this.predicateFactory.shouldApplyCellLevelPredicate(entity, Acl.Operation.read, it))
|
|
225
|
+
: fields.filter(it => this.predicateFactory.shouldApplyCellLevelPredicate(entity, Acl.Operation.read, it, isQueryRoot))
|
|
220
226
|
|
|
221
|
-
return this.createWhere(entity, fieldsForPredicate, resultWhere, relationContext, isBackReferenceContext, ancestorPath)
|
|
227
|
+
return this.createWhere(entity, fieldsForPredicate, resultWhere, relationContext, isBackReferenceContext, ancestorPath, isQueryRoot)
|
|
222
228
|
}
|
|
223
229
|
}
|
|
@@ -220,7 +220,8 @@ export class FieldsVisitor implements Model.RelationByTypeVisitor<void>, Model.C
|
|
|
220
220
|
}
|
|
221
221
|
|
|
222
222
|
private getRequiredPredicate(entity: Model.Entity, field: Model.AnyField): Acl.Predicate | undefined {
|
|
223
|
-
const
|
|
223
|
+
const isRoot = this.relationPath.length === 0
|
|
224
|
+
const fieldPredicate = this.predicateFactory.getFieldPredicate(entity, Acl.Operation.read, field.name, isRoot)
|
|
224
225
|
return fieldPredicate.isSameAsPrimary ? undefined : fieldPredicate.predicate
|
|
225
226
|
}
|
|
226
227
|
}
|
|
@@ -11,6 +11,13 @@ interface Test {
|
|
|
11
11
|
result: Acl.Permissions
|
|
12
12
|
}
|
|
13
13
|
|
|
14
|
+
interface ContextualTest {
|
|
15
|
+
acl: Acl.Schema
|
|
16
|
+
roles: string[]
|
|
17
|
+
rootResult: Acl.Permissions
|
|
18
|
+
allResult: Acl.Permissions
|
|
19
|
+
}
|
|
20
|
+
|
|
14
21
|
const execute = (test: Test) => {
|
|
15
22
|
const model: Model.Schema = new SchemaBuilder()
|
|
16
23
|
.entity('Entity1', e => e.column('lorem').column('bar'))
|
|
@@ -27,6 +34,21 @@ const execute = (test: Test) => {
|
|
|
27
34
|
assert.deepStrictEqual(test.acl, initialAcl)
|
|
28
35
|
}
|
|
29
36
|
|
|
37
|
+
const executeContextual = (test: ContextualTest) => {
|
|
38
|
+
const model: Model.Schema = new SchemaBuilder()
|
|
39
|
+
.entity('Entity1', e => e.column('lorem').column('bar'))
|
|
40
|
+
.entity('Entity2', e => e.oneHasOne('xyz', r => r.target('Entity1')))
|
|
41
|
+
.buildSchema()
|
|
42
|
+
const factory = new PermissionFactory()
|
|
43
|
+
const { root, all } = factory.createContextual({
|
|
44
|
+
...emptySchema,
|
|
45
|
+
model,
|
|
46
|
+
acl: test.acl,
|
|
47
|
+
}, test.roles)
|
|
48
|
+
assert.deepStrictEqual(root, test.rootResult)
|
|
49
|
+
assert.deepStrictEqual(all, test.allResult)
|
|
50
|
+
}
|
|
51
|
+
|
|
30
52
|
describe('Permission merger', () => {
|
|
31
53
|
it('merge inheritance', () => {
|
|
32
54
|
execute({
|
|
@@ -845,3 +867,143 @@ describe('Permission merger', () => {
|
|
|
845
867
|
})
|
|
846
868
|
})
|
|
847
869
|
})
|
|
870
|
+
|
|
871
|
+
describe('Contextual permission merger', () => {
|
|
872
|
+
it('createContextual: root drops through-only, all includes both', () => {
|
|
873
|
+
executeContextual({
|
|
874
|
+
acl: {
|
|
875
|
+
roles: {
|
|
876
|
+
throughRole: {
|
|
877
|
+
variables: {},
|
|
878
|
+
entities: {
|
|
879
|
+
Entity2: {
|
|
880
|
+
predicates: {
|
|
881
|
+
foo: { xyz: { lorem: { eq: 'through' } } },
|
|
882
|
+
},
|
|
883
|
+
operations: {
|
|
884
|
+
read: {
|
|
885
|
+
title: 'foo',
|
|
886
|
+
},
|
|
887
|
+
noRoot: ['read'],
|
|
888
|
+
},
|
|
889
|
+
},
|
|
890
|
+
},
|
|
891
|
+
},
|
|
892
|
+
rootRole: {
|
|
893
|
+
variables: {},
|
|
894
|
+
entities: {
|
|
895
|
+
Entity2: {
|
|
896
|
+
predicates: {
|
|
897
|
+
bar: { xyz: { lorem: { eq: 'root' } } },
|
|
898
|
+
},
|
|
899
|
+
operations: {
|
|
900
|
+
read: {
|
|
901
|
+
title: 'bar',
|
|
902
|
+
},
|
|
903
|
+
},
|
|
904
|
+
},
|
|
905
|
+
},
|
|
906
|
+
},
|
|
907
|
+
},
|
|
908
|
+
},
|
|
909
|
+
roles: ['throughRole', 'rootRole'],
|
|
910
|
+
rootResult: {
|
|
911
|
+
Entity2: {
|
|
912
|
+
operations: {
|
|
913
|
+
read: {
|
|
914
|
+
title: 'bar',
|
|
915
|
+
id: 'bar',
|
|
916
|
+
},
|
|
917
|
+
},
|
|
918
|
+
predicates: {
|
|
919
|
+
bar: {
|
|
920
|
+
xyz: {
|
|
921
|
+
lorem: { eq: 'root' },
|
|
922
|
+
},
|
|
923
|
+
},
|
|
924
|
+
},
|
|
925
|
+
},
|
|
926
|
+
},
|
|
927
|
+
allResult: {
|
|
928
|
+
Entity2: {
|
|
929
|
+
operations: {
|
|
930
|
+
read: {
|
|
931
|
+
title: '__merge__foo__bar',
|
|
932
|
+
id: '__merge__foo__bar',
|
|
933
|
+
},
|
|
934
|
+
},
|
|
935
|
+
predicates: {
|
|
936
|
+
__merge__foo__bar: {
|
|
937
|
+
or: [
|
|
938
|
+
{ xyz: { lorem: { eq: 'through' } } },
|
|
939
|
+
{ xyz: { lorem: { eq: 'root' } } },
|
|
940
|
+
],
|
|
941
|
+
},
|
|
942
|
+
},
|
|
943
|
+
},
|
|
944
|
+
},
|
|
945
|
+
})
|
|
946
|
+
})
|
|
947
|
+
|
|
948
|
+
it('createContextual: both roles without noRoot produce same root and all', () => {
|
|
949
|
+
executeContextual({
|
|
950
|
+
acl: {
|
|
951
|
+
roles: {
|
|
952
|
+
role1: {
|
|
953
|
+
variables: {},
|
|
954
|
+
entities: {
|
|
955
|
+
Entity1: {
|
|
956
|
+
predicates: {},
|
|
957
|
+
operations: {
|
|
958
|
+
read: {
|
|
959
|
+
id: true,
|
|
960
|
+
lorem: true,
|
|
961
|
+
},
|
|
962
|
+
},
|
|
963
|
+
},
|
|
964
|
+
},
|
|
965
|
+
},
|
|
966
|
+
role2: {
|
|
967
|
+
variables: {},
|
|
968
|
+
entities: {
|
|
969
|
+
Entity1: {
|
|
970
|
+
predicates: {},
|
|
971
|
+
operations: {
|
|
972
|
+
read: {
|
|
973
|
+
id: true,
|
|
974
|
+
bar: true,
|
|
975
|
+
},
|
|
976
|
+
},
|
|
977
|
+
},
|
|
978
|
+
},
|
|
979
|
+
},
|
|
980
|
+
},
|
|
981
|
+
},
|
|
982
|
+
roles: ['role1', 'role2'],
|
|
983
|
+
rootResult: {
|
|
984
|
+
Entity1: {
|
|
985
|
+
predicates: {},
|
|
986
|
+
operations: {
|
|
987
|
+
read: {
|
|
988
|
+
id: true,
|
|
989
|
+
lorem: true,
|
|
990
|
+
bar: true,
|
|
991
|
+
},
|
|
992
|
+
},
|
|
993
|
+
},
|
|
994
|
+
},
|
|
995
|
+
allResult: {
|
|
996
|
+
Entity1: {
|
|
997
|
+
predicates: {},
|
|
998
|
+
operations: {
|
|
999
|
+
read: {
|
|
1000
|
+
id: true,
|
|
1001
|
+
lorem: true,
|
|
1002
|
+
bar: true,
|
|
1003
|
+
},
|
|
1004
|
+
},
|
|
1005
|
+
},
|
|
1006
|
+
},
|
|
1007
|
+
})
|
|
1008
|
+
})
|
|
1009
|
+
})
|