@contember/schema-utils 2.2.0-alpha.2 → 2.2.0-alpha.5
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/src/acl/schemaUtils.cjs +4 -1
- package/dist/development/src/acl/schemaUtils.cjs.map +1 -1
- package/dist/development/src/acl/schemaUtils.js +4 -1
- package/dist/development/src/acl/schemaUtils.js.map +1 -1
- package/dist/development/src/definition-generator/AclDefinitionCodeGenerator.cjs +19 -5
- package/dist/development/src/definition-generator/AclDefinitionCodeGenerator.cjs.map +1 -1
- package/dist/development/src/definition-generator/AclDefinitionCodeGenerator.js +19 -5
- package/dist/development/src/definition-generator/AclDefinitionCodeGenerator.js.map +1 -1
- package/dist/development/src/validation/MembershipResolver.cjs +3 -2
- package/dist/development/src/validation/MembershipResolver.cjs.map +1 -1
- package/dist/development/src/validation/MembershipResolver.js +3 -2
- package/dist/development/src/validation/MembershipResolver.js.map +1 -1
- package/dist/production/src/acl/schemaUtils.cjs +4 -1
- package/dist/production/src/acl/schemaUtils.cjs.map +1 -1
- package/dist/production/src/acl/schemaUtils.js +4 -1
- package/dist/production/src/acl/schemaUtils.js.map +1 -1
- package/dist/production/src/definition-generator/AclDefinitionCodeGenerator.cjs +19 -5
- package/dist/production/src/definition-generator/AclDefinitionCodeGenerator.cjs.map +1 -1
- package/dist/production/src/definition-generator/AclDefinitionCodeGenerator.js +19 -5
- package/dist/production/src/definition-generator/AclDefinitionCodeGenerator.js.map +1 -1
- package/dist/production/src/validation/MembershipResolver.cjs +3 -2
- package/dist/production/src/validation/MembershipResolver.cjs.map +1 -1
- package/dist/production/src/validation/MembershipResolver.js +3 -2
- package/dist/production/src/validation/MembershipResolver.js.map +1 -1
- package/dist/tests/tsconfig.tsbuildinfo +1 -1
- package/dist/types/acl/AllowAllPermissionFactory.d.ts.map +1 -1
- package/dist/types/acl/PredicateDefinitionProcessor.d.ts.map +1 -1
- package/dist/types/acl/schemaUtils.d.ts.map +1 -1
- package/dist/types/dataUtils.d.ts +1 -1
- package/dist/types/dataUtils.d.ts.map +1 -1
- package/dist/types/definition-generator/AclDefinitionCodeGenerator.d.ts +6 -0
- package/dist/types/definition-generator/AclDefinitionCodeGenerator.d.ts.map +1 -1
- package/dist/types/definition-generator/DefinitionCodeGenerator.d.ts.map +1 -1
- package/dist/types/definition-generator/DefinitionNamingConventions.d.ts.map +1 -1
- package/dist/types/definition-generator/TsDefinitionGenerator.d.ts.map +1 -1
- package/dist/types/json-schema/collectUnsupportedKeywords.d.ts.map +1 -1
- package/dist/types/json-schema/jsonSchemaToTsType.d.ts.map +1 -1
- package/dist/types/json-schema/keywords.d.ts +2 -2
- package/dist/types/json-schema/keywords.d.ts.map +1 -1
- package/dist/types/lax/LaxSchemaConverter.d.ts.map +1 -1
- package/dist/types/lax/schema.d.ts +87 -87
- package/dist/types/model/NamingConventions.d.ts.map +1 -1
- package/dist/types/model/modelUtils.d.ts.map +1 -1
- package/dist/types/model/resolveDefaultColumnType.d.ts.map +1 -1
- package/dist/types/schemaChecksum.d.ts.map +1 -1
- package/dist/types/schemaFilter.d.ts.map +1 -1
- package/dist/types/schemaNormalizer.d.ts.map +1 -1
- package/dist/types/tsconfig.tsbuildinfo +1 -1
- package/dist/types/type-schema/condition.d.ts.map +1 -1
- package/dist/types/type-schema/settings.d.ts +3 -3
- package/dist/types/type-schema/where.d.ts.map +1 -1
- package/dist/types/utils/deepCompare.d.ts.map +1 -1
- package/dist/types/utils/isIt.d.ts.map +1 -1
- package/dist/types/utils/printJsValue.d.ts.map +1 -1
- package/dist/types/validation/AclValidator.d.ts.map +1 -1
- package/dist/types/validation/ActionsValidator.d.ts.map +1 -1
- package/dist/types/validation/MembershipResolver.d.ts.map +1 -1
- package/dist/types/validation/ModelValidator.d.ts.map +1 -1
- package/dist/types/validation/SchemaValidator.d.ts.map +1 -1
- package/dist/types/validation/ValidationValidator.d.ts.map +1 -1
- package/dist/types/validation/errors.d.ts.map +1 -1
- package/package.json +4 -4
- package/src/acl/schemaUtils.ts +8 -1
- package/src/definition-generator/AclDefinitionCodeGenerator.ts +22 -6
- package/src/validation/MembershipResolver.ts +5 -2
- package/tests/cases/unit/membershipResolver.test.ts +36 -1
- package/tests/cases/unit/tsDefinitionGenerator.test.ts +36 -0
|
@@ -54,7 +54,7 @@ export class AclDefinitionCodeGenerator {
|
|
|
54
54
|
|
|
55
55
|
public generateEntityAcl({ entity, schema }: { entity: Model.Entity; schema: Schema }): string {
|
|
56
56
|
const aclOutput: string[] = []
|
|
57
|
-
const
|
|
57
|
+
const nonPrimaryFields = Object.keys(entity.fields).filter(it => it !== entity.primary)
|
|
58
58
|
for (const [roleName, roleDefinition] of Object.entries(schema.acl.roles)) {
|
|
59
59
|
const entityPermission = roleDefinition.entities[entity.name]
|
|
60
60
|
if (!entityPermission) {
|
|
@@ -65,7 +65,8 @@ export class AclDefinitionCodeGenerator {
|
|
|
65
65
|
const operations = this.getMatchingOperations({
|
|
66
66
|
predicate: predicateName,
|
|
67
67
|
operations: entityPermission.operations,
|
|
68
|
-
|
|
68
|
+
entity,
|
|
69
|
+
nonPrimaryFields,
|
|
69
70
|
})
|
|
70
71
|
if (Object.keys(operations).length > 0) {
|
|
71
72
|
const processor = new PredicateDefinitionProcessor(schema.model)
|
|
@@ -87,7 +88,8 @@ export class AclDefinitionCodeGenerator {
|
|
|
87
88
|
const trueOperations = this.getMatchingOperations({
|
|
88
89
|
predicate: true,
|
|
89
90
|
operations: entityPermission.operations,
|
|
90
|
-
|
|
91
|
+
entity,
|
|
92
|
+
nonPrimaryFields,
|
|
91
93
|
})
|
|
92
94
|
if (Object.keys(trueOperations).length > 0) {
|
|
93
95
|
const aclDefinition = printJsValue({ ...trueOperations }, indentFirstLevel)
|
|
@@ -101,16 +103,17 @@ export class AclDefinitionCodeGenerator {
|
|
|
101
103
|
return `${aclOutput.join('')}`
|
|
102
104
|
}
|
|
103
105
|
|
|
104
|
-
private getMatchingOperations({ operations, predicate,
|
|
106
|
+
private getMatchingOperations({ operations, predicate, entity, nonPrimaryFields }: {
|
|
105
107
|
operations: Acl.EntityOperations
|
|
106
108
|
predicate: Acl.Predicate
|
|
107
|
-
|
|
109
|
+
entity: Model.Entity
|
|
110
|
+
nonPrimaryFields: string[]
|
|
108
111
|
}): { read?: string[] | true; create?: string[] | boolean; update?: string[] | boolean; delete?: true } {
|
|
109
112
|
const result: ReturnType<AclDefinitionCodeGenerator['getMatchingOperations']> = {}
|
|
110
113
|
for (const op of ['read', 'create', 'update'] as const) {
|
|
111
114
|
const fields = Object.entries(operations[op] ?? {}).filter(([, it]) => it === predicate).map(([it]) => it)
|
|
112
115
|
if (fields.length === 0) {
|
|
113
|
-
} else if (fields
|
|
116
|
+
} else if (this.coversEveryNonPrimaryField(fields, entity, nonPrimaryFields)) {
|
|
114
117
|
result[op] = true
|
|
115
118
|
} else {
|
|
116
119
|
result[op] = fields
|
|
@@ -122,4 +125,17 @@ export class AclDefinitionCodeGenerator {
|
|
|
122
125
|
|
|
123
126
|
return result
|
|
124
127
|
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* `read: true` re-parses as every field except the primary, so it may only stand in for a grant that
|
|
131
|
+
* covers exactly that set. Comparing counts instead treated a grant naming the primary plus all but
|
|
132
|
+
* one field as complete, and regenerating it silently granted the missing field.
|
|
133
|
+
*/
|
|
134
|
+
private coversEveryNonPrimaryField(fields: string[], entity: Model.Entity, nonPrimaryFields: string[]): boolean {
|
|
135
|
+
if (fields.includes(entity.primary)) {
|
|
136
|
+
return false
|
|
137
|
+
}
|
|
138
|
+
const granted = new Set(fields)
|
|
139
|
+
return nonPrimaryFields.every(it => granted.has(it))
|
|
140
|
+
}
|
|
125
141
|
}
|
|
@@ -19,13 +19,16 @@ export class MembershipResolver {
|
|
|
19
19
|
const parsed: ParsedMembership[] = []
|
|
20
20
|
const roles = acl.roles
|
|
21
21
|
for (const membership of memberships) {
|
|
22
|
-
|
|
22
|
+
// own-property lookup so a role named `__proto__` / `constructor` resolves to "not found"
|
|
23
|
+
// rather than an inherited Object.prototype member
|
|
24
|
+
if (!Object.prototype.hasOwnProperty.call(roles, membership.role)) {
|
|
23
25
|
errors.push(new MembershipValidationError(membership.role, MembershipValidationErrorType.ROLE_NOT_FOUND))
|
|
24
26
|
continue
|
|
25
27
|
}
|
|
26
28
|
const roleVariables = getRoleVariables(membership.role, acl)
|
|
27
29
|
for (const variable of membership.variables) {
|
|
28
|
-
|
|
30
|
+
const roleVariable = Object.prototype.hasOwnProperty.call(roleVariables, variable.name) ? roleVariables[variable.name] : undefined
|
|
31
|
+
if (!roleVariable || (!isAssumed && roleVariable.type === Acl.VariableType.predefined)) {
|
|
29
32
|
errors.push(
|
|
30
33
|
new MembershipValidationError(
|
|
31
34
|
membership.role,
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { expect, test } from 'bun:test'
|
|
2
|
-
import { MembershipResolver, MembershipValidationErrorType } from '../../../src/index.js'
|
|
2
|
+
import { getRoleVariables, MembershipResolver, MembershipValidationErrorType } from '../../../src/index.js'
|
|
3
3
|
import { Acl } from '@contember/schema'
|
|
4
4
|
|
|
5
5
|
const id1 = 'bff057b3-11f7-4bc7-abe3-1f2ef266824d'
|
|
@@ -268,3 +268,38 @@ test('use fallback value', () => {
|
|
|
268
268
|
{ role: 'editor', variables: [{ name: 'localeID', condition: { always: true } }] },
|
|
269
269
|
])
|
|
270
270
|
})
|
|
271
|
+
|
|
272
|
+
// Own-property hardening: a role / variable named after an inherited Object.prototype member must resolve
|
|
273
|
+
// to "not found" rather than the inherited member (which would otherwise throw on `role.inherits` /
|
|
274
|
+
// `role.variables`, or pass as a defined variable). Exercised here in schema-utils itself because these
|
|
275
|
+
// helpers are shared cross-package (the direct add-member path and the A09 IdP claim mapping both use them).
|
|
276
|
+
test('a role named after a prototype member resolves to ROLE_NOT_FOUND (own-property, no throw)', () => {
|
|
277
|
+
const reader = new MembershipResolver()
|
|
278
|
+
for (const role of ['__proto__', 'constructor', 'toString', 'hasOwnProperty']) {
|
|
279
|
+
const result = reader.resolve({ roles: {} }, [{ role, variables: [] }], MembershipResolver.UnknownIdentity, false)
|
|
280
|
+
expect(result.errors).toEqual([{ error: MembershipValidationErrorType.ROLE_NOT_FOUND, role, variable: undefined }])
|
|
281
|
+
expect(result.memberships).toStrictEqual([])
|
|
282
|
+
}
|
|
283
|
+
})
|
|
284
|
+
|
|
285
|
+
test('a variable named after a prototype member resolves to VARIABLE_NOT_FOUND (own-property)', () => {
|
|
286
|
+
const reader = new MembershipResolver()
|
|
287
|
+
const acl: Acl.Schema = { roles: { editor: { variables: {}, entities: {} } } }
|
|
288
|
+
for (const name of ['__proto__', 'constructor', 'toString', 'hasOwnProperty']) {
|
|
289
|
+
const result = reader.resolve(acl, [{ role: 'editor', variables: [{ name, values: ['x'] }] }], MembershipResolver.UnknownIdentity, false)
|
|
290
|
+
expect(result.errors).toEqual([{ error: MembershipValidationErrorType.VARIABLE_NOT_FOUND, role: 'editor', variable: name }])
|
|
291
|
+
}
|
|
292
|
+
})
|
|
293
|
+
|
|
294
|
+
test('getRoleVariables: a prototype-named role, and an `inherits` pointing at a missing role, resolve to {} (own-property, no throw)', () => {
|
|
295
|
+
for (const role of ['__proto__', 'constructor', 'toString', 'hasOwnProperty']) {
|
|
296
|
+
expect(getRoleVariables(role, { roles: {} })).toEqual({})
|
|
297
|
+
}
|
|
298
|
+
// an `inherits` entry naming a role absent from the schema must resolve to no variables, not throw
|
|
299
|
+
const acl: Acl.Schema = {
|
|
300
|
+
roles: {
|
|
301
|
+
editor: { variables: { a: { type: Acl.VariableType.entity, entityName: 'A' } }, entities: {}, inherits: ['ghost'] },
|
|
302
|
+
},
|
|
303
|
+
}
|
|
304
|
+
expect(getRoleVariables('editor', acl)).toEqual({ a: { type: Acl.VariableType.entity, entityName: 'A' } })
|
|
305
|
+
})
|
|
@@ -73,3 +73,39 @@ test('definition generator round-trips per-column index options', () => {
|
|
|
73
73
|
expect(generated).toContain(`@c.Index({ fields: ['title', { field: 'rank', order: 'desc', nulls: 'last' }] })`)
|
|
74
74
|
expect(generated).toContain(`@c.Index({ fields: [{ field: 'title', opClass: 'text_pattern_ops' }] })`)
|
|
75
75
|
})
|
|
76
|
+
|
|
77
|
+
namespace ExplicitPrimaryReadModel {
|
|
78
|
+
export const editorRole = c.createRole('editor')
|
|
79
|
+
|
|
80
|
+
@c.Allow(editorRole, { read: ['id', 'a'] })
|
|
81
|
+
export class Article {
|
|
82
|
+
a = c.stringColumn()
|
|
83
|
+
b = c.stringColumn()
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
test('definition generator does not widen a grant that names the primary key', () => {
|
|
88
|
+
const generator = new DefinitionCodeGenerator()
|
|
89
|
+
const generated = generator.generate(createSchema(ExplicitPrimaryReadModel))
|
|
90
|
+
// regression: the `read: true` collapse compared counts, and `fields` includes the primary while
|
|
91
|
+
// `numberOfEntityFieldsWithoutId` excludes it - so `{id, a}` on a two-column entity collapsed to
|
|
92
|
+
// `read: true`, which re-parses as `{a, b}` and grants `b`.
|
|
93
|
+
expect(generated).toContain(`@c.Allow(editorRole, {\n\tread: ['id', 'a'],\n})`)
|
|
94
|
+
expect(generated).not.toContain(`@c.Allow(editorRole, {\n\tread: true,\n})`)
|
|
95
|
+
})
|
|
96
|
+
|
|
97
|
+
namespace FullReadModel {
|
|
98
|
+
export const editorRole = c.createRole('editor')
|
|
99
|
+
|
|
100
|
+
@c.Allow(editorRole, { read: true })
|
|
101
|
+
export class Article {
|
|
102
|
+
a = c.stringColumn()
|
|
103
|
+
b = c.stringColumn()
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
test('definition generator still collapses a grant covering every non-primary field', () => {
|
|
108
|
+
const generator = new DefinitionCodeGenerator()
|
|
109
|
+
const generated = generator.generate(createSchema(FullReadModel))
|
|
110
|
+
expect(generated).toContain(`@c.Allow(editorRole, {\n\tread: true,\n})`)
|
|
111
|
+
})
|