-
Notifications
You must be signed in to change notification settings - Fork 90
Description
Describe the bug
I'm trying to write a new spec for a Backstage plugin that uses discriminated union types rendered as oneOf. I'm getting an AJV validationg error where Optic is using AJV to validate its own patched schema, which I think means that the patched schema is invalid. All of the code that I outline below can be found in this PR, here. Basically, I'm trying to use oneOf and enums to differentiate between two separate types. From what I can tell with json-schema-to-ts, my spec is correct, but I'm getting a few weird errors from AJV surfaced through Optic,
- enum contains multiple values that are the same
- value that shouldn't be an array is not an array
- value doesn't match schema from
anyOf
Full error messages below.
The types in question are,
type ConditionalPolicyDecision = {
result: AuthorizeResult.CONDITIONAL;
pluginId: string;
resourceType: string;
conditions: PermissionCriteria<PermissionCondition>;
};
type DefinitivePolicyDecision = {
result: AuthorizeResult.ALLOW | AuthorizeResult.DENY;
}
export type PolicyDecision =
| DefinitivePolicyDecision
| ConditionalPolicyDecision;So far, my JSON schema (YAML) looks like this,
DefinitivePolicyDecision:
type: object
properties:
result:
type: string
enum:
- ALLOW
- DENY
id:
type: string
required:
- result
- id
additionalProperties: false
ConditionalPolicyDecision:
type: object
properties:
result:
type: string
enum:
- CONDITIONAL
pluginId:
type: string
resourceType:
type: string
conditions:
$ref: '#/components/schemas/PermissionCriteria'
id:
type: string
required:
- result
- id
- pluginId
- resourceType
- conditions
additionalProperties: true
PolicyDecision:
oneOf:
- $ref: '#/components/schemas/ConditionalPolicyDecision'
- $ref: '#/components/schemas/DefinitivePolicyDecision'This results in the following schema (added a console.dir here),
| const schema: SchemaObject = JSON.parse(JSON.stringify(input)); |
{
type: 'object',
properties: {
items: {
type: 'array',
items: {
oneOf: [
{
type: 'object',
properties: {
result: {
type: 'string',
enum: [ 'CONDITIONAL', 'ALLOW', 'ALLOW', 'DENY', 'DENY' ]
},
pluginId: { type: 'string' },
resourceType: { type: 'string' },
conditions: {
oneOf: [
{
type: 'object',
properties: {
allOf: {
type: 'array',
items: {
type: 'object',
properties: {
resourceType: { type: 'string' },
rule: { type: 'string' },
params: {
type: 'object',
additionalProperties: {
oneOf: [
{
oneOf: [
{ type: 'string' },
{ type: 'number' },
{
type: 'boolean',
nullable: true
}
]
},
{
type: 'array',
items: {
oneOf: [
{ type: 'string' },
{ type: 'number' },
{
type: 'boolean',
nullable: true
}
]
}
}
]
}
}
},
required: [ 'resourceType', 'rule' ],
additionalProperties: true
},
minItems: 1
}
},
additionalProperties: true
},
{
type: 'object',
properties: {
anyOf: {
type: 'array',
items: {
type: 'object',
properties: {
resourceType: { type: 'string' },
rule: { type: 'string' },
params: {
type: 'object',
additionalProperties: {
oneOf: [
{
oneOf: [
{ type: 'string' },
{ type: 'number' },
{
type: 'boolean',
nullable: true
}
]
},
{
type: 'array',
items: {
oneOf: [
{ type: 'string' },
{ type: 'number' },
{
type: 'boolean',
nullable: true
}
]
}
}
]
}
}
},
required: [ 'resourceType', 'rule' ],
additionalProperties: true
},
minItems: 1
}
},
additionalProperties: true
},
{
type: 'object',
properties: {
not: {
type: 'array',
items: {
type: 'object',
properties: {
resourceType: { type: 'string' },
rule: { type: 'string' },
params: {
type: 'object',
additionalProperties: {
oneOf: [
{
oneOf: [
{ type: 'string' },
{ type: 'number' },
{
type: 'boolean',
nullable: true
}
]
},
{
type: 'array',
items: {
oneOf: [
{ type: 'string' },
{ type: 'number' },
{
type: 'boolean',
nullable: true
}
]
}
}
]
}
}
},
required: [ 'resourceType', 'rule' ],
additionalProperties: true
},
minItems: 1
}
},
additionalProperties: true
},
{
type: 'object',
properties: {
resourceType: { type: 'string' },
rule: { type: 'string' },
params: {
type: 'object',
additionalProperties: {
oneOf: [
{
oneOf: [
{ type: 'string' },
{ type: 'number' },
{ type: 'boolean', nullable: true }
]
},
{
type: 'array',
items: {
oneOf: [
{ type: 'string' },
{ type: 'number' },
{ type: 'boolean', nullable: true }
]
}
}
]
}
}
},
required: [ 'resourceType', 'rule' ],
additionalProperties: true
}
]
},
id: { type: 'string' }
},
required: [ 'result', 'id' ],
additionalProperties: true
},
{
type: 'object',
properties: {
result: {
type: 'string',
enum: [ 'ALLOW', 'DENY', 'CONDITIONAL' ]
},
id: { type: 'string' },
pluginId: { type: 'string' },
resourceType: { type: 'string' },
conditions: {
type: 'object',
properties: {
rule: { type: 'string' },
params: { type: 'array', items: { type: 'string' } }
},
required: [ 'rule', 'params' ]
}
},
required: [ 'result', 'id' ],
additionalProperties: false
}
]
}
}
},
required: [ 'items' ],
additionalProperties: false
}which throws an error during AJV compilation,
Error: schema is invalid: data/properties/items/items/oneOf/0/properties/result/enum must NOT have duplicate items (items ## 3 and 4 are identical), data/properties/items/items must be array, data/properties/items/items must match a schema in anyOf
at Ajv.validateSchema (/Users/sennyeya/.asdf/installs/nodejs/19.0.1/lib/node_modules/@useoptic/optic/node_modules/ajv/dist/core.js:266:23)
at Ajv._addSchema (/Users/sennyeya/.asdf/installs/nodejs/19.0.1/lib/node_modules/@useoptic/optic/node_modules/ajv/dist/core.js:460:18)
at Ajv.compile (/Users/sennyeya/.asdf/installs/nodejs/19.0.1/lib/node_modules/@useoptic/optic/node_modules/ajv/dist/core.js:158:26)
at ShapeDiffTraverser.traverse
...
Taking a look at the logs, this request causes part of the enum error by adding the "ALLOW" and "DENY" values twice. The other 2 errors I'm not sure about.
{
request: {
host: 'localhost:8001',
method: 'post',
path: '/authorize',
body: {
contentType: 'application/json',
body: '{"items":[{"id":"123","permission":{"type":"resource","name":"test.permission.1","resourceType":"test-resource-1","attributes":{}},"resourceRef":"resource:1"},{"id":"234","permission":{"type":"resource","name":"test.permission.2","resourceType":"test-resource-2","attributes":{}},"resourceRef":"resource:2"},{"id":"345","permission":{"type":"resource","name":"test.permission.3","resourceType":"test-resource-1","attributes":{}},"resourceRef":"resource:3"},{"id":"456","permission":{"type":"resource","name":"test.permission.4","resourceType":"test-resource-2","attributes":{}},"resourceRef":"resource:4"}]}',
size: 607
},
headers: [],
query: []
},
response: {
statusCode: '200',
body: {
contentType: 'application/json; charset=utf-8',
body: '{"items":[{"id":"123","result":"ALLOW"},{"id":"234","result":"ALLOW"},{"id":"345","result":"DENY"},{"id":"456","result":"DENY"}]}',
size: 129
},
headers: []
}
}
Internal optic schema diff after running the above request,
--- old.json 2024-01-11 16:39:58
+++ new.json 2024-01-11 16:01:40
@@ -8,7 +8,10 @@
{
"type": "object",
"properties": {
- "result": { "type": "string", "enum": ["CONDITIONAL"] },
+ "result": {
+ "type": "string",
+ "enum": ["CONDITIONAL", "ALLOW", "ALLOW", "DENY", "DENY"]
+ },
"pluginId": { "type": "string" },
"resourceType": { "type": "string" },
"conditions": {
@@ -179,13 +182,7 @@
},
"id": { "type": "string" }
},
- "required": [
- "result",
- "id",
- "pluginId",
- "resourceType",
- "conditions"
- ],
+ "required": ["result", "id"],
"additionalProperties": true
},
{
@@ -207,13 +204,7 @@
"required": ["rule", "params"]
}
},
- "required": [
- "result",
- "id",
- "pluginId",
- "resourceType",
- "conditions"
- ],
+ "required": ["result", "id"],
"additionalProperties": false
}
]
To Reproduce
See my PR here. I'm running PORT=8001 optic capture src/schema/openapi.yaml --server-override http://localhost:8001 in plugins/permission-backend to get the output seen above.
Expected behavior
An error is not thrown and the schema validates as expected.
Details (please complete the following information):
- OS and version: [e.g. Mac OS 13.1] MacOS 13.0.1
- Optic version: [e.g. v0.37.1] 0.52 and 0.53.20
- NodeJS version: [e.g. 18.0.0] 19.0.1