Skip to content

Commit 7f949cf

Browse files
authored
feat: object validation (#137)
1 parent 1ef8583 commit 7f949cf

File tree

1 file changed

+130
-57
lines changed

1 file changed

+130
-57
lines changed

server/src/openfga-yaml-schema.ts

Lines changed: 130 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -1,39 +1,46 @@
1-
import { AuthorizationModel, CheckRequestTupleKey, Condition, ListObjectsRequest, RelationReference, TupleKey, TypeDefinition } from "@openfga/sdk";
1+
import {
2+
AuthorizationModel,
3+
CheckRequestTupleKey,
4+
Condition,
5+
ListObjectsRequest,
6+
RelationReference,
7+
TupleKey,
8+
TypeDefinition,
9+
} from "@openfga/sdk";
210
import Ajv, { Schema, ValidateFunction, SchemaValidateFunction } from "ajv";
311

4-
512
type Store = {
6-
name: string
7-
model_file?: string
8-
model?: string
9-
tuple_files?: string
10-
tuples: TupleKey[]
11-
tests: Test[]
12-
}
13+
name: string;
14+
model_file?: string;
15+
model?: string;
16+
tuple_files?: string;
17+
tuples: TupleKey[];
18+
tests: Test[];
19+
};
1320

1421
type Test = {
15-
tuples: TupleKey[]
16-
check: CheckTest[]
17-
list_objects: ListObjectTest[]
18-
}
22+
tuples: TupleKey[];
23+
check: CheckTest[];
24+
list_objects: ListObjectTest[];
25+
};
1926

2027
type CheckTest = Omit<CheckRequestTupleKey, "relation"> & {
21-
assertions: Record<string, boolean>
22-
context: Record<string, any>
23-
}
28+
assertions: Record<string, boolean>;
29+
context: Record<string, any>;
30+
};
2431

2532
type ListObjectTest = Omit<ListObjectsRequest, "relation"> & {
26-
assertions: Record<string, boolean>
27-
}
33+
assertions: Record<string, boolean>;
34+
};
2835

29-
type BaseError = { keyword: string; message: string; instancePath: string; }
36+
type BaseError = { keyword: string; message: string; instancePath: string };
3037

3138
// Errors for tuples validation
3239
const invalidTuple = (message: string, instancePath: string): BaseError => {
3340
return {
3441
keyword: "valid_tuple",
3542
message,
36-
instancePath
43+
instancePath,
3744
};
3845
};
3946

@@ -46,15 +53,21 @@ const relationMustExistOnType = (relation: string, type: string, instancePath: s
4653
};
4754

4855
const userNotTypeRestriction = (user: string, tuple: TupleKey, instancePath: string) => {
49-
return invalidTuple(`'${user}' is not a type restriction on relation '${tuple.relation}' of type '${tuple.object.split(":")[0]}'.`, instancePath + "/user");
56+
return invalidTuple(
57+
`'${user}' is not a type restriction on relation '${tuple.relation}' of type '${tuple.object.split(":")[0]}'.`,
58+
instancePath + "/user",
59+
);
5060
};
5161

5262
const conditionDoesntExist = (tuple: TupleKey, instancePath: string) => {
5363
return invalidTuple(`condition '${tuple.condition?.name}' is not defined.`, instancePath + "/condition/name");
5464
};
5565

5666
const notAParameter = (param: string, tuple: TupleKey, instancePath: string) => {
57-
return invalidTuple(`'${param}' is not a parameter on condition '${tuple.condition?.name}'.`, instancePath + `/condition/context/${param}`);
67+
return invalidTuple(
68+
`'${param}' is not a parameter on condition '${tuple.condition?.name}'.`,
69+
instancePath + `/condition/context/${param}`,
70+
);
5871
};
5972

6073
// Errors for store validation
@@ -80,8 +93,10 @@ const invalidTypeUser = (type: string, types: string[], instancePath: string) =>
8093

8194
const nonMatchingRelationType = (relation: string, user: string, values: string[], instancePath: string) => {
8295
if (values.length) {
83-
return invalidStore(`\`${relation}\` is not a relation on \`${user}\`, and does not exist in model - valid relations are [${values}].`, instancePath);
84-
96+
return invalidStore(
97+
`\`${relation}\` is not a relation on \`${user}\`, and does not exist in model - valid relations are [${values}].`,
98+
instancePath,
99+
);
85100
}
86101
return invalidStore(`\`${relation}\` is not a relation on \`${user}\`, and does not exist in model.`, instancePath);
87102
};
@@ -91,14 +106,17 @@ const invalidAssertion = (assertion: string, object: string, instancePath: strin
91106
};
92107

