@contember/schema-utils 2.2.0-alpha.3 → 2.2.0-alpha.6

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.
Files changed (67) hide show
  1. package/dist/development/src/acl/schemaUtils.cjs +4 -1
  2. package/dist/development/src/acl/schemaUtils.cjs.map +1 -1
  3. package/dist/development/src/acl/schemaUtils.js +4 -1
  4. package/dist/development/src/acl/schemaUtils.js.map +1 -1
  5. package/dist/development/src/definition-generator/AclDefinitionCodeGenerator.cjs +19 -5
  6. package/dist/development/src/definition-generator/AclDefinitionCodeGenerator.cjs.map +1 -1
  7. package/dist/development/src/definition-generator/AclDefinitionCodeGenerator.js +19 -5
  8. package/dist/development/src/definition-generator/AclDefinitionCodeGenerator.js.map +1 -1
  9. package/dist/development/src/validation/MembershipResolver.cjs +3 -2
  10. package/dist/development/src/validation/MembershipResolver.cjs.map +1 -1
  11. package/dist/development/src/validation/MembershipResolver.js +3 -2
  12. package/dist/development/src/validation/MembershipResolver.js.map +1 -1
  13. package/dist/production/src/acl/schemaUtils.cjs +4 -1
  14. package/dist/production/src/acl/schemaUtils.cjs.map +1 -1
  15. package/dist/production/src/acl/schemaUtils.js +4 -1
  16. package/dist/production/src/acl/schemaUtils.js.map +1 -1
  17. package/dist/production/src/definition-generator/AclDefinitionCodeGenerator.cjs +19 -5
  18. package/dist/production/src/definition-generator/AclDefinitionCodeGenerator.cjs.map +1 -1
  19. package/dist/production/src/definition-generator/AclDefinitionCodeGenerator.js +19 -5
  20. package/dist/production/src/definition-generator/AclDefinitionCodeGenerator.js.map +1 -1
  21. package/dist/production/src/validation/MembershipResolver.cjs +3 -2
  22. package/dist/production/src/validation/MembershipResolver.cjs.map +1 -1
  23. package/dist/production/src/validation/MembershipResolver.js +3 -2
  24. package/dist/production/src/validation/MembershipResolver.js.map +1 -1
  25. package/dist/tests/tsconfig.tsbuildinfo +1 -1
  26. package/dist/types/acl/AllowAllPermissionFactory.d.ts.map +1 -1
  27. package/dist/types/acl/PredicateDefinitionProcessor.d.ts.map +1 -1
  28. package/dist/types/acl/schemaUtils.d.ts.map +1 -1
  29. package/dist/types/dataUtils.d.ts +1 -1
  30. package/dist/types/dataUtils.d.ts.map +1 -1
  31. package/dist/types/definition-generator/AclDefinitionCodeGenerator.d.ts +6 -0
  32. package/dist/types/definition-generator/AclDefinitionCodeGenerator.d.ts.map +1 -1
  33. package/dist/types/definition-generator/DefinitionCodeGenerator.d.ts.map +1 -1
  34. package/dist/types/definition-generator/DefinitionNamingConventions.d.ts.map +1 -1
  35. package/dist/types/definition-generator/TsDefinitionGenerator.d.ts.map +1 -1
  36. package/dist/types/json-schema/collectUnsupportedKeywords.d.ts.map +1 -1
  37. package/dist/types/json-schema/jsonSchemaToTsType.d.ts.map +1 -1
  38. package/dist/types/json-schema/keywords.d.ts +2 -2
  39. package/dist/types/json-schema/keywords.d.ts.map +1 -1
  40. package/dist/types/lax/LaxSchemaConverter.d.ts.map +1 -1
  41. package/dist/types/lax/schema.d.ts +87 -87
  42. package/dist/types/model/NamingConventions.d.ts.map +1 -1
  43. package/dist/types/model/modelUtils.d.ts.map +1 -1
  44. package/dist/types/model/resolveDefaultColumnType.d.ts.map +1 -1
  45. package/dist/types/schemaChecksum.d.ts.map +1 -1
  46. package/dist/types/schemaFilter.d.ts.map +1 -1
  47. package/dist/types/schemaNormalizer.d.ts.map +1 -1
  48. package/dist/types/tsconfig.tsbuildinfo +1 -1
  49. package/dist/types/type-schema/condition.d.ts.map +1 -1
  50. package/dist/types/type-schema/settings.d.ts +3 -3
  51. package/dist/types/type-schema/where.d.ts.map +1 -1
  52. package/dist/types/utils/deepCompare.d.ts.map +1 -1
  53. package/dist/types/utils/isIt.d.ts.map +1 -1
  54. package/dist/types/utils/printJsValue.d.ts.map +1 -1
  55. package/dist/types/validation/AclValidator.d.ts.map +1 -1
  56. package/dist/types/validation/ActionsValidator.d.ts.map +1 -1
  57. package/dist/types/validation/MembershipResolver.d.ts.map +1 -1
  58. package/dist/types/validation/ModelValidator.d.ts.map +1 -1
  59. package/dist/types/validation/SchemaValidator.d.ts.map +1 -1
  60. package/dist/types/validation/ValidationValidator.d.ts.map +1 -1
  61. package/dist/types/validation/errors.d.ts.map +1 -1
  62. package/package.json +4 -4
  63. package/src/acl/schemaUtils.ts +8 -1
  64. package/src/definition-generator/AclDefinitionCodeGenerator.ts +22 -6
  65. package/src/validation/MembershipResolver.ts +5 -2
  66. package/tests/cases/unit/membershipResolver.test.ts +36 -1
  67. 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 numberOfEntityFieldsWithoutId = Object.keys(entity.fields).length - 1
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
- numberOfEntityFieldsWithoutId,
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
- numberOfEntityFieldsWithoutId,
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, numberOfEntityFieldsWithoutId }: {
106
+ private getMatchingOperations({ operations, predicate, entity, nonPrimaryFields }: {
105
107
  operations: Acl.EntityOperations
106
108
  predicate: Acl.Predicate
107
- numberOfEntityFieldsWithoutId: number
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.length === numberOfEntityFieldsWithoutId) {
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
- if (!roles[membership.role]) {
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
- if (!roleVariables[variable.name] || (!isAssumed && roleVariables[variable.name].type === Acl.VariableType.predefined)) {
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
+ })