@contember/engine-content-api 1.0.0-rc.7 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/src/acl/Authorizator.d.ts +8 -3
- package/dist/src/acl/Authorizator.d.ts.map +1 -1
- package/dist/src/acl/Authorizator.js +46 -0
- package/dist/src/acl/Authorizator.js.map +1 -1
- package/dist/src/acl/index.d.ts +0 -1
- package/dist/src/acl/index.d.ts.map +1 -1
- package/dist/src/acl/index.js +0 -1
- package/dist/src/acl/index.js.map +1 -1
- package/dist/src/introspection/IntrospectionSchemaFactory.js +2 -2
- package/dist/src/introspection/IntrospectionSchemaFactory.js.map +1 -1
- package/dist/src/mapper/select/SelectBuilder.d.ts.map +1 -1
- package/dist/src/mapper/select/SelectBuilder.js +1 -3
- package/dist/src/mapper/select/SelectBuilder.js.map +1 -1
- package/dist/src/resolvers/MutationResolver.d.ts.map +1 -1
- package/dist/src/resolvers/MutationResolver.js +4 -3
- package/dist/src/resolvers/MutationResolver.js.map +1 -1
- package/dist/src/schema/EntityTypeProvider.js +3 -3
- package/dist/src/schema/EntityTypeProvider.js.map +1 -1
- package/dist/src/schema/FieldAccessVisitor.js +2 -2
- package/dist/src/schema/FieldAccessVisitor.js.map +1 -1
- package/dist/src/schema/MutationProvider.js +2 -2
- package/dist/src/schema/MutationProvider.js.map +1 -1
- package/dist/src/schema/QueryProvider.js +1 -1
- package/dist/src/schema/QueryProvider.js.map +1 -1
- package/dist/src/schema/WhereTypeProvider.js +2 -2
- package/dist/src/schema/WhereTypeProvider.js.map +1 -1
- package/dist/src/schema/entities/FieldTypeVisitor.d.ts +5 -3
- package/dist/src/schema/entities/FieldTypeVisitor.d.ts.map +1 -1
- package/dist/src/schema/entities/FieldTypeVisitor.js +13 -2
- package/dist/src/schema/entities/FieldTypeVisitor.js.map +1 -1
- package/dist/src/schema/mutations/CreateEntityInputFieldVisitor.js +1 -1
- package/dist/src/schema/mutations/CreateEntityInputFieldVisitor.js.map +1 -1
- package/dist/src/schema/mutations/CreateEntityRelationAllowedOperationsVisitor.js +3 -3
- package/dist/src/schema/mutations/CreateEntityRelationAllowedOperationsVisitor.js.map +1 -1
- package/dist/src/schema/mutations/EntityInputProvider.js +1 -1
- package/dist/src/schema/mutations/EntityInputProvider.js.map +1 -1
- package/dist/src/schema/mutations/UpdateEntityInputFieldVisitor.js +1 -1
- package/dist/src/schema/mutations/UpdateEntityInputFieldVisitor.js.map +1 -1
- package/dist/src/schema/mutations/UpdateEntityRelationAllowedOperationsVisitor.d.ts.map +1 -1
- package/dist/src/schema/mutations/UpdateEntityRelationAllowedOperationsVisitor.js +5 -5
- package/dist/src/schema/mutations/UpdateEntityRelationAllowedOperationsVisitor.js.map +1 -1
- package/dist/src/schema/mutations/UpdateEntityRelationInputFieldVisitor.js +1 -1
- package/dist/src/schema/mutations/UpdateEntityRelationInputFieldVisitor.js.map +1 -1
- package/dist/src/tsconfig.tsbuildinfo +1 -1
- package/dist/tests/cases/integration/graphQlRequests/upsert/basicUpsert.test.js +45 -2
- package/dist/tests/cases/integration/graphQlRequests/upsert/basicUpsert.test.js.map +1 -1
- package/dist/tests/cases/integration/graphQlSchema/graphQlSchemaBuilder.test.js +45 -1
- package/dist/tests/cases/integration/graphQlSchema/graphQlSchemaBuilder.test.js.map +1 -1
- package/dist/tests/cases/integration/graphQlSchema/schema-acl-predicate.gql +110 -0
- package/dist/tests/cases/integration/graphQlSchema/schema-acl-predicate2.gql +110 -0
- package/dist/tests/src/test.js +1 -1
- package/dist/tests/src/test.js.map +1 -1
- package/dist/tests/tsconfig.tsbuildinfo +1 -1
- package/package.json +10 -10
- package/src/acl/Authorizator.ts +48 -4
- package/src/acl/index.ts +0 -1
- package/src/introspection/IntrospectionSchemaFactory.ts +2 -2
- package/src/mapper/select/SelectBuilder.ts +1 -3
- package/src/resolvers/MutationResolver.ts +4 -3
- package/src/schema/EntityTypeProvider.ts +3 -3
- package/src/schema/FieldAccessVisitor.ts +2 -2
- package/src/schema/MutationProvider.ts +2 -2
- package/src/schema/QueryProvider.ts +1 -1
- package/src/schema/WhereTypeProvider.ts +2 -2
- package/src/schema/entities/FieldTypeVisitor.ts +17 -8
- package/src/schema/mutations/CreateEntityInputFieldVisitor.ts +1 -1
- package/src/schema/mutations/CreateEntityRelationAllowedOperationsVisitor.ts +3 -3
- package/src/schema/mutations/EntityInputProvider.ts +1 -1
- package/src/schema/mutations/UpdateEntityInputFieldVisitor.ts +1 -1
- package/src/schema/mutations/UpdateEntityRelationAllowedOperationsVisitor.ts +5 -9
- package/src/schema/mutations/UpdateEntityRelationInputFieldVisitor.ts +1 -1
- package/tests/cases/integration/graphQlRequests/upsert/basicUpsert.test.ts +49 -2
- package/tests/cases/integration/graphQlSchema/graphQlSchemaBuilder.test.ts +57 -2
- package/tests/cases/integration/graphQlSchema/schema-acl-predicate.gql +110 -0
- package/tests/cases/integration/graphQlSchema/schema-acl-predicate2.gql +110 -0
- package/tests/src/test.ts +2 -2
- package/dist/src/acl/StaticAuthorizator.d.ts +0 -9
- package/dist/src/acl/StaticAuthorizator.d.ts.map +0 -1
- package/dist/src/acl/StaticAuthorizator.js +0 -32
- package/dist/src/acl/StaticAuthorizator.js.map +0 -1
- package/src/acl/StaticAuthorizator.ts +0 -29
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@contember/engine-content-api",
|
|
3
|
-
"version": "1.0.0
|
|
3
|
+
"version": "1.0.0",
|
|
4
4
|
"license": "Apache-2.0",
|
|
5
5
|
"main": "dist/src/index.js",
|
|
6
6
|
"typings": "dist/src/index.d.ts",
|
|
@@ -10,19 +10,19 @@
|
|
|
10
10
|
"test": "uvu dist/tests/cases/ \\.js$"
|
|
11
11
|
},
|
|
12
12
|
"dependencies": {
|
|
13
|
-
"@contember/database": "^1.0.0
|
|
14
|
-
"@contember/dic": "^1.0.0
|
|
15
|
-
"@contember/engine-common": "^1.0.0
|
|
16
|
-
"@contember/graphql-utils": "^1.0.0
|
|
17
|
-
"@contember/schema": "^1.0.0
|
|
18
|
-
"@contember/schema-utils": "^1.0.0
|
|
13
|
+
"@contember/database": "^1.0.0",
|
|
14
|
+
"@contember/dic": "^1.0.0",
|
|
15
|
+
"@contember/engine-common": "^1.0.0",
|
|
16
|
+
"@contember/graphql-utils": "^1.0.0",
|
|
17
|
+
"@contember/schema": "^1.0.0",
|
|
18
|
+
"@contember/schema-utils": "^1.0.0",
|
|
19
19
|
"@graphql-tools/schema": "^7.1.5",
|
|
20
20
|
"graphql-tag": "^2.12.5"
|
|
21
21
|
},
|
|
22
22
|
"devDependencies": {
|
|
23
|
-
"@contember/database-tester": "^1.0.0
|
|
24
|
-
"@contember/engine-api-tester": "^1.0.0
|
|
25
|
-
"@contember/schema-definition": "^1.0.0
|
|
23
|
+
"@contember/database-tester": "^1.0.0",
|
|
24
|
+
"@contember/engine-api-tester": "^1.0.0",
|
|
25
|
+
"@contember/schema-definition": "^1.0.0",
|
|
26
26
|
"@graphql-codegen/cli": "^1.15.2",
|
|
27
27
|
"@graphql-codegen/typescript": "^1.15.2",
|
|
28
28
|
"@graphql-codegen/typescript-operations": "^1.15.2",
|
package/src/acl/Authorizator.ts
CHANGED
|
@@ -1,8 +1,52 @@
|
|
|
1
1
|
import { Acl } from '@contember/schema'
|
|
2
2
|
|
|
3
|
-
export interface Authorizator {
|
|
4
|
-
isAllowed(operation: Acl.Operation, entity: string): boolean
|
|
5
|
-
isAllowed(operation: Acl.Operation.create | Acl.Operation.read | Acl.Operation.update, entity: string, field: string,): boolean
|
|
6
3
|
|
|
7
|
-
|
|
4
|
+
export type AuthorizationResult = 'yes' | 'no' | 'maybe'
|
|
5
|
+
|
|
6
|
+
export class Authorizator {
|
|
7
|
+
constructor(private readonly permissions: Acl.Permissions) {
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
getEntityPermission(operation: Acl.Operation, entity: string): AuthorizationResult {
|
|
11
|
+
if (!this.permissions[entity]) {
|
|
12
|
+
return 'no'
|
|
13
|
+
}
|
|
14
|
+
const entityPermissions: Acl.EntityPermissions = this.permissions[entity]
|
|
15
|
+
|
|
16
|
+
if (operation === Acl.Operation.delete) {
|
|
17
|
+
return this.predicateToResult(entityPermissions.operations.delete)
|
|
18
|
+
}
|
|
19
|
+
const fieldPermissions = entityPermissions.operations[operation]
|
|
20
|
+
if (!fieldPermissions) {
|
|
21
|
+
return 'no'
|
|
22
|
+
}
|
|
23
|
+
return Object.values(fieldPermissions).filter(it => !!it).length > 0 ? 'yes' : 'no'
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
getFieldPredicate(operation: Acl.Operation.create | Acl.Operation.read | Acl.Operation.update, entity: string, field: string): Acl.Predicate {
|
|
27
|
+
return this.permissions[entity]?.operations[operation]?.[field] ?? false
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
getFieldPermissions(operation: Acl.Operation.create | Acl.Operation.read | Acl.Operation.update, entity: string, field: string): AuthorizationResult {
|
|
31
|
+
const fieldPermissions = this.getFieldPredicate(operation, entity, field)
|
|
32
|
+
if (!fieldPermissions) {
|
|
33
|
+
return 'no'
|
|
34
|
+
}
|
|
35
|
+
return this.predicateToResult(fieldPermissions)
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
private predicateToResult(predicate: Acl.Predicate | undefined): AuthorizationResult {
|
|
39
|
+
if (!predicate) {
|
|
40
|
+
return 'no'
|
|
41
|
+
}
|
|
42
|
+
if (predicate === true) {
|
|
43
|
+
return 'yes'
|
|
44
|
+
}
|
|
45
|
+
return 'maybe'
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
isCustomPrimaryAllowed(entity: string): boolean {
|
|
50
|
+
return this.permissions?.[entity]?.operations?.customPrimary || false
|
|
51
|
+
}
|
|
8
52
|
}
|
package/src/acl/index.ts
CHANGED
|
@@ -107,7 +107,7 @@ export class IntrospectionSchemaFactory {
|
|
|
107
107
|
},
|
|
108
108
|
})
|
|
109
109
|
return Object.values(getEntity(this.model, entityName).fields)
|
|
110
|
-
.filter(it => this.authorizator.
|
|
110
|
+
.filter(it => this.authorizator.getFieldPermissions(Acl.Operation.read, entityName, it.name) !== 'no')
|
|
111
111
|
.map((it): ContentSchema._Column | ContentSchema._Relation => ({
|
|
112
112
|
...additionalInfo[it.name],
|
|
113
113
|
name: it.name,
|
|
@@ -154,7 +154,7 @@ export class IntrospectionSchemaFactory {
|
|
|
154
154
|
|
|
155
155
|
public create(): ContentSchema._Schema {
|
|
156
156
|
const entities = Object.values(this.model.entities)
|
|
157
|
-
.filter(it => this.authorizator.
|
|
157
|
+
.filter(it => this.authorizator.getEntityPermission(Acl.Operation.read, it.name) !== 'no')
|
|
158
158
|
.map(entity => ({
|
|
159
159
|
name: entity.name,
|
|
160
160
|
customPrimaryAllowed: this.authorizator.isCustomPrimaryAllowed(entity.name),
|
|
@@ -142,8 +142,6 @@ export class SelectBuilder {
|
|
|
142
142
|
this.qb = this.qb.select([columnPath.back().getAlias(), columnName], columnPath.getAlias())
|
|
143
143
|
const rows = await this.rows
|
|
144
144
|
const columnAlias = columnPath.getAlias()
|
|
145
|
-
return rows
|
|
146
|
-
.map((it): Input.PrimaryValue => it[columnAlias] as Input.PrimaryValue)
|
|
147
|
-
.filter((val, index, all) => all.indexOf(val) === index)
|
|
145
|
+
return Array.from(new Set(rows.map((it): Input.PrimaryValue => it[columnAlias] as Input.PrimaryValue)))
|
|
148
146
|
}
|
|
149
147
|
}
|
|
@@ -378,12 +378,13 @@ export class MutationResolver {
|
|
|
378
378
|
if (errorResponse) {
|
|
379
379
|
return errorResponse
|
|
380
380
|
}
|
|
381
|
-
const primary = getInsertPrimary(result)
|
|
382
|
-
|
|
381
|
+
const primary = getInsertPrimary(result)
|
|
382
|
+
const byResolved = primary ? { [entity.primary]: primary } : input.by
|
|
383
|
+
if (!byResolved) {
|
|
383
384
|
throw new ImplementationException('MutationResolver::resolveUpsertInternal does not handle result properly')
|
|
384
385
|
}
|
|
385
386
|
|
|
386
|
-
const nodes = await this.resolveResultNodes(mapper, entity,
|
|
387
|
+
const nodes = await this.resolveResultNodes(mapper, entity, byResolved, queryAst)
|
|
387
388
|
return {
|
|
388
389
|
ok: true,
|
|
389
390
|
validation: {
|
|
@@ -53,14 +53,14 @@ export class EntityTypeProvider {
|
|
|
53
53
|
}
|
|
54
54
|
|
|
55
55
|
public getEntity(entityName: string): GraphQLObjectType {
|
|
56
|
-
if (
|
|
56
|
+
if (this.authorizator.getEntityPermission(Acl.Operation.read, entityName) === 'no') {
|
|
57
57
|
throw new ImplementationException()
|
|
58
58
|
}
|
|
59
59
|
return this.entities(entityName)
|
|
60
60
|
}
|
|
61
61
|
|
|
62
62
|
public getConnection(entityName: string): GraphQLObjectType {
|
|
63
|
-
if (
|
|
63
|
+
if (this.authorizator.getEntityPermission(Acl.Operation.read, entityName) === 'no') {
|
|
64
64
|
throw new ImplementationException()
|
|
65
65
|
}
|
|
66
66
|
return this.connections(entityName)
|
|
@@ -100,7 +100,7 @@ export class EntityTypeProvider {
|
|
|
100
100
|
|
|
101
101
|
const fields: { [field: string]: GraphQLFieldConfig<any, any> } = accessibleFields.reduce(
|
|
102
102
|
(result, fieldName) => {
|
|
103
|
-
const fieldTypeVisitor = new FieldTypeVisitor(this.columnTypeResolver, this)
|
|
103
|
+
const fieldTypeVisitor = new FieldTypeVisitor(this.columnTypeResolver, this, this.authorizator)
|
|
104
104
|
const type: GraphQLOutputType = acceptFieldVisitor(this.schema, entity, fieldName, fieldTypeVisitor)
|
|
105
105
|
|
|
106
106
|
const fieldArgsVisitor = new FieldArgsVisitor(this.whereTypeProvider, this.orderByTypeProvider)
|
|
@@ -8,10 +8,10 @@ export class FieldAccessVisitor implements Model.ColumnVisitor<boolean>, Model.R
|
|
|
8
8
|
) {}
|
|
9
9
|
|
|
10
10
|
visitColumn(entity: Model.Entity, column: Model.AnyColumn) {
|
|
11
|
-
return this.authorizator.
|
|
11
|
+
return this.authorizator.getFieldPermissions(this.operation, entity.name, column.name) !== 'no'
|
|
12
12
|
}
|
|
13
13
|
|
|
14
14
|
visitRelation({}, {}, targetEntity: Model.Entity) {
|
|
15
|
-
return this.authorizator.
|
|
15
|
+
return this.authorizator.getEntityPermission(this.operation, targetEntity.name) !== 'no'
|
|
16
16
|
}
|
|
17
17
|
}
|
|
@@ -69,7 +69,7 @@ export class MutationProvider {
|
|
|
69
69
|
if (entity.view) {
|
|
70
70
|
return undefined
|
|
71
71
|
}
|
|
72
|
-
if (
|
|
72
|
+
if (this.authorizator.getEntityPermission(Acl.Operation.delete, entityName) === 'no') {
|
|
73
73
|
return undefined
|
|
74
74
|
}
|
|
75
75
|
const uniqueWhere = this.whereTypeProvider.getEntityUniqueWhereType(entityName)
|
|
@@ -180,7 +180,7 @@ export class MutationProvider {
|
|
|
180
180
|
errorMessage: { type: GraphQLString },
|
|
181
181
|
errors: { type: this.resultSchemaTypeProvider.errorListResultType },
|
|
182
182
|
}
|
|
183
|
-
if (this.authorizator.
|
|
183
|
+
if (this.authorizator.getEntityPermission(Acl.Operation.read, entityName) !== 'no') {
|
|
184
184
|
const nodeType = this.entityTypeProvider.getEntity(entityName)
|
|
185
185
|
fields.node = { type: nodeType, resolve: aliasAwareResolver }
|
|
186
186
|
}
|
|
@@ -19,7 +19,7 @@ export class QueryProvider {
|
|
|
19
19
|
) {}
|
|
20
20
|
|
|
21
21
|
public getQueries(entity: Model.Entity): { [fieldName: string]: GraphQLFieldConfig<any, Context, any> } {
|
|
22
|
-
if (
|
|
22
|
+
if (this.authorizator.getEntityPermission(Acl.Operation.read, entity.name) === 'no') {
|
|
23
23
|
return {}
|
|
24
24
|
}
|
|
25
25
|
return {
|
|
@@ -22,14 +22,14 @@ export class WhereTypeProvider {
|
|
|
22
22
|
) {}
|
|
23
23
|
|
|
24
24
|
public getEntityWhereType(entityName: string): GraphQLInputObjectType {
|
|
25
|
-
if (
|
|
25
|
+
if (this.authorizator.getEntityPermission(Acl.Operation.read, entityName) === 'no') {
|
|
26
26
|
throw new ImplementationException()
|
|
27
27
|
}
|
|
28
28
|
return this.whereSingleton(entityName)
|
|
29
29
|
}
|
|
30
30
|
|
|
31
31
|
public getEntityUniqueWhereType(entityName: string): undefined | GraphQLInputObjectType {
|
|
32
|
-
if (
|
|
32
|
+
if (this.authorizator.getEntityPermission(Acl.Operation.read, entityName) === 'no') {
|
|
33
33
|
return undefined
|
|
34
34
|
}
|
|
35
35
|
return this.uniqueWhereSingleton(entityName)
|
|
@@ -1,20 +1,29 @@
|
|
|
1
1
|
import { GraphQLList, GraphQLNonNull, GraphQLOutputType } from 'graphql'
|
|
2
|
-
import { Model } from '@contember/schema'
|
|
2
|
+
import { Acl, Model } from '@contember/schema'
|
|
3
3
|
import { ColumnTypeResolver } from '../ColumnTypeResolver'
|
|
4
4
|
import { EntityTypeProvider } from '../EntityTypeProvider'
|
|
5
|
+
import { Authorizator } from '../../acl'
|
|
6
|
+
import { ImplementationException } from '../../exception'
|
|
5
7
|
|
|
6
8
|
export class FieldTypeVisitor implements Model.ColumnVisitor<GraphQLOutputType>, Model.RelationByGenericTypeVisitor<GraphQLOutputType> {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
this.entityTypeProvider = entityTypeProvider
|
|
9
|
+
constructor(
|
|
10
|
+
private readonly columnTypeResolver: ColumnTypeResolver,
|
|
11
|
+
private readonly entityTypeProvider: EntityTypeProvider,
|
|
12
|
+
private readonly authorizator: Authorizator,
|
|
13
|
+
) {
|
|
13
14
|
}
|
|
14
15
|
|
|
15
16
|
public visitColumn(entity: Model.Entity, column: Model.AnyColumn): GraphQLOutputType {
|
|
16
17
|
const basicType = this.columnTypeResolver.getType(column)
|
|
17
|
-
|
|
18
|
+
const fieldPredicate = this.authorizator.getFieldPredicate(Acl.Operation.read, entity.name, column.name)
|
|
19
|
+
const idPredicate = this.authorizator.getFieldPredicate(Acl.Operation.read, entity.name, entity.primary)
|
|
20
|
+
if (!fieldPredicate || !idPredicate) {
|
|
21
|
+
throw new ImplementationException()
|
|
22
|
+
}
|
|
23
|
+
if (!column.nullable && (fieldPredicate === true || fieldPredicate === idPredicate)) {
|
|
24
|
+
return new GraphQLNonNull(basicType)
|
|
25
|
+
}
|
|
26
|
+
return basicType
|
|
18
27
|
}
|
|
19
28
|
|
|
20
29
|
public visitHasMany(entity: Model.Entity, relation: Model.Relation): GraphQLOutputType {
|
|
@@ -19,7 +19,7 @@ export class CreateEntityInputFieldVisitor implements
|
|
|
19
19
|
if (entity.primary === column.name && !this.authorizator.isCustomPrimaryAllowed(entity.name)) {
|
|
20
20
|
return undefined
|
|
21
21
|
}
|
|
22
|
-
if (
|
|
22
|
+
if (this.authorizator.getFieldPermissions(Acl.Operation.create, entity.name, column.name) === 'no') {
|
|
23
23
|
return undefined
|
|
24
24
|
}
|
|
25
25
|
const type = this.columnTypeResolver.getType(column)
|
|
@@ -65,9 +65,9 @@ export class CreateEntityRelationAllowedOperationsVisitor implements
|
|
|
65
65
|
): Input.CreateRelationOperation[] {
|
|
66
66
|
const result: Input.CreateRelationOperation[] = []
|
|
67
67
|
|
|
68
|
-
const canReadTargetEntity = this.authorizator.
|
|
69
|
-
const canCreateTargetEntity = this.authorizator.
|
|
70
|
-
const canCreateOwning = this.authorizator.
|
|
68
|
+
const canReadTargetEntity = this.authorizator.getEntityPermission(Acl.Operation.read, targetEntity.name) !== 'no'
|
|
69
|
+
const canCreateTargetEntity = this.authorizator.getEntityPermission(Acl.Operation.create, targetEntity.name) !== 'no'
|
|
70
|
+
const canCreateOwning = this.authorizator.getFieldPermissions(Acl.Operation.create, owningEntity.name, owningRelation.name) !== 'no'
|
|
71
71
|
|
|
72
72
|
if (canReadTargetEntity && canCreateOwning) {
|
|
73
73
|
result.push(Input.CreateRelationOperation.connect)
|
|
@@ -51,7 +51,7 @@ export class EntityInputProvider<Operation extends EntityInputType> {
|
|
|
51
51
|
throw new ImplementationException(`EntityInputProvider: Invalid operation ${this.operation}`)
|
|
52
52
|
}
|
|
53
53
|
})()
|
|
54
|
-
if (
|
|
54
|
+
if (this.authorizator.getEntityPermission(operation, entity.name) === 'no') {
|
|
55
55
|
return undefined
|
|
56
56
|
}
|
|
57
57
|
|
|
@@ -18,7 +18,7 @@ export class UpdateEntityInputFieldVisitor implements
|
|
|
18
18
|
if (entity.primary === column.name) {
|
|
19
19
|
return undefined
|
|
20
20
|
}
|
|
21
|
-
if (
|
|
21
|
+
if (this.authorizator.getFieldPermissions(Acl.Operation.update, entity.name, column.name) === 'no') {
|
|
22
22
|
return undefined
|
|
23
23
|
}
|
|
24
24
|
const type = this.columnTypeResolver.getType(column)
|
|
@@ -76,15 +76,11 @@ export class UpdateEntityRelationAllowedOperationsVisitor implements
|
|
|
76
76
|
): Input.UpdateRelationOperation[] {
|
|
77
77
|
const result: Input.UpdateRelationOperation[] = []
|
|
78
78
|
|
|
79
|
-
const canReadTargetEntity = this.authorizator.
|
|
80
|
-
const canCreateTargetEntity = this.authorizator.
|
|
81
|
-
const canUpdateTargetEntity = this.authorizator.
|
|
82
|
-
const canDeleteTargetEntity = this.authorizator.
|
|
83
|
-
const canUpdateOwningRelation = this.authorizator.
|
|
84
|
-
Acl.Operation.update,
|
|
85
|
-
owningEntity.name,
|
|
86
|
-
owningRelation.name,
|
|
87
|
-
)
|
|
79
|
+
const canReadTargetEntity = this.authorizator.getEntityPermission(Acl.Operation.read, targetEntity.name) !== 'no'
|
|
80
|
+
const canCreateTargetEntity = this.authorizator.getEntityPermission(Acl.Operation.create, targetEntity.name) !== 'no'
|
|
81
|
+
const canUpdateTargetEntity = this.authorizator.getEntityPermission(Acl.Operation.update, targetEntity.name) !== 'no'
|
|
82
|
+
const canDeleteTargetEntity = this.authorizator.getEntityPermission(Acl.Operation.delete, targetEntity.name) !== 'no'
|
|
83
|
+
const canUpdateOwningRelation = this.authorizator.getFieldPermissions(Acl.Operation.update, owningEntity.name, owningRelation.name) !== 'no'
|
|
88
84
|
|
|
89
85
|
if (canReadTargetEntity && canUpdateOwningRelation) {
|
|
90
86
|
result.push(Input.UpdateRelationOperation.connect)
|
|
@@ -174,7 +174,7 @@ export class UpdateEntityRelationInputFieldVisitor implements
|
|
|
174
174
|
}
|
|
175
175
|
|
|
176
176
|
private createWhereInput(targetEntity: Model.Entity): GraphQLInputFieldConfig | undefined {
|
|
177
|
-
if (
|
|
177
|
+
if (this.authorizator.getEntityPermission(Acl.Operation.read, targetEntity.name) === 'no') {
|
|
178
178
|
return undefined
|
|
179
179
|
}
|
|
180
180
|
const uniqueWhere = this.whereTypeBuilder.getEntityUniqueWhereType(targetEntity.name)
|
|
@@ -98,8 +98,54 @@ test('upsert author (exists)', async () => {
|
|
|
98
98
|
response: { rowCount: 1 },
|
|
99
99
|
},
|
|
100
100
|
{
|
|
101
|
-
sql: SQL`select "root_"."id" as "root_id" from "public"."author" as "root_" where "root_"."
|
|
102
|
-
parameters: [
|
|
101
|
+
sql: SQL`select "root_"."id" as "root_id" from "public"."author" as "root_" where "root_"."slug" = ?`,
|
|
102
|
+
parameters: ['john-doe'],
|
|
103
|
+
response: { rows: [{ root_id: testUuid(2) }] },
|
|
104
|
+
},
|
|
105
|
+
]),
|
|
106
|
+
],
|
|
107
|
+
return: {
|
|
108
|
+
data: {
|
|
109
|
+
upsertAuthor: {
|
|
110
|
+
ok: true,
|
|
111
|
+
node: {
|
|
112
|
+
id: testUuid(2),
|
|
113
|
+
},
|
|
114
|
+
},
|
|
115
|
+
},
|
|
116
|
+
},
|
|
117
|
+
})
|
|
118
|
+
})
|
|
119
|
+
|
|
120
|
+
test('upsert author (exists) with noop update)', async () => {
|
|
121
|
+
await execute({
|
|
122
|
+
schema: new SchemaBuilder()
|
|
123
|
+
.entity('Author', entity =>
|
|
124
|
+
entity
|
|
125
|
+
.column('name', c => c.type(Model.ColumnType.String))
|
|
126
|
+
.column('slug', c => c.type(Model.ColumnType.String).unique()),
|
|
127
|
+
)
|
|
128
|
+
.buildSchema(),
|
|
129
|
+
query: GQL`
|
|
130
|
+
mutation {
|
|
131
|
+
upsertAuthor(by: {slug: "john-doe"}, update: {}, create: {slug: "john-doe", name: "John Doe"}) {
|
|
132
|
+
ok
|
|
133
|
+
node {
|
|
134
|
+
id
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}`,
|
|
138
|
+
executes: [
|
|
139
|
+
...sqlTransaction([
|
|
140
|
+
{
|
|
141
|
+
sql: SQL`select "root_"."id"
|
|
142
|
+
from "public"."author" as "root_" where "root_"."slug" = ?`,
|
|
143
|
+
parameters: ['john-doe'],
|
|
144
|
+
response: { rows: [{ id: testUuid(2) }] },
|
|
145
|
+
},
|
|
146
|
+
{
|
|
147
|
+
sql: SQL`select "root_"."id" as "root_id" from "public"."author" as "root_" where "root_"."slug" = ?`,
|
|
148
|
+
parameters: ['john-doe'],
|
|
103
149
|
response: { rows: [{ root_id: testUuid(2) }] },
|
|
104
150
|
},
|
|
105
151
|
]),
|
|
@@ -117,3 +163,4 @@ test('upsert author (exists)', async () => {
|
|
|
117
163
|
})
|
|
118
164
|
})
|
|
119
165
|
test.run()
|
|
166
|
+
|
|
@@ -3,7 +3,7 @@ import { Acl, Model } from '@contember/schema'
|
|
|
3
3
|
import * as path from 'path'
|
|
4
4
|
|
|
5
5
|
import { AllowAllPermissionFactory, SchemaBuilder, SchemaDefinition } from '@contember/schema-definition'
|
|
6
|
-
import { GraphQlSchemaBuilderFactory,
|
|
6
|
+
import { GraphQlSchemaBuilderFactory, Authorizator } from '../../../../src'
|
|
7
7
|
import * as model from './model'
|
|
8
8
|
import { promises as fs } from 'fs'
|
|
9
9
|
import * as assert from 'uvu/assert'
|
|
@@ -23,7 +23,7 @@ const testSchema = async (test: Test) => {
|
|
|
23
23
|
const schema = schemaResult instanceof SchemaBuilder ? schemaResult.buildSchema() : schemaResult
|
|
24
24
|
const schemaWithAcl = { ...schema, acl: { roles: {}, variables: {} } }
|
|
25
25
|
const permissions = test.permissions(schemaWithAcl)
|
|
26
|
-
const authorizator = new
|
|
26
|
+
const authorizator = new Authorizator(permissions)
|
|
27
27
|
const graphQlSchemaBuilder = schemaFactory.create(schemaWithAcl, authorizator)
|
|
28
28
|
const graphQlSchema = graphQlSchemaBuilder.build()
|
|
29
29
|
|
|
@@ -110,6 +110,61 @@ graphqlSchemaBuilderTest('restricted access to fields by permissions', async ()
|
|
|
110
110
|
graphQlSchemaFile: 'schema-acl.gql',
|
|
111
111
|
})
|
|
112
112
|
})
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
graphqlSchemaBuilderTest('conditionally restricted read of some fields', async () => {
|
|
116
|
+
await testSchema({
|
|
117
|
+
schema: builder =>
|
|
118
|
+
builder.entity('Test', e =>
|
|
119
|
+
e
|
|
120
|
+
.column('a', c => c.type(Model.ColumnType.String).notNull()),
|
|
121
|
+
),
|
|
122
|
+
permissions: () => ({
|
|
123
|
+
Test: {
|
|
124
|
+
predicates: {
|
|
125
|
+
testPredicate: {
|
|
126
|
+
a: { eq: 'Foo' },
|
|
127
|
+
},
|
|
128
|
+
},
|
|
129
|
+
operations: {
|
|
130
|
+
read: {
|
|
131
|
+
id: true,
|
|
132
|
+
a: 'testPredicate',
|
|
133
|
+
},
|
|
134
|
+
},
|
|
135
|
+
},
|
|
136
|
+
}),
|
|
137
|
+
graphQlSchemaFile: 'schema-acl-predicate.gql',
|
|
138
|
+
})
|
|
139
|
+
})
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
graphqlSchemaBuilderTest('conditionally restricted read of whole row', async () => {
|
|
143
|
+
await testSchema({
|
|
144
|
+
schema: builder =>
|
|
145
|
+
builder.entity('Test', e =>
|
|
146
|
+
e
|
|
147
|
+
.column('a', c => c.type(Model.ColumnType.String).notNull()),
|
|
148
|
+
),
|
|
149
|
+
permissions: () => ({
|
|
150
|
+
Test: {
|
|
151
|
+
predicates: {
|
|
152
|
+
testPredicate: {
|
|
153
|
+
a: { eq: 'Foo' },
|
|
154
|
+
},
|
|
155
|
+
},
|
|
156
|
+
operations: {
|
|
157
|
+
read: {
|
|
158
|
+
id: 'testPredicate',
|
|
159
|
+
a: 'testPredicate',
|
|
160
|
+
},
|
|
161
|
+
},
|
|
162
|
+
},
|
|
163
|
+
}),
|
|
164
|
+
graphQlSchemaFile: 'schema-acl-predicate2.gql',
|
|
165
|
+
})
|
|
166
|
+
})
|
|
167
|
+
|
|
113
168
|
const oneHasManySchema = (builder: SchemaBuilder) =>
|
|
114
169
|
builder.entity('Root', e =>
|
|
115
170
|
e
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
type Query {
|
|
2
|
+
getTest(by: TestUniqueWhere!, filter: TestWhere): Test
|
|
3
|
+
listTest(filter: TestWhere, orderBy: [TestOrderBy!], offset: Int, limit: Int): [Test!]!
|
|
4
|
+
paginateTest(filter: TestWhere, orderBy: [TestOrderBy!], skip: Int, first: Int): TestConnection!
|
|
5
|
+
transaction: QueryTransaction
|
|
6
|
+
_info: Info
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
type Test {
|
|
10
|
+
_meta: TestMeta
|
|
11
|
+
id: UUID!
|
|
12
|
+
a: String
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
type TestMeta {
|
|
16
|
+
id: FieldMeta
|
|
17
|
+
a: FieldMeta
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
type FieldMeta {
|
|
21
|
+
readable: Boolean
|
|
22
|
+
updatable: Boolean
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
scalar UUID
|
|
26
|
+
|
|
27
|
+
input TestUniqueWhere {
|
|
28
|
+
id: UUID
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
input TestWhere {
|
|
32
|
+
id: UUIDCondition
|
|
33
|
+
a: StringCondition
|
|
34
|
+
and: [TestWhere]
|
|
35
|
+
or: [TestWhere]
|
|
36
|
+
not: TestWhere
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
input UUIDCondition {
|
|
40
|
+
and: [UUIDCondition!]
|
|
41
|
+
or: [UUIDCondition!]
|
|
42
|
+
not: UUIDCondition
|
|
43
|
+
null: Boolean
|
|
44
|
+
isNull: Boolean
|
|
45
|
+
eq: UUID
|
|
46
|
+
notEq: UUID
|
|
47
|
+
in: [UUID!]
|
|
48
|
+
notIn: [UUID!]
|
|
49
|
+
lt: UUID
|
|
50
|
+
lte: UUID
|
|
51
|
+
gt: UUID
|
|
52
|
+
gte: UUID
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
input StringCondition {
|
|
56
|
+
and: [StringCondition!]
|
|
57
|
+
or: [StringCondition!]
|
|
58
|
+
not: StringCondition
|
|
59
|
+
null: Boolean
|
|
60
|
+
isNull: Boolean
|
|
61
|
+
eq: String
|
|
62
|
+
notEq: String
|
|
63
|
+
in: [String!]
|
|
64
|
+
notIn: [String!]
|
|
65
|
+
lt: String
|
|
66
|
+
lte: String
|
|
67
|
+
gt: String
|
|
68
|
+
gte: String
|
|
69
|
+
contains: String
|
|
70
|
+
startsWith: String
|
|
71
|
+
endsWith: String
|
|
72
|
+
containsCI: String
|
|
73
|
+
startsWithCI: String
|
|
74
|
+
endsWithCI: String
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
input TestOrderBy {
|
|
78
|
+
_random: Boolean
|
|
79
|
+
_randomSeeded: Int
|
|
80
|
+
id: OrderDirection
|
|
81
|
+
a: OrderDirection
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
enum OrderDirection {
|
|
85
|
+
asc
|
|
86
|
+
desc
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
type TestConnection {
|
|
90
|
+
pageInfo: PageInfo!
|
|
91
|
+
edges: [TestEdge!]!
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
type PageInfo {
|
|
95
|
+
totalCount: Int!
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
type TestEdge {
|
|
99
|
+
node: Test!
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
type QueryTransaction {
|
|
103
|
+
getTest(by: TestUniqueWhere!, filter: TestWhere): Test
|
|
104
|
+
listTest(filter: TestWhere, orderBy: [TestOrderBy!], offset: Int, limit: Int): [Test!]!
|
|
105
|
+
paginateTest(filter: TestWhere, orderBy: [TestOrderBy!], skip: Int, first: Int): TestConnection!
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
type Info {
|
|
109
|
+
description: String
|
|
110
|
+
}
|