Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

#feat16 : Added command graph (browser based). #45

Open
wants to merge 9 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 27 additions & 0 deletions source/commands/graph.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import React from 'react';
import { AuthProvider } from '../components/AuthProvider.js';
import Graph from '../components/graphCommand.js';
import { type infer as zInfer, object, string } from 'zod';
import { option } from 'pastel';

export const options = object({
apiKey: string()
.optional()
.describe(
option({
description: 'The API key for the Permit env, project or Workspace',
}),
),
});

type Props = {
options: zInfer<typeof options>;
};

export default function graph({ options }: Props) {
return (
<AuthProvider>
<Graph options={options} />
</AuthProvider>
);
}
159 changes: 159 additions & 0 deletions source/components/HtmlGraphSaver.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
import { writeFileSync } from 'fs';
import { resolve } from 'path';
import open from 'open';

export const saveHTMLGraph = (graphData: { nodes: any[]; edges: any[] }) => {
const outputHTMLPath = resolve(process.cwd(), 'permit-graph.html');
const htmlTemplate = `
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>ReBAC Graph</title>
<script src="https://cdnjs.cloudflare.com/ajax/libs/cytoscape/3.23.0/cytoscape.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/dagre/0.8.5/dagre.min.js"></script>
<script src="https://unpkg.com/cytoscape-dagre/cytoscape-dagre.js"></script>
<link href="https://fonts.googleapis.com/css2?family=Manrope:wght@400;600&display=swap" rel="stylesheet">

<style>
body {
display: flex;
flex-direction: column;
margin: 0;
height: 100vh;
background-color: rgb(43, 20, 0);
font-family: 'Manrope', Arial, sans-serif;
color: #ffffff;
}

#title {
text-align: center;
font-size: 30px;
font-weight: 600;
height: 50px;
line-height: 50px;
background-clip: text;
-webkit-background-clip: text;
color: transparent; /* Makes the text color transparent to apply gradient */
background-image: linear-gradient(to right, #ffba81, #bb84ff); /* Gradient color */
background-color: rgb(43, 20, 0); /* Same background color as bg-[#2B1400] */
}

#cy {
flex: 1;
width: 100%;
background-color: #FFF1E7; /* Base background color */
padding: 10px;
background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADAAAAAwCAYAAABXAvmHAAAACXBIWXMAABYlAAAWJQFJUiTwAAAAj0lEQVR4Ae3YMQoEIRBE0Ro1NhHvfz8xMhXc3RnYGyjFwH8n6E931NfnRy8W9HIEuBHgRoAbAW4EuBHgRoAbAW4EuBHglrTZGEOtNcUYVUpRzlknbd9A711rLc05n5DTtgfcw//dWzhte0CtVSklhRCeEzrt4jNnRoAbAW4EuBHgRoAbAW4EuBHgRoAbAW5fFH4dU6tFNJ4AAAAASUVORK5CYII=");
background-size: 30px 35px; /* Matches the original image dimensions */
background-repeat: repeat; /* Ensures the pattern repeats */
}
</style>
</head>
<body>
<div id="title">Permit ReBAC Graph</div>
<div id="cy"></div>
<script>
const graphData = ${JSON.stringify(graphData, null, 2)};
cytoscape.use(cytoscapeDagre);

const cy = cytoscape({
container: document.getElementById('cy'),
elements: [...graphData.nodes, ...graphData.edges],
style: [
{
selector: 'edge',
style: {
'line-color': 'rgb(18, 165, 148)',
'width': 5,
'shape': 'round-rectangle',
'target-arrow-shape': 'triangle',
'target-arrow-color': 'rgb(18, 165, 148)',
'curve-style': 'taxi',
'taxi-turn': 30,
'taxi-direction': 'downward',
'taxi-turn-min-distance': 20,
'label': 'data(label)',
'color': '#ffffff',
'font-size': 25,
'font-family': 'Manrope, Arial, sans-serif',
'font-weight': 500, /* Adjusted for edge labels */
'text-background-color': 'rgb(18, 165, 148)',
'text-background-opacity': 0.8,
'text-background-padding': 8,
'text-margin-y': 0,
},
},{
selector: 'edge.relationship-connection',
style: {
'line-color': '#F76808',
'target-arrow-color': '#F76808',
'text-background-color': '#F76808',
}
},{
selector: 'edge.implicit-role-connection',
style: {
'line-color': 'rgb(18, 165, 148)',
'line-style': 'dashed',
'target-arrow-color': 'rgb(18, 165, 148)',
'text-background-color': 'rgb(18, 165, 148)',
}
},
{
selector: 'node',
style: {
'background-color': 'rgb(255, 255, 255)',
'border-color': 'rgb(211, 179, 250)',
'border-width': 8,
'shape': 'round-rectangle',
'label': 'data(label)',
'color': 'rgb(151, 78, 242)',
'font-size': 30,
'font-family': 'Manrope, Arial, sans-serif',
'font-weight': 700, /* Adjusted for node labels */
'text-valign': 'center',
'text-halign': 'center',
'width': 'label',
'height': 'label',
'padding': 45,
},
},{
selector: 'node.user-node',
style: {
'border-color': '#FFB381', /*light Orange border */
'color': '#F76808', /* Orange text */

},
},
{
selector: 'node.resource-instance-node',
style: {
'border-color': '#D3B3FA', /* light Purple border */
'color': '#974EF2', /* Purple text */

},
},
],
layout: {
name: 'dagre',
rankDir: 'LR',
nodeSep: 70,
edgeSep: 50,
rankSep: 150,
animate: true,
fit: true,
padding: 20,
directed: true,
spacingFactor: 1.5,
},
});
</script>
</body>
</html>
`;

writeFileSync(outputHTMLPath, htmlTemplate, 'utf8');
console.log(`Graph saved as: ${outputHTMLPath}`);
open(outputHTMLPath);
};
106 changes: 106 additions & 0 deletions source/components/generateGraphData.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
// Define types
type ResourceInstance = {
label: string;
value: string;
id: string;
};

