@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
JavaScript
"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);
}