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