type Relationship = {
label: string;
value: string;
};

type RoleAssignment = {
user: string;
role: string;
resourceInstance: string;
};

// Generate Graph Data
export const generateGraphData = (
resources: ResourceInstance[],
relationships: Map<string, Relationship[]>,
roleAssignments: RoleAssignment[],
) => {
const nodes: { data: { id: string; label: string }; classes?: string }[] =
resources.map(resource => ({
data: { id: resource.id, label: ` ${resource.label}` },
classes: 'resource-instance-node',
}));

const edges: {
data: { source: string; target: string; label: string };
classes?: string;
}[] = [];
const existingNodeIds = new Set(nodes.map(node => node.data.id));

relationships.forEach((relations, resourceId) => {
relations.forEach(relation => {
if (!existingNodeIds.has(relation.value)) {
nodes.push({
data: { id: relation.value, label: `${relation.value}` },
});
existingNodeIds.add(relation.value);
}

edges.push({
data: {
source: resourceId,
target: relation.value,
label: relation.label,
},
classes: 'relationship-connection', // Class for orange lines
});
});
});

// add role assignments to the graph
roleAssignments.forEach(assignment => {
// Add user nodes with a specific class
if (!existingNodeIds.has(assignment.user)) {
nodes.push({
data: { id: assignment.user, label: `${assignment.user}` },
classes: 'user-node',
});
existingNodeIds.add(assignment.user);
}

// Connect user to resource instance
edges.push({
data: {
source: assignment.user,
target: assignment.resourceInstance,
label: `${assignment.role}`,
},
});
});

// Infer implicit assignments and add them as dashed green lines
const implicitAssignments: { user: string; resourceInstance: string }[] = [];
relationships.forEach((relations, sourceId) => {
const directlyAssignedUsers = roleAssignments
.filter(assignment => assignment.resourceInstance === sourceId)
.map(assignment => assignment.user);

relations.forEach(relation => {
directlyAssignedUsers.forEach(user => {
implicitAssignments.push({
user, // The user indirectly assigned
resourceInstance: relation.value, // Target resource instance
});
});
});
});

implicitAssignments.forEach(assignment => {
edges.push({
data: {
source: assignment.user,
target: assignment.resourceInstance,
label: 'DERIVES', // Label for dashed green lines
},
classes: 'implicit-role-connection', // Class for styling dashed green lines
});
});
return { nodes, edges };
};
Loading