UNPKG

@openfga/frontend-utils

Version:

Exposes helpful utilities for building authoring experiences of OpenFGA Models.

298 lines (297 loc) 14.1 kB
"use strict"; var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } return new (P || (P = Promise))(function (resolve, reject) { function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } step((generator = generator.apply(thisArg, _arguments || [])).next()); }); }; Object.defineProperty(exports, "__esModule", { value: true }); exports.TreeBuilder = void 0; const graph_typings_1 = require("./graph.typings"); /** * Taking a string representation of a user (object or userset), returns the proper object fields * * @param {string} userString - a string in the form of: `<type>:<id>`, `<type>:<id>#<relation>`, `<type>:*` * * @typedef {Object} UserComponents * @property {string} object - a string in the form of: `<type>:<id>` * @property {string} relation - the relation part of a userset * @property {boolean} isWildcard * * @return {UserComponents} - the object and relation components of a user */ const getUserComponents = (userString) => { const objectRelation = userString.split("#"); const isWildcard = objectRelation[1] === "*" || userString === "*"; return { object: objectRelation[0], relation: isWildcard ? undefined : objectRelation[1], isWildcard, }; }; class TreeBuilder { constructor(openFgaApi, capturedTuple, storeId, existingTree, authorizationModelId) { this.openFgaApi = openFgaApi; this.capturedTuple = capturedTuple; this.storeId = storeId; this.existingTree = existingTree; this.authorizationModelId = authorizationModelId; this.expandedTuples = {}; if (existingTree) { this.currentTree = existingTree; } } addParent(user, parent, type, inActivePath) { if (!this.currentTree) return; if (!this.currentTree[user]) { this.currentTree[user] = { parents: {} }; } this.currentTree[user].parents[parent] = { inActivePath, type }; if (parent === `${this.capturedTuple.object}#${this.capturedTuple.relation}`) { if (!this.currentTree[this.capturedTuple.object]) { this.currentTree[this.capturedTuple.object] = { parents: {}, inActivePath: true }; } this.addParent(`${this.capturedTuple.object}#${this.capturedTuple.relation}`, this.capturedTuple.object, undefined, true); } } expandTuple(tuple) { return __awaiter(this, void 0, void 0, function* () { return this.openFgaApi.expand(this.storeId, { tuple_key: { relation: tuple.relation, object: tuple.object, }, authorization_model_id: this.authorizationModelId, }); }); } walkDirectUser(node, user) { return __awaiter(this, void 0, void 0, function* () { const tupleKey = getUserComponents(user); this.addParent(user, node.name, graph_typings_1.RelationType.DirectUsers); // this is a relation that can be broken down further, e.g. github-org:auth0#member if (tupleKey.relation) { yield this.walk(tupleKey); } }); } walkDirectUsers(node) { return __awaiter(this, void 0, void 0, function* () { var _a, _b; const users = ((_b = (_a = node === null || node === void 0 ? void 0 : node.leaf) === null || _a === void 0 ? void 0 : _a.users) === null || _b === void 0 ? void 0 : _b.users) || []; yield Promise.all(users.map((user) => __awaiter(this, void 0, void 0, function* () { return this.walkDirectUser(node, user); }))); }); } walkComputedUserSet(node, computedUserSet, viaTupleToUserset) { return __awaiter(this, void 0, void 0, function* () { if (!computedUserSet || computedUserSet.split(":").length !== 2) { return; } this.addParent(computedUserSet, node.name, viaTupleToUserset ? graph_typings_1.RelationType.TupleToUserset : graph_typings_1.RelationType.ComputedUserset); const tupleKey = getUserComponents(computedUserSet); yield this.walk(tupleKey); }); } walkTupleToUserset(node) { return __awaiter(this, void 0, void 0, function* () { var _a, _b; const tupleToUsersetTupleset = (_b = (_a = node.leaf) === null || _a === void 0 ? void 0 : _a.tupleToUserset) === null || _b === void 0 ? void 0 : _b.tupleset; if (!tupleToUsersetTupleset) { return; } const tupleKey = getUserComponents(tupleToUsersetTupleset); // this.addParent(tupleToUsersetTupleset, node.name, RelationType.TupleToUserset); yield this.walk(tupleKey); }); } getNodeType(node) { var _a, _b, _c, _d, _e, _f; if ((_b = (_a = node === null || node === void 0 ? void 0 : node.leaf) === null || _a === void 0 ? void 0 : _a.computed) === null || _b === void 0 ? void 0 : _b.userset) { return graph_typings_1.RelationType.ComputedUserset; } if ((_d = (_c = node === null || node === void 0 ? void 0 : node.leaf) === null || _c === void 0 ? void 0 : _c.tupleToUserset) === null || _d === void 0 ? void 0 : _d.computed) { return graph_typings_1.RelationType.TupleToUserset; } if (!((_f = (_e = node === null || node === void 0 ? void 0 : node.union) === null || _e === void 0 ? void 0 : _e.nodes) === null || _f === void 0 ? void 0 : _f.length)) { return graph_typings_1.RelationType.DirectUsers; } } walkNode(node) { return __awaiter(this, void 0, void 0, function* () { var _a, _b, _c, _d, _e; const promises = []; const type = this.getNodeType(node); switch (type) { case graph_typings_1.RelationType.DirectUsers: promises.push(this.walkDirectUsers(node)); break; case graph_typings_1.RelationType.ComputedUserset: const computedUserSet = (_b = (_a = node.leaf) === null || _a === void 0 ? void 0 : _a.computed) === null || _b === void 0 ? void 0 : _b.userset; promises.push(this.walkComputedUserSet(node, computedUserSet)); break; case graph_typings_1.RelationType.TupleToUserset: promises.push(this.walkTupleToUserset(node)); const computedList = ((_d = (_c = node.leaf) === null || _c === void 0 ? void 0 : _c.tupleToUserset) === null || _d === void 0 ? void 0 : _d.computed) || []; promises.push(...computedList.map((computed) => __awaiter(this, void 0, void 0, function* () { return this.walkComputedUserSet(node, computed.userset, true); }))); break; // Union default: const childNodes = ((_e = node.union) === null || _e === void 0 ? void 0 : _e.nodes) || []; promises.push(...childNodes.map((childNode) => __awaiter(this, void 0, void 0, function* () { return this.walkNode(childNode); }))); } yield Promise.all(promises); }); } walk(currentTuple) { return __awaiter(this, void 0, void 0, function* () { var _a; const currentTupleKey = `${currentTuple.object}#${currentTuple.relation}`; if (!currentTuple.relation || this.expandedTuples[currentTupleKey]) { return; } if (this.currentTree && !this.currentTree[currentTupleKey]) { this.currentTree[currentTupleKey] = { parents: {} }; } this.expandedTuples[currentTupleKey] = true; const data = yield this.expandTuple(currentTuple); const rootNode = (_a = data.tree) === null || _a === void 0 ? void 0 : _a.root; yield this.walkNode(rootNode); }); } // This is a workaround for the tree generation function generating nodes the have no parents // If we find one, clear it - logic while drawing the graph will deal with hiding nodes linked to it deleteHangingNodes() { const { tree = {} } = this; Object.keys(tree).forEach((nodeName) => { const { parents } = tree[nodeName]; if (!Object.keys(parents).length && nodeName !== this.capturedTuple.object) { // eslint-disable-next-line @typescript-eslint/no-dynamic-delete delete tree[nodeName]; } }); } get tree() { return this.currentTree; } buildTree() { return __awaiter(this, void 0, void 0, function* () { if (this.tree) { return; } this.currentTree = {}; yield this.walk(this.capturedTuple); }); } fillActivePath(targetUser) { const { tree = {} } = this; const { object: targetObject } = this.capturedTuple; // Clone the tree const fullTreeObject = JSON.parse(JSON.stringify(tree)); let nextPaths = ["*", targetUser]; const traversedPaths = {}; while (nextPaths.length) { const nextPathsObject = {}; nextPaths.forEach((nextPath) => { if (traversedPaths[nextPath]) { return; } else { traversedPaths[nextPath] = true; } if (!fullTreeObject[nextPath]) { return; } if (fullTreeObject[nextPath].parents[targetObject]) { // if one of the parents is the actual object we are looking for, delete all other parents // this prevents looping cases where we find the object and still tru to keep going Object.keys(fullTreeObject[nextPath].parents).forEach((parent) => { if (parent !== targetObject) { delete fullTreeObject[nextPath].parents[parent]; } }); } Object.keys(fullTreeObject[nextPath].parents).forEach((parent) => { if (parent === nextPath) { return; } fullTreeObject[nextPath].parents[parent].inActivePath = true; nextPathsObject[parent] = true; }); }); nextPaths = Object.keys(nextPathsObject); } return fullTreeObject; } buildGraph(targetUser, onlyInActivePath) { const { capturedTuple } = this; let { tree = {} } = this; const hasUser = !!targetUser; const graph = { nodes: [], edges: [], }; let shouldHideNode = false; this.deleteHangingNodes(); if (targetUser) { tree = this.fillActivePath(targetUser); } Object.keys(tree).forEach((nodeKey) => { const node = tree[nodeKey]; const [object, relation] = nodeKey.split("#"); let hasFoundParentInActivePath; Object.keys(node.parents).forEach((parentKey) => { var _a; const parentNode = node.parents[parentKey]; // If the parent node does not exist in the tree, ignore the whole node if (!tree[parentKey]) { shouldHideNode = true; return; } if (onlyInActivePath && !parentNode.inActivePath) { return; } hasFoundParentInActivePath = true; const type = (_a = parentNode.type) === null || _a === void 0 ? void 0 : _a.replace(/\s/gu, ""); if (type) { graph.nodes.push({ id: `${parentKey}.${type}.${nodeKey}`, label: parentNode.type, }); graph.edges.push({ to: `${parentKey}.${type}.${nodeKey}`, from: parentKey, isActive: parentNode.inActivePath, group: graph_typings_1.GraphEdgeGroup.Default, }); } graph.edges.push({ to: nodeKey, from: type ? `${parentKey}.${type}.${nodeKey}` : parentKey, label: parentKey === capturedTuple.object ? `${relation} from` : "", isActive: parentNode.inActivePath, group: graph_typings_1.GraphEdgeGroup.Default, }); }); const isUserNode = nodeKey === targetUser || (nodeKey === "*" && hasUser); const isObjectNode = nodeKey === capturedTuple.object; // Only add the node if: // 1- It is the main object we are checking for OR // 2- It has a parent in the "active" path for the relationship OR // 3- We are not drawing only the active oath or force hiding the node if (isObjectNode || hasFoundParentInActivePath || !(onlyInActivePath || shouldHideNode)) { const isActive = isObjectNode || isUserNode; graph.nodes.push({ id: nodeKey, label: object === "*" && hasUser ? `${targetUser} via everyone (*)` : nodeKey, isActive, }); } }); return graph; } } exports.TreeBuilder = TreeBuilder;