@openfga/frontend-utils
Version:
Exposes helpful utilities for building authoring experiences of OpenFGA Models.
298 lines (297 loc) • 14.1 kB
JavaScript
"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;