UNPKG

@openfga/syntax-transformer

Version:

Javascript implementation of ANTLR Grammar for the OpenFGA DSL and parser from and to the OpenFGA JSON Syntax

735 lines (734 loc) 38.8 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.validateJSON = validateJSON; exports.validateDSL = validateDSL; const keywords_1 = require("./keywords"); const dsltojson_1 = require("../transformer/dsltojson"); const errors_1 = require("../errors"); const exceptions_1 = require("../util/exceptions"); const validate_rules_1 = require("./validate-rules"); var RelationDefOperator; (function (RelationDefOperator) { RelationDefOperator["Union"] = "union"; RelationDefOperator["Intersection"] = "intersection"; RelationDefOperator["Difference"] = "difference"; })(RelationDefOperator || (RelationDefOperator = {})); var RewriteType; (function (RewriteType) { RewriteType["Direct"] = "direct"; RewriteType["ComputedUserset"] = "computed_userset"; RewriteType["TupleToUserset"] = "tuple_to_userset"; })(RewriteType || (RewriteType = {})); const getTypeRestrictionString = (typeRestriction) => { let typeRestrictionString = typeRestriction.type; if (typeRestriction.wildcard) { typeRestrictionString += ":*"; } else if (typeRestriction.relation) { typeRestrictionString += `#${typeRestriction.relation}`; } if (typeRestriction.condition) { typeRestrictionString += ` with ${typeRestriction.condition}`; } return typeRestrictionString; }; const getTypeRestrictions = (relatedTypes) => { return relatedTypes.map((u) => getTypeRestrictionString(u)); }; const getRelationalParserResult = (userset) => { var _a, _b, _c, _d; let target, from = undefined; if (userset.computedUserset) { target = userset.computedUserset.relation || undefined; } else { target = ((_b = (_a = userset.tupleToUserset) === null || _a === void 0 ? void 0 : _a.computedUserset) === null || _b === void 0 ? void 0 : _b.relation) || undefined; from = ((_d = (_c = userset.tupleToUserset) === null || _c === void 0 ? void 0 : _c.tupleset) === null || _d === void 0 ? void 0 : _d.relation) || undefined; } let rewrite = RewriteType.Direct; if (target) { rewrite = RewriteType.ComputedUserset; } if (from) { rewrite = RewriteType.TupleToUserset; } return { target, from, rewrite }; }; const getRelationDefName = (userset) => { var _a; let relationDefName = (_a = userset.computedUserset) === null || _a === void 0 ? void 0 : _a.relation; const parserResult = getRelationalParserResult(userset); if (parserResult.rewrite === RewriteType.ComputedUserset) { relationDefName = parserResult.target; } else if (parserResult.rewrite === RewriteType.TupleToUserset) { relationDefName = `${parserResult.target} from ${parserResult.from}`; } return relationDefName; }; const deepCopy = (object) => { return JSON.parse(JSON.stringify(object)); }; // Ensure a relation is assignable, the rest of the checks are to ensure that no model has this as well as additional properties defined const relationIsSingle = (currentRelation) => { return (!Object.prototype.hasOwnProperty.call(currentRelation, RelationDefOperator.Union) && !Object.prototype.hasOwnProperty.call(currentRelation, RelationDefOperator.Intersection) && !Object.prototype.hasOwnProperty.call(currentRelation, RelationDefOperator.Difference)); }; // Return all the allowable types for the specified type/relation function allowableTypes(typeName, type, relation) { var _a; const allowedTypes = []; const currentRelations = typeName[type].relations[relation]; const currentRelationMetadata = getTypeRestrictions(((_a = typeName[type].metadata) === null || _a === void 0 ? void 0 : _a.relations[relation].directly_related_user_types) || []); const isValid = relationIsSingle(currentRelations); // for now, we assume that the type/relation must be single and rewrite is direct if (isValid) { const childDef = getRelationalParserResult(currentRelations); switch (childDef.rewrite) { case RewriteType.Direct: { allowedTypes.push(...currentRelationMetadata); } } } return [allowedTypes, isValid]; } // helper function to figure out whether the specified allowable types // are tuple to user set. If so, return the type and relationship. // Otherwise, return null as relationship const destructTupleToUserset = (allowableType) => { const [tupleString, decodedConditionName] = allowableType.split(" with "); const isWildcard = tupleString.includes(":*"); const splittedWords = tupleString.replace(":*", "").split("#"); return { decodedType: splittedWords[0], decodedRelation: splittedWords[1], isWildcard, decodedConditionName }; }; // for the type/relation, whether there are any unique entry points, and if a loop is found // if there are unique entry points (i.e., direct relations) then it will return true // otherwise, it will follow its children to see if there are unique entry points // if there is a loop during traversal, the function will return a boolean indicating so function hasEntryPointOrLoop(typeMap, typeName, relationName, rewrite, visitedRecords) { var _a, _b, _c, _d; // Deep copy const visited = deepCopy(visitedRecords); if (!relationName) { // nothing to do if relation is undefined return { hasEntry: false, loop: false }; } if (!visited[typeName]) { visited[typeName] = {}; } visited[typeName][relationName] = true; const currentRelation = typeMap[typeName].relations; if (!currentRelation || !currentRelation[relationName]) { return { hasEntry: false, loop: false }; } const relationMetadata = (_a = typeMap[typeName].metadata) === null || _a === void 0 ? void 0 : _a.relations; if (!typeMap[typeName].relations || !typeMap[typeName].relations[relationName]) { return { hasEntry: false, loop: false }; } if (rewrite.this) { for (const assignableType of getTypeRestrictions(((_b = relationMetadata === null || relationMetadata === void 0 ? void 0 : relationMetadata[relationName]) === null || _b === void 0 ? void 0 : _b.directly_related_user_types) || [])) { const { decodedType, decodedRelation, isWildcard } = destructTupleToUserset(assignableType); if (!decodedRelation || isWildcard) { return { hasEntry: true, loop: false }; } const assignableRelation = typeMap[decodedType].relations[decodedRelation]; if (!assignableRelation) { return { hasEntry: false, loop: false }; } if ((_c = visited[decodedType]) === null || _c === void 0 ? void 0 : _c[decodedRelation]) { continue; } const { hasEntry } = hasEntryPointOrLoop(typeMap, decodedType, decodedRelation, assignableRelation, visited); if (hasEntry) { return { hasEntry: true, loop: false }; } } return { hasEntry: false, loop: false }; } else if (rewrite.computedUserset) { const computedRelationName = rewrite.computedUserset.relation; if (!computedRelationName) { return { hasEntry: false, loop: false }; } if (!typeMap[typeName].relations[computedRelationName]) { return { hasEntry: false, loop: false }; } const computedRelation = typeMap[typeName].relations[computedRelationName]; if (!computedRelation) { return { hasEntry: false, loop: false }; } // Loop detected if (visited[typeName][computedRelationName]) { return { hasEntry: false, loop: true }; } return hasEntryPointOrLoop(typeMap, typeName, computedRelationName, computedRelation, visited); } else if (rewrite.tupleToUserset) { const tuplesetRelationName = rewrite.tupleToUserset.tupleset.relation; const computedRelationName = rewrite.tupleToUserset.computedUserset.relation; if (!tuplesetRelationName || !computedRelationName) { return { hasEntry: false, loop: false }; } const tuplesetRelation = typeMap[typeName].relations[tuplesetRelationName]; if (!tuplesetRelation) { return { hasEntry: false, loop: false }; } for (const assignableType of getTypeRestrictions(((_d = relationMetadata === null || relationMetadata === void 0 ? void 0 : relationMetadata[tuplesetRelationName]) === null || _d === void 0 ? void 0 : _d.directly_related_user_types) || [])) { const assignableRelation = typeMap[assignableType].relations[computedRelationName]; if (assignableRelation) { if (visited[assignableType] && visited[assignableType][computedRelationName]) { continue; } const { hasEntry } = hasEntryPointOrLoop(typeMap, assignableType, computedRelationName, assignableRelation, visited); if (hasEntry) { return { hasEntry: true, loop: false }; } } } return { hasEntry: false, loop: false }; } else if (rewrite.union) { let hasLoop = false; for (const child of rewrite.union.child) { const { hasEntry, loop } = hasEntryPointOrLoop(typeMap, typeName, relationName, child, deepCopy(visited)); if (hasEntry) { return { hasEntry: true, loop: false }; } hasLoop = hasLoop || loop; } return { hasEntry: false, loop: hasLoop }; } else if (rewrite.intersection) { for (const child of rewrite.intersection.child) { const { hasEntry, loop } = hasEntryPointOrLoop(typeMap, typeName, relationName, child, deepCopy(visited)); if (!hasEntry) { return { hasEntry: false, loop }; } } return { hasEntry: true, loop: false }; } else if (rewrite.difference) { const visited = deepCopy(visitedRecords); const baseResult = hasEntryPointOrLoop(typeMap, typeName, relationName, rewrite.difference.base, visited); if (!baseResult.hasEntry) { return { hasEntry: false, loop: baseResult.loop }; } const subtractResult = hasEntryPointOrLoop(typeMap, typeName, relationName, rewrite.difference.subtract, visited); if (!subtractResult.hasEntry) { return { hasEntry: false, loop: subtractResult.loop }; } return { hasEntry: true, loop: false }; } return { hasEntry: false, loop: false }; } const geConditionLineNumber = (conditionName, lines, skipIndex) => { if (!skipIndex) { skipIndex = 0; } if (!lines) { return undefined; } return (lines.slice(skipIndex).findIndex((line) => line.trim().startsWith(`condition ${conditionName}`)) + skipIndex); }; const getTypeLineNumber = (typeName, lines, skipIndex) => { if (!skipIndex) { skipIndex = 0; } if (!lines) { return undefined; } return lines.slice(skipIndex).findIndex((line) => line.trim().match(`^type ${typeName}$`)) + skipIndex; }; const getRelationLineNumber = (relation, lines, skipIndex) => { if (!skipIndex) { skipIndex = 0; } if (!lines) { return undefined; } return (lines .slice(skipIndex) .findIndex((line) => line.trim().replace(/ {2,}/g, " ").match(`^define ${relation}\\s*:`)) + skipIndex); }; const getSchemaLineNumber = (schema, lines) => { if (!lines) { return undefined; } const index = lines.findIndex((line) => line.trim().replace(/ {2,}/g, " ").match(`^schema ${schema}$`)); // As findIndex returns -1 when it doesn't find the line, we want to return 0 instead if (index >= 1) { return index; } else { return 0; } }; function checkForDuplicatesTypeNamesInRelation(collector, relationDef, relationName, typeName, typeDefFile, typeDefModule, lines) { var _a; const typeNameSet = new Set(); (_a = relationDef.directly_related_user_types) === null || _a === void 0 ? void 0 : _a.forEach((typeDef) => { var _a; const typeDefName = getTypeRestrictionString(typeDef); if (typeNameSet.has(typeDefName)) { const typeIndex = getTypeLineNumber(typeDef.type, lines); const lineIndex = getRelationLineNumber(relationName, lines, typeIndex); const file = ((_a = relationDef.source_info) === null || _a === void 0 ? void 0 : _a.file) || typeDefFile; const module = relationDef.module || typeDefModule; collector.raiseDuplicateTypeRestriction(typeDefName, relationName, typeName, { file, module }, lineIndex); } typeNameSet.add(typeDefName); }); } // ensure all the referenced relations are defined function checkForDuplicatesInRelation(collector, typeDef, relationName, lines) { var _a, _b, _c, _d, _e, _f, _g; const relationDef = typeDef.relations[relationName]; const file = (_b = (_a = typeDef.metadata) === null || _a === void 0 ? void 0 : _a.source_info) === null || _b === void 0 ? void 0 : _b.file; const module = (_c = typeDef.metadata) === null || _c === void 0 ? void 0 : _c.module; // Union const relationUnionNameSet = new Set(); (_e = (_d = relationDef.union) === null || _d === void 0 ? void 0 : _d.child) === null || _e === void 0 ? void 0 : _e.forEach((userset) => { const relationDefName = getRelationDefName(userset); if (relationDefName && relationUnionNameSet.has(relationDefName)) { const typeIndex = getTypeLineNumber(typeDef.type, lines); const lineIndex = getRelationLineNumber(relationName, lines, typeIndex); collector.raiseDuplicateType(relationDefName, relationName, typeDef.type, { file, module }, lineIndex); } relationUnionNameSet.add(relationDefName); }); // Intersection const relationIntersectionNameSet = new Set(); (_g = (_f = relationDef.intersection) === null || _f === void 0 ? void 0 : _f.child) === null || _g === void 0 ? void 0 : _g.forEach((userset) => { const relationDefName = getRelationDefName(userset); if (relationDefName && relationIntersectionNameSet.has(relationDefName)) { const typeIndex = getTypeLineNumber(typeDef.type, lines); const lineIndex = getRelationLineNumber(relationName, lines, typeIndex); collector.raiseDuplicateType(relationDefName, relationName, typeDef.type, { file, module }, lineIndex); } relationIntersectionNameSet.add(relationDefName); }); // Difference if (Object.prototype.hasOwnProperty.call(relationDef, RelationDefOperator.Difference)) { const baseName = getRelationDefName(relationDef.difference.base); const subtractName = getRelationDefName(relationDef.difference.subtract); if (baseName && baseName === subtractName) { const typeIndex = getTypeLineNumber(typeDef.type, lines); const lineIndex = getRelationLineNumber(relationName, lines, typeIndex); collector.raiseDuplicateType(baseName, relationName, typeDef.type, { file, module }, lineIndex); } } } // helper function to ensure all childDefs are defined function childDefDefined(collector, typeMap, type, relation, childDef, conditions = {}, lines) { var _a, _b, _c, _d, _e; const relations = typeMap[type].relations; if (!relations || !relations[relation]) { return; } const currentRelationMetadata = (_a = typeMap[type].metadata) === null || _a === void 0 ? void 0 : _a.relations[relation]; let file = (_b = currentRelationMetadata === null || currentRelationMetadata === void 0 ? void 0 : currentRelationMetadata.source_info) === null || _b === void 0 ? void 0 : _b.file; if (!file) { file = (_d = (_c = typeMap[type].metadata) === null || _c === void 0 ? void 0 : _c.source_info) === null || _d === void 0 ? void 0 : _d.file; } let module = currentRelationMetadata === null || currentRelationMetadata === void 0 ? void 0 : currentRelationMetadata.module; if (!module) { module = (_e = typeMap[type].metadata) === null || _e === void 0 ? void 0 : _e.module; } switch (childDef.rewrite) { case RewriteType.Direct: { // for this case, as long as the type / type+relation defined, we should be fine const fromPossibleTypes = getTypeRestrictions((currentRelationMetadata === null || currentRelationMetadata === void 0 ? void 0 : currentRelationMetadata.directly_related_user_types) || []); if (!fromPossibleTypes.length) { const typeIndex = getTypeLineNumber(type, lines); const lineIndex = getRelationLineNumber(relation, lines, typeIndex); collector.raiseAssignableRelationMustHaveTypes(relation, lineIndex); } for (const item of fromPossibleTypes) { const { decodedType, decodedRelation, isWildcard, decodedConditionName } = destructTupleToUserset(item); if (!typeMap[decodedType]) { // Split line at definition as InvalidType should mark the value, not the key const typeIndex = getTypeLineNumber(type, lines); const lineIndex = getRelationLineNumber(relation, lines, typeIndex); collector.raiseInvalidType(decodedType, type, relation, { file, module }, lineIndex); } if (decodedConditionName && !conditions[decodedConditionName]) { // condition name is not defined const typeIndex = getTypeLineNumber(type, lines); const lineIndex = getRelationLineNumber(relation, lines, typeIndex); collector.raiseInvalidConditionNameInParameter(`${decodedConditionName}`, type, relation, decodedConditionName, { file, module }, lineIndex); } if (isWildcard && decodedRelation) { // we cannot have both wild carded and relation at the same time const typeIndex = getTypeLineNumber(type, lines); const lineIndex = getRelationLineNumber(relation, lines, typeIndex); collector.raiseAssignableTypeWildcardRelation(item, type, relation, { file, module }, lineIndex); } else if (decodedRelation) { if (!typeMap[decodedType] || !typeMap[decodedType].relations[decodedRelation]) { // type/relation is not defined const typeIndex = getTypeLineNumber(type, lines); const lineIndex = getRelationLineNumber(relation, lines, typeIndex); collector.raiseInvalidTypeRelation(`${decodedType}#${decodedRelation}`, decodedType, relation, decodedRelation, type, lineIndex, { file, module }); } } } break; } case RewriteType.ComputedUserset: { if (childDef.target && !relations[childDef.target]) { const typeIndex = getTypeLineNumber(type, lines); const lineIndex = getRelationLineNumber(relation, lines, typeIndex); const value = childDef.target; collector.raiseInvalidRelationError(value, type, relation, Object.keys(relations), lineIndex, { file, module, }); } break; } case RewriteType.TupleToUserset: { // for this case, we need to consider both the "from" and "relation" if (childDef.from && childDef.target) { // 1. Check to see if the childDef.from exists // Ensure that the relation referenced in the from exists // (e.g. ensures that `b` exists as a relation on the type in the case of `a from b`) if (!relations[childDef.from]) { const typeIndex = getTypeLineNumber(type, lines); // org const lineIndex = getRelationLineNumber(relation, lines, typeIndex); // has_assigned collector.raiseInvalidTypeRelation(`${childDef.target} from ${childDef.from}`, type, relation, childDef.from, type, lineIndex, { file, module }); } else { // 2. Ensure that the childDef.from relation is directly assignable // That means that the relation referenced is: // a. directly assignable // b. not a rewrite (not union, intersection or exclusion) // c. none of the directly assignable types contains a wildcard or a relation // d. on every valid assignable type, ensure that the computed relation (e.g. a in a from b) is a relation on those types const [fromTypes, isValid] = allowableTypes(typeMap, type, childDef.from); if (isValid && fromTypes.length) { const childRelationNotValid = []; for (const item of fromTypes) { const { decodedType, decodedRelation, isWildcard } = destructTupleToUserset(item); if (isWildcard || decodedRelation) { // we cannot have both wildcard or decoded relation and relation at the same time const typeIndex = getTypeLineNumber(type, lines); const lineIndex = getRelationLineNumber(relation, lines, typeIndex); collector.raiseTupleUsersetRequiresDirect(childDef.from, type, relation, { file, module }, lineIndex); } else { // check to see if the relation is defined in any children if (!typeMap[decodedType] || !typeMap[decodedType].relations[childDef.target]) { const typeIndex = getTypeLineNumber(type, lines); const lineIndex = getRelationLineNumber(relation, lines, typeIndex); childRelationNotValid.push({ symbol: `${childDef.target} from ${childDef.from}`, typeName: decodedType, relationName: childDef.target, parent: childDef.from, lineIndex, }); } } } // if none of the children have this relation defined, we should raise error. // otherwise, the relation is defined in at least 1 child and should be considered valid if (childRelationNotValid.length === fromTypes.length) { for (const item of childRelationNotValid) { const { lineIndex, symbol, typeName, relationName, parent } = item; collector.raiseInvalidRelationOnTupleset(symbol, typeName, type, relation, relationName, parent, lineIndex, { module, file, }); } } } else { // the from is not allowed. Only direct assignable types are allowed. const typeIndex = getTypeLineNumber(type, lines); const lineIndex = getRelationLineNumber(relation, lines, typeIndex); collector.raiseTupleUsersetRequiresDirect(childDef.from, type, relation, { module, file }, lineIndex); } } } break; } } } // ensure all the referenced relations are defined function relationDefined(collector, typeMap, type, relation, conditions, lines) { var _a, _b, _c, _d; const relations = typeMap[type].relations; if (!relations || !relations[relation]) { return; } const currentRelation = Object.assign({}, relations[relation]); const children = [currentRelation]; while (children.length) { const child = children.shift(); if ((_a = child === null || child === void 0 ? void 0 : child.union) === null || _a === void 0 ? void 0 : _a.child.length) { children.push(...child.union.child); } else if ((_b = child === null || child === void 0 ? void 0 : child.intersection) === null || _b === void 0 ? void 0 : _b.child.length) { children.push(...child.intersection.child); } else if (((_c = child === null || child === void 0 ? void 0 : child.difference) === null || _c === void 0 ? void 0 : _c.base) && child.difference.subtract) { children.push((_d = child === null || child === void 0 ? void 0 : child.difference) === null || _d === void 0 ? void 0 : _d.base, child.difference.subtract); } else if (child) { childDefDefined(collector, typeMap, type, relation, getRelationalParserResult(child), conditions, lines); } } } function modelValidation(collector, errors, authorizationModel, fileToModuleMap, //relationsPerType: Record<string, TransformedType> lines) { var _a, _b, _c, _d, _e, _f, _g; if (errors.length) { // no point in looking at directly assignable types if the model itself already // has other problems return; } const typeMap = {}; const usedConditionNamesSet = new Set(); (_a = authorizationModel.type_definitions) === null || _a === void 0 ? void 0 : _a.forEach((typeDef) => { var _a, _b; const typeName = typeDef.type; typeMap[typeName] = typeDef; for (const relationName in (_a = typeDef.metadata) === null || _a === void 0 ? void 0 : _a.relations) { (((_b = typeDef.metadata) === null || _b === void 0 ? void 0 : _b.relations[relationName].directly_related_user_types) || []).forEach((typeRestriction) => { if (typeRestriction.condition) { usedConditionNamesSet.add(typeRestriction.condition); } }); } }); // first, validate to ensure all the relation are defined (_b = authorizationModel.type_definitions) === null || _b === void 0 ? void 0 : _b.forEach((typeDef) => { const typeName = typeDef.type; // parse through each of the relations to do validation for (const relationDef in typeDef.relations) { relationDefined(collector, typeMap, typeName, relationDef, authorizationModel.conditions, lines); } }); if (errors.length === 0) { const typeSet = new Set(); (_c = authorizationModel.type_definitions) === null || _c === void 0 ? void 0 : _c.forEach((typeDef) => { var _a, _b, _c, _d, _e; const typeName = typeDef.type; const file = (_b = (_a = typeDef.metadata) === null || _a === void 0 ? void 0 : _a.source_info) === null || _b === void 0 ? void 0 : _b.file; const module = (_c = typeDef.metadata) === null || _c === void 0 ? void 0 : _c.module; // check for duplicate types if (typeSet.has(typeName)) { const typeIndex = getTypeLineNumber(typeName, lines); collector.raiseDuplicateTypeName(typeName, { file, module }, typeIndex); } typeSet.add(typeDef.type); for (const relationDefKey in (_d = typeDef.metadata) === null || _d === void 0 ? void 0 : _d.relations) { // check for duplicate type names in the relation checkForDuplicatesTypeNamesInRelation(collector, (_e = typeDef.metadata) === null || _e === void 0 ? void 0 : _e.relations[relationDefKey], relationDefKey, typeName, file, module, lines); // check for duplicate relations checkForDuplicatesInRelation(collector, typeDef, relationDefKey, lines); } }); } // next, ensure all relation have entry point // we can skip if there are errors because errors (such as missing relations) will likely lead to no entries if (errors.length === 0) { (_d = authorizationModel.type_definitions) === null || _d === void 0 ? void 0 : _d.forEach((typeDef) => { var _a, _b, _c, _d, _e; const typeName = typeDef.type; // parse through each of the relations to do validation for (const relationName in typeDef.relations) { const currentRelation = typeMap[typeName].relations; const currentRelationMetadata = (_a = typeDef.metadata) === null || _a === void 0 ? void 0 : _a.relations[relationName]; let file = (_b = currentRelationMetadata === null || currentRelationMetadata === void 0 ? void 0 : currentRelationMetadata.source_info) === null || _b === void 0 ? void 0 : _b.file; if (!file) { file = (_d = (_c = typeDef.metadata) === null || _c === void 0 ? void 0 : _c.source_info) === null || _d === void 0 ? void 0 : _d.file; } let module = currentRelationMetadata === null || currentRelationMetadata === void 0 ? void 0 : currentRelationMetadata.module; if (!module) { module = (_e = typeDef.metadata) === null || _e === void 0 ? void 0 : _e.module; } // Track the modules defined per file if (file && module) { if (!fileToModuleMap[file]) { fileToModuleMap[file] = new Set(); } fileToModuleMap[file].add(module); } const { hasEntry, loop } = hasEntryPointOrLoop(typeMap, typeName, relationName, currentRelation[relationName], {}); if (!hasEntry) { const typeIndex = getTypeLineNumber(typeName, lines); //team 3, group 7, const lineIndex = getRelationLineNumber(relationName, lines, typeIndex); //viewer 6, viewer 10 if (loop) { collector.raiseNoEntryPointLoop(relationName, typeName, { file, module }, lineIndex); } else { collector.raiseNoEntryPoint(relationName, typeName, { file, module }, lineIndex); } } } }); } if (authorizationModel.conditions) { for (const [conditionName, condition] of Object.entries(authorizationModel.conditions)) { const module = (_e = condition.metadata) === null || _e === void 0 ? void 0 : _e.module; const file = (_g = (_f = condition.metadata) === null || _f === void 0 ? void 0 : _f.source_info) === null || _g === void 0 ? void 0 : _g.file; // Track the modules defined per file if (file && module) { if (!fileToModuleMap[file]) { fileToModuleMap[file] = new Set(); } fileToModuleMap[file].add(module); } // Ensure that the nested condition name matches if (conditionName != condition.name) { collector.raiseDifferentNestedConditionName(conditionName, condition.name); } // Ensure that the condition has been used if (!usedConditionNamesSet.has(conditionName)) { const conditionIndex = geConditionLineNumber(conditionName, lines); collector.raiseUnusedCondition(conditionName, { module, file }, conditionIndex); } } } } function populateRelations(collector, authorizationModel, typeRegex, relationRegex, fileToModuleMap, lines) { var _a; // Looking at the types (_a = authorizationModel.type_definitions) === null || _a === void 0 ? void 0 : _a.forEach((typeDef) => { var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o; const typeName = typeDef.type; const file = (_b = (_a = typeDef.metadata) === null || _a === void 0 ? void 0 : _a.source_info) === null || _b === void 0 ? void 0 : _b.file; const module = (_c = typeDef.metadata) === null || _c === void 0 ? void 0 : _c.module; // Track the modules defined per file if (file && module) { if (!fileToModuleMap[file]) { fileToModuleMap[file] = new Set(); } fileToModuleMap[file].add(module); } if (typeName === keywords_1.Keyword.SELF || typeName === keywords_1.ReservedKeywords.THIS) { const lineIndex = getTypeLineNumber(typeName, lines); collector.raiseReservedTypeName(typeName, lineIndex, { file: (_e = (_d = typeDef.metadata) === null || _d === void 0 ? void 0 : _d.source_info) === null || _e === void 0 ? void 0 : _e.file, module: (_f = typeDef.metadata) === null || _f === void 0 ? void 0 : _f.module, }); } if (!typeRegex.regex.test(typeName)) { const lineIndex = getTypeLineNumber(typeName, lines); collector.raiseInvalidName(typeName, typeRegex.rule, undefined, lineIndex, { file: (_h = (_g = typeDef.metadata) === null || _g === void 0 ? void 0 : _g.source_info) === null || _h === void 0 ? void 0 : _h.file, module: (_j = typeDef.metadata) === null || _j === void 0 ? void 0 : _j.module, }); } for (const relationKey in typeDef.relations) { const relationName = relationKey; let relationMeta = (_l = (_k = typeDef.metadata) === null || _k === void 0 ? void 0 : _k.relations) === null || _l === void 0 ? void 0 : _l[relationKey]; if (!(relationMeta === null || relationMeta === void 0 ? void 0 : relationMeta.module)) { // relation belongs to typedef relationMeta = typeDef.metadata; } if (relationName === keywords_1.Keyword.SELF || relationName === keywords_1.ReservedKeywords.THIS) { const typeIndex = getTypeLineNumber(typeName, lines); const lineIndex = getRelationLineNumber(relationName, lines, typeIndex); collector.raiseReservedRelationName(relationName, lineIndex, { file: (_m = relationMeta === null || relationMeta === void 0 ? void 0 : relationMeta.source_info) === null || _m === void 0 ? void 0 : _m.file, module: relationMeta === null || relationMeta === void 0 ? void 0 : relationMeta.module, }); } if (!relationRegex.regex.test(relationName)) { const typeIndex = getTypeLineNumber(typeName, lines); const lineIndex = getRelationLineNumber(relationName, lines, typeIndex); collector.raiseInvalidName(relationName, relationRegex.rule, typeName, lineIndex, { file: (_o = relationMeta === null || relationMeta === void 0 ? void 0 : relationMeta.source_info) === null || _o === void 0 ? void 0 : _o.file, module: relationMeta === null || relationMeta === void 0 ? void 0 : relationMeta.module, }); } } }); } /** * validateJSON - Given a JSON string, validates that it is a valid OpenFGA model * @param {string} dslString * @param {AuthorizationModel} authorizationModel * @param {ValidationOptions} options */ function validateJSON(authorizationModel, options = {}, dslString) { const lines = dslString === null || dslString === void 0 ? void 0 : dslString.split("\n"); const errors = []; const collector = new exceptions_1.ExceptionCollector(errors, lines); const typeValidation = options.typeValidation || `^${validate_rules_1.Rules.type}$`; const relationValidation = options.relationValidation || `^${validate_rules_1.Rules.relation}$`; const defaultRegex = new RegExp("[a-zA-Z]*"); const fileToModuleMap = {}; let typeRegex = { regex: defaultRegex, rule: typeValidation, }; try { typeRegex = { regex: new RegExp(typeValidation), rule: typeValidation, }; // eslint-disable-next-line @typescript-eslint/no-explicit-any } catch (e) { throw new errors_1.ConfigurationError(`Incorrect type regex specification for ${typeValidation}`, e); } let relationRegex = { regex: defaultRegex, rule: relationValidation, }; try { relationRegex = { regex: new RegExp(relationValidation), rule: relationValidation, }; // eslint-disable-next-line @typescript-eslint/no-explicit-any } catch (e) { throw new errors_1.ConfigurationError(`Incorrect relation regex specification for ${relationValidation}`, e); } populateRelations(collector, authorizationModel, typeRegex, relationRegex, fileToModuleMap, lines); const schemaVersion = authorizationModel.schema_version; if (!schemaVersion) { collector.raiseSchemaVersionRequired("", 0); } switch (schemaVersion) { case "1.1": case "1.2": modelValidation(collector, errors, authorizationModel, fileToModuleMap, lines); break; case undefined: break; default: { const lineIndex = getSchemaLineNumber(schemaVersion, lines); collector.raiseInvalidSchemaVersion(schemaVersion, lineIndex); break; } } for (const [file, modules] of Object.entries(fileToModuleMap)) { if (modules.size === 1) { continue; } collector.raiseMultipleModulesInSingleFile(file, modules); } if (errors.length) { throw new errors_1.ModelValidationError(errors); } } /** * validateDSL - Given a string, validates that it is in valid FGA DSL syntax * @param {string} dsl * @param {ValidationOptions} options * @throws {DSLSyntaxError} */ function validateDSL(dsl, options = {}) { const { listener, errorListener } = (0, dsltojson_1.parseDSL)(dsl); if (errorListener.errors.length) { throw new errors_1.DSLSyntaxError(errorListener.errors); } validateJSON(listener.authorizationModel, options, dsl); }