UNPKG

@openfga/syntax-transformer

Version:

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

432 lines (431 loc) 19.9 kB
"use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || (function () { var ownKeys = function(o) { ownKeys = Object.getOwnPropertyNames || function (o) { var ar = []; for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; return ar; }; return ownKeys(o); }; return function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); __setModuleDefault(result, mod); return result; }; })(); var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.parseDSL = parseDSL; exports.transformDSLToJSONObject = transformDSLToJSONObject; exports.transformDSLToJSON = transformDSLToJSON; exports.transformModularDSLToJSONObject = transformModularDSLToJSONObject; const antlr = __importStar(require("antlr4")); const antlr4_1 = require("antlr4"); const OpenFGAParserListener_1 = __importDefault(require("../gen/OpenFGAParserListener")); const OpenFGALexer_1 = __importDefault(require("../gen/OpenFGALexer")); const OpenFGAParser_1 = __importDefault(require("../gen/OpenFGAParser")); const errors_1 = require("../errors"); var RelationDefinitionOperator; (function (RelationDefinitionOperator) { RelationDefinitionOperator["RELATION_DEFINITION_OPERATOR_NONE"] = ""; RelationDefinitionOperator["RELATION_DEFINITION_OPERATOR_OR"] = "or"; RelationDefinitionOperator["RELATION_DEFINITION_OPERATOR_AND"] = "and"; RelationDefinitionOperator["RELATION_DEFINITION_OPERATOR_BUT_NOT"] = "but not"; })(RelationDefinitionOperator || (RelationDefinitionOperator = {})); function parseExpression(rewrites, operator) { let relationDef; if (!(rewrites === null || rewrites === void 0 ? void 0 : rewrites.length)) { return; } if ((rewrites === null || rewrites === void 0 ? void 0 : rewrites.length) === 1) { relationDef = rewrites[0]; } else { switch (operator) { case RelationDefinitionOperator.RELATION_DEFINITION_OPERATOR_OR: relationDef = { union: { child: rewrites, }, }; break; case RelationDefinitionOperator.RELATION_DEFINITION_OPERATOR_AND: relationDef = { intersection: { child: rewrites, }, }; break; case RelationDefinitionOperator.RELATION_DEFINITION_OPERATOR_BUT_NOT: relationDef = { difference: { base: rewrites.shift(), subtract: rewrites.shift(), }, }; break; } } return relationDef; } /** * This Visitor walks the tree generated by parsers and produces Python code * * @returns {object} */ class OpenFgaDslListener extends OpenFGAParserListener_1.default { constructor() { super(...arguments); this.authorizationModel = {}; this.typeDefExtensions = new Map(); this.isModularModel = false; this.rewriteStack = []; this.exitModuleHeader = (ctx) => { if (!ctx._moduleName) { return; } this.isModularModel = true; this.moduleName = ctx._moduleName.getText(); }; this.exitModelHeader = (ctx) => { if (ctx.SCHEMA_VERSION()) { this.authorizationModel.schema_version = ctx.SCHEMA_VERSION().getText(); } }; // eslint-disable-next-line @typescript-eslint/no-unused-vars this.enterTypeDefs = (_ctx) => { this.authorizationModel.type_definitions = []; }; // eslint-disable-next-line @typescript-eslint/no-unused-vars this.exitTypeDefs = (_ctx) => { var _a; if (!((_a = this.authorizationModel.type_definitions) === null || _a === void 0 ? void 0 : _a.length)) { delete this.authorizationModel.type_definitions; } }; // eslint-disable-next-line @typescript-eslint/no-unused-vars this.enterTypeDef = (ctx) => { var _a; if (!ctx._typeName) { return; } if (ctx.EXTEND() && !this.isModularModel) { (_a = ctx.parser) === null || _a === void 0 ? void 0 : _a.notifyErrorListeners("extend can only be used in a modular model", ctx._typeName.start, undefined); } this.currentTypeDef = { type: ctx._typeName.getText(), relations: {}, metadata: { relations: {} }, }; if (this.isModularModel) { this.currentTypeDef.metadata.module = this.moduleName; } }; this.exitTypeDef = (ctx) => { var _a, _b, _c, _d, _e, _f, _g; if (!((_a = this.currentTypeDef) === null || _a === void 0 ? void 0 : _a.type)) { return; } if (this.isModularModel && !Object.keys(((_c = (_b = this.currentTypeDef) === null || _b === void 0 ? void 0 : _b.metadata) === null || _c === void 0 ? void 0 : _c.relations) || {}).length) { // eslint-disable-next-line @typescript-eslint/no-explicit-any this.currentTypeDef.metadata.relations = undefined; } else if (!this.isModularModel && !Object.keys(((_e = (_d = this.currentTypeDef) === null || _d === void 0 ? void 0 : _d.metadata) === null || _e === void 0 ? void 0 : _e.relations) || {}).length) { // eslint-disable-next-line @typescript-eslint/no-explicit-any this.currentTypeDef.metadata = null; } (_f = this.authorizationModel.type_definitions) === null || _f === void 0 ? void 0 : _f.push(this.currentTypeDef); if (ctx.EXTEND() && this.isModularModel) { if (this.typeDefExtensions.has(this.currentTypeDef.type)) { (_g = ctx.parser) === null || _g === void 0 ? void 0 : _g.notifyErrorListeners(`'${this.currentTypeDef.type}' is already extended in file.`, ctx._typeName.start, undefined); } else { this.typeDefExtensions.set(this.currentTypeDef.type, this.currentTypeDef); } } this.currentTypeDef = undefined; }; // eslint-disable-next-line @typescript-eslint/no-unused-vars this.enterRelationDeclaration = (_ctx) => { this.currentRelation = { rewrites: [], typeInfo: { directly_related_user_types: [] }, }; this.rewriteStack = []; }; this.exitRelationDeclaration = (ctx) => { var _a, _b, _c, _d, _e, _f; if (!ctx.relationName()) { return; } const relationName = ctx.relationName().getText(); const rewrites = (_a = this.currentRelation) === null || _a === void 0 ? void 0 : _a.rewrites; const relationDef = parseExpression(rewrites, (_b = this.currentRelation) === null || _b === void 0 ? void 0 : _b.operator); if (relationDef) { // Throw error if same named relation occurs more than once in a relationship definition block if (this.currentTypeDef.relations[relationName]) { (_c = ctx.parser) === null || _c === void 0 ? void 0 : _c.notifyErrorListeners(`'${relationName}' is already defined in '${(_d = this.currentTypeDef) === null || _d === void 0 ? void 0 : _d.type}'`, ctx.relationName().start, undefined); } this.currentTypeDef.relations[relationName] = relationDef; const directlyRelatedUserTypes = (_f = (_e = this.currentRelation) === null || _e === void 0 ? void 0 : _e.typeInfo) === null || _f === void 0 ? void 0 : _f.directly_related_user_types; this.currentTypeDef.metadata.relations[relationName] = { directly_related_user_types: directlyRelatedUserTypes, }; // Only add the module name for a relation when we're parsing an extended type if (this.isModularModel && ctx.parentCtx.EXTEND()) { this.currentTypeDef.metadata.relations[relationName].module = this.moduleName; } } this.currentRelation = undefined; }; // eslint-disable-next-line @typescript-eslint/no-unused-vars this.enterRelationDefDirectAssignment = (_ctx) => { this.currentRelation.typeInfo = { directly_related_user_types: [] }; }; // eslint-disable-next-line @typescript-eslint/no-unused-vars this.exitRelationDefDirectAssignment = (_ctx) => { var _a, _b; const partialRewrite = { this: {}, }; (_b = (_a = this.currentRelation) === null || _a === void 0 ? void 0 : _a.rewrites) === null || _b === void 0 ? void 0 : _b.push(partialRewrite); }; this.exitRelationDefTypeRestriction = (ctx) => { var _a; const relationRef = {}; const baseRestriction = ctx.relationDefTypeRestrictionBase(); if (!baseRestriction) { return; } relationRef.type = (_a = baseRestriction._relationDefTypeRestrictionType) === null || _a === void 0 ? void 0 : _a.getText(); const usersetRestriction = baseRestriction._relationDefTypeRestrictionRelation; const wildcardRestriction = baseRestriction._relationDefTypeRestrictionWildcard; if (ctx.conditionName()) { relationRef.condition = ctx.conditionName().getText(); } if (usersetRestriction) { relationRef.relation = usersetRestriction.getText(); } if (wildcardRestriction) { relationRef.wildcard = {}; } this.currentRelation.typeInfo.directly_related_user_types.push(relationRef); }; this.exitRelationDefRewrite = (ctx) => { var _a, _b; let partialRewrite = { computedUserset: { relation: ctx._rewriteComputedusersetName.getText(), }, }; if (ctx._rewriteTuplesetName) { partialRewrite = { tupleToUserset: Object.assign(Object.assign({}, partialRewrite), { tupleset: { relation: ctx._rewriteTuplesetName.getText(), } }), }; } (_b = (_a = this.currentRelation) === null || _a === void 0 ? void 0 : _a.rewrites) === null || _b === void 0 ? void 0 : _b.push(partialRewrite); }; this.exitRelationRecurse = () => { var _a, _b; const rewrites = (_a = this.currentRelation) === null || _a === void 0 ? void 0 : _a.rewrites; const relationDef = parseExpression(rewrites, (_b = this.currentRelation) === null || _b === void 0 ? void 0 : _b.operator); if (relationDef) { this.currentRelation.rewrites = [relationDef]; } }; this.enterRelationRecurseNoDirect = () => { var _a; (_a = this.rewriteStack) === null || _a === void 0 ? void 0 : _a.push({ rewrites: this.currentRelation.rewrites, operator: this.currentRelation.operator, }); this.currentRelation.rewrites = []; }; this.exitRelationRecurseNoDirect = () => { var _a, _b; const rewrites = (_a = this.currentRelation) === null || _a === void 0 ? void 0 : _a.rewrites; const relationDef = parseExpression(rewrites, (_b = this.currentRelation) === null || _b === void 0 ? void 0 : _b.operator); const popped = this.rewriteStack.pop(); if (relationDef) { this.currentRelation.operator = popped === null || popped === void 0 ? void 0 : popped.operator; this.currentRelation.rewrites = [...popped.rewrites, relationDef]; } }; this.enterRelationDefPartials = (ctx) => { if (ctx.OR_list().length) { this.currentRelation.operator = RelationDefinitionOperator.RELATION_DEFINITION_OPERATOR_OR; } else if (ctx.AND_list().length) { this.currentRelation.operator = RelationDefinitionOperator.RELATION_DEFINITION_OPERATOR_AND; } else if (ctx.BUT_NOT()) { this.currentRelation.operator = RelationDefinitionOperator.RELATION_DEFINITION_OPERATOR_BUT_NOT; } }; this.enterCondition = (ctx) => { var _a; if (ctx.conditionName() === null) { return; } if (!this.authorizationModel.conditions) { this.authorizationModel.conditions = {}; } const conditionName = ctx.conditionName().getText(); if (this.authorizationModel.conditions[conditionName]) { (_a = ctx.parser) === null || _a === void 0 ? void 0 : _a.notifyErrorListeners(`condition '${conditionName}' is already defined in the model`, ctx.conditionName().start, undefined); } this.currentCondition = { name: conditionName, expression: "", parameters: {}, }; if (this.isModularModel) { this.currentCondition.metadata = { module: this.moduleName, }; } }; this.exitConditionParameter = (ctx) => { var _a, _b, _c, _d; if (!ctx.parameterName() || !ctx.parameterType()) { return; } const parameterName = ctx.parameterName().getText(); if ((_b = (_a = this.currentCondition) === null || _a === void 0 ? void 0 : _a.parameters) === null || _b === void 0 ? void 0 : _b[parameterName]) { (_c = ctx.parser) === null || _c === void 0 ? void 0 : _c.notifyErrorListeners(`parameter '${parameterName}' is already defined in the condition '${(_d = this.currentCondition) === null || _d === void 0 ? void 0 : _d.name}'`, ctx.parameterName().start, undefined); } const paramContainer = ctx.parameterType().CONDITION_PARAM_CONTAINER(); const conditionParamTypeRef = {}; if (paramContainer) { conditionParamTypeRef.type_name = `TYPE_NAME_${paramContainer.getText().toUpperCase()}`; const genericTypeName = ctx.parameterType().CONDITION_PARAM_TYPE() && `TYPE_NAME_${ctx.parameterType().CONDITION_PARAM_TYPE().getText().toUpperCase()}`; if (genericTypeName) { conditionParamTypeRef.generic_types = [{ type_name: genericTypeName }]; } } else { conditionParamTypeRef.type_name = `TYPE_NAME_${ctx.parameterType().getText().toUpperCase()}`; } this.currentCondition.parameters[parameterName] = conditionParamTypeRef; }; this.exitConditionExpression = (ctx) => { this.currentCondition.expression = ctx.getText().trim(); }; this.exitCondition = () => { if (this.currentCondition) { this.authorizationModel.conditions[this.currentCondition.name] = this.currentCondition; this.currentCondition = undefined; } }; } } class OpenFgaDslErrorListener extends antlr4_1.ErrorListener { constructor() { super(...arguments); this.errors = []; } syntaxError(_recognizer, offendingSymbol, line, // line is one based, i.e. the first line will be 1 column, // column is zero based, i.e. the first column will be 0 msg, e) { let metadata = undefined; let columnOffset = 0; if (offendingSymbol instanceof antlr.Token) { metadata = { symbol: offendingSymbol.text, }; columnOffset = metadata.symbol.length; } this.errors.push(new errors_1.DSLSyntaxSingleError({ line: { start: line - 1, end: line - 1 }, column: { start: column, end: column + columnOffset }, msg, }, metadata, e)); } } function parseDSL(data) { const cleanedData = data .split("\n") .map((line) => { if (line.trimStart()[0] === "#") { return ""; } return line.split(" #")[0].trimEnd(); }) .join("\n"); const is = new antlr.InputStream(cleanedData); const errorListener = new OpenFgaDslErrorListener(); // Create the Lexer const lexer = new OpenFGALexer_1.default(is); lexer.removeErrorListeners(); lexer.addErrorListener(errorListener); const stream = new antlr.CommonTokenStream(lexer); // Create the Parser const parser = new OpenFGAParser_1.default(stream); parser.removeErrorListeners(); parser.addErrorListener(errorListener); // Finally parse the expression const listener = new OpenFgaDslListener(); new antlr.ParseTreeWalker().walk(listener, parser.main()); return { listener, errorListener }; } /** * transformDSLToJSONObject - Converts models authored in FGA DSL syntax to the json syntax accepted by the OpenFGA API * @param {string} data * @returns {AuthorizationModel} */ function transformDSLToJSONObject(data) { const { listener, errorListener } = parseDSL(data); if (errorListener.errors.length) { throw new errors_1.DSLSyntaxError(errorListener.errors); } return listener.authorizationModel; } /** * transformDSLToJSONObject - Converts models authored in FGA DSL syntax to a stringified json representation * @param {string} data * @returns {string} */ function transformDSLToJSON(data) { return JSON.stringify(transformDSLToJSONObject(data)); } /** * transformModularDSLToJSONObject - Converts a part of a modular model in DSL syntax to the json syntax accepted by * OpenFGA API and also returns the type definitions that are extended in the DSL if any are. * @internal * @param {string} data * @returns {ModularDSLTransformResult} */ function transformModularDSLToJSONObject(data) { const { listener, errorListener } = parseDSL(data); if (errorListener.errors.length) { throw new errors_1.DSLSyntaxError(errorListener.errors); } return { authorizationModel: listener.authorizationModel, typeDefExtensions: listener.typeDefExtensions, }; }