93108
const unidentifiedTestParam = (testParam: string, instancePath: string) => {
94-
return invalidStore(`\`${testParam}\` is not a recognized paramaeter for any condition defined in the model.`, instancePath);
109+
return invalidStore(
110+
`\`${testParam}\` is not a recognized paramaeter for any condition defined in the model.`,
111+
instancePath,
112+
);
95113
};
96114

97-
const undefinedUserTuple = (user: string, instancePath: string) => {
115+
const undefinedTypeTuple = (user: string, instancePath: string) => {
98116
return {
99117
keyword: "valid_store_warning",
100118
message: `${user} does not match any existing tuples; the check is still valid - but double check to ensure this is intended.`,
101-
instancePath: instancePath + "/user"
119+
instancePath,
102120
};
103121
};
104122

@@ -335,27 +353,42 @@ function validateUserField(model: AuthorizationModel, types: string[], userField
335353
if (userField.includes("#")) {
336354
const [type, relation] = userField.split("#");
337355

338-
const userRelations = model.type_definitions.filter(typeDef => typeDef.type === user).flatMap((typeDef) => {
339-
const relationArray: string[] = [];
340-
for(const rel in typeDef.relations) {
341-
relationArray.push(type + "#" + rel);
342-
}
343-
return relationArray;
344-
});
356+
const userRelations = model.type_definitions
357+
.filter((typeDef) => typeDef.type === user)
358+
.flatMap((typeDef) => {
359+
const relationArray: string[] = [];
360+
for (const rel in typeDef.relations) {
361+
relationArray.push(type + "#" + rel);
362+
}
363+
return relationArray;
364+
});
345365

346366
if (!userRelations.includes(userField)) {
347-
errors.push(nonMatchingRelationType(relation, user, userRelations.map(rel => rel.split("#")[1]), instancePath + "/user"));
367+
errors.push(
368+
nonMatchingRelationType(
369+
relation,
370+
user,
371+
userRelations.map((rel) => rel.split("#")[1]),
372+
instancePath + "/user",
373+
),
374+
);
348375
}
349-
350376
}
351377
return errors;
352378
}
353379

354-
function validateAssertionField(model: AuthorizationModel, typeField: string, assertions: Record<string, any>, instancePath: string) {
380+
function validateAssertionField(
381+
model: AuthorizationModel,
382+
typeField: string,
383+
assertions: Record<string, any>,
384+
instancePath: string,
385+
) {
355386
const errors = [];
356387

357388
// Validate assertions exist as relations
358-
const typesRelations = model.type_definitions.filter(tuple => tuple.type === typeField).map(tuple => tuple.relations);
389+
const typesRelations = model.type_definitions
390+
.filter((tuple) => tuple.type === typeField)
391+
.map((tuple) => tuple.relations);
359392
for (const assertion in assertions) {
360393
for (const relation in typesRelations) {
361394
if (!typesRelations[relation]?.[assertion]) {
@@ -368,55 +401,75 @@ function validateAssertionField(model: AuthorizationModel, typeField: string, as
368401
}
369402

370403
// Validate Check Tuple
371-
function validateCheck(model: AuthorizationModel, checkTest: CheckTest, tuples: TupleKey[], params: string[], instancePath: string) {
372-
const errors = [];
404+
function validateCheck(
405+
model: AuthorizationModel,
406+
checkTest: CheckTest,
407+
tuples: TupleKey[],
408+
params: string[],
409+
instancePath: string,
410+
) {
411+
const userErrors = [];
373412

374-
const types = model.type_definitions.map(d => d.type);
413+
const types = model.type_definitions.map((d) => d.type);
375414

376415
const checkUser = checkTest.user;
377416
const checkObject = checkTest.object;
378417

379-
errors.push(...validateUserField(model, types, checkUser, instancePath));
418+
userErrors.push(...validateUserField(model, types, checkUser, instancePath));
380419

381-
if (!errors.length ) {
382-
if(!tuples.map(tuple => tuple.user).filter(user => user === checkUser).length) {
383-
errors.push(undefinedUserTuple(checkUser, instancePath));
420+
if (!userErrors.length) {
421+
if (!tuples.map((tuple) => tuple.user).filter((user) => user === checkUser).length) {
422+
userErrors.push(undefinedTypeTuple(checkUser, instancePath + "/user"));
384423
}
385424
}
386425

426+
const objectErrors = [];
427+
387428
const object = checkObject.split(":")[0];
388429

389430
// Ensure valid type of object
390431
if (!types.includes(object)) {
391-
errors.push(invalidTypeUser(object, types, instancePath + "/object"));
432+
objectErrors.push(invalidTypeUser(object, types, instancePath + "/object"));
392433
}
393434

394-
errors.push(...validateAssertionField(model, object, checkTest.assertions, instancePath));
435+
if (!objectErrors.length) {
436+
if (!tuples.map((tuple) => tuple.object).filter((object) => object === checkObject).length) {
437+
objectErrors.push(undefinedTypeTuple(checkObject, instancePath + "/object"));
438+
}
439+
}
440+
441+
objectErrors.push(...validateAssertionField(model, object, checkTest.assertions, instancePath));
395442

396443
const context = checkTest.context;
397444
for (const testParam in context) {
398445
if (!params.includes(testParam)) {
399-
errors.push(unidentifiedTestParam(testParam, instancePath + `/context/${testParam}`));
446+
objectErrors.push(unidentifiedTestParam(testParam, instancePath + `/context/${testParam}`));
400447
}
401448
}
402449

403-
return errors;
450+
return [...userErrors, ...objectErrors];
404451
}
405452

406453
// Validate List Object
407-
function validateListObject(model: AuthorizationModel, listObjects: ListObjectTest, tuples: TupleKey[], params: string[], instancePath: string) {
454+
function validateListObject(
455+
model: AuthorizationModel,
456+
listObjects: ListObjectTest,
457+
tuples: TupleKey[],
458+
params: string[],
459+
instancePath: string,
460+
) {
408461
const errors = [];
409462

410-
const types = model.type_definitions.map(d => d.type);
463+
const types = model.type_definitions.map((d) => d.type);
411464

412465
const listUser = listObjects.user;
413466
const listType = listObjects.type;
414467

415468
errors.push(...validateUserField(model, types, listUser, instancePath));
416469

417-
if (!errors.length ) {
418-
if(!tuples.map(tuple => tuple.user).filter(user => user === listUser).length) {
419-
errors.push(undefinedUserTuple(listUser, instancePath));
470+
if (!errors.length) {
471+
if (!tuples.map((tuple) => tuple.user).filter((user) => user === listUser).length) {
472+
errors.push(undefinedTypeTuple(listUser, instancePath + "/user"));
420473
}
421474
}
422475

@@ -467,15 +520,31 @@ function validateTestTypes(store: Store, model: AuthorizationModel, instancePath
467520
if (!test.check[checkNo].user || !test.check[checkNo].object) {
468521
return false;
469522
}
470-
errors.push(...validateCheck(model, test.check[checkNo], tuples, params, instancePath + `/tests/${testNo}/check/${checkNo}`));
523+
errors.push(
524+
...validateCheck(
525+
model,
526+
test.check[checkNo],
527+
tuples,
528+
params,
529+
instancePath + `/tests/${testNo}/check/${checkNo}`,
530+
),
531+
);
471532
}
472533

473534
// Validate list objects
474535
for (const listNo in test.list_objects) {
475536
if (!test.list_objects[listNo].user || !test.list_objects[listNo].type) {
476537
return false;
477538
}
478-
errors.push(...validateListObject(model, test.list_objects[listNo], tuples, params, instancePath + `/tests/${testNo}/list_objects/${listNo}`));
539+
errors.push(
540+
...validateListObject(
541+
model,
542+
test.list_objects[listNo],
543+
tuples,
544+
params,
545+
instancePath + `/tests/${testNo}/list_objects/${listNo}`,
546+
),
547+
);
479548
}
480549
}
481550

@@ -486,7 +555,11 @@ function validateTestTypes(store: Store, model: AuthorizationModel, instancePath
486555
return true;
487556
}
488557

489-
const validateStore: SchemaValidateFunction = function (this: { jsonModel: AuthorizationModel }, store: Store, cxt: { instancePath: string }): boolean {
558+
const validateStore: SchemaValidateFunction = function (
559+
this: { jsonModel: AuthorizationModel },
560+
store: Store,
561+
cxt: { instancePath: string },
562+
): boolean {
490563
validateStore.errors = validateStore.errors || [];
491564

492565
// Require model or model_file

0 commit comments

Comments
 (0)