From 050a25bc74641488000e4fc5413c8528d5ef557c Mon Sep 17 00:00:00 2001 From: harshtech123 Date: Mon, 9 Dec 2024 14:17:37 +0530 Subject: [PATCH 1/8] added graph command --- permit-graph.html | 238 +++++++++++++++++++++++++ source/commands/graph.tsx | 23 +++ source/components/HtmlGraphSaver.ts | 117 ++++++++++++ source/components/generateGraphData.ts | 81 +++++++++ source/components/graphCommand.tsx | 228 +++++++++++++++++++++++ source/lib/api.ts | 1 + 6 files changed, 688 insertions(+) create mode 100644 permit-graph.html create mode 100644 source/commands/graph.tsx create mode 100644 source/components/HtmlGraphSaver.ts create mode 100644 source/components/generateGraphData.ts create mode 100644 source/components/graphCommand.tsx diff --git a/permit-graph.html b/permit-graph.html new file mode 100644 index 0000000..d4c760e --- /dev/null +++ b/permit-graph.html @@ -0,0 +1,238 @@ + + + + + + + ReBAC Graph + + + + + + +
Permit ReBAC Graph
+
+ + + diff --git a/source/commands/graph.tsx b/source/commands/graph.tsx new file mode 100644 index 0000000..cdf6907 --- /dev/null +++ b/source/commands/graph.tsx @@ -0,0 +1,23 @@ +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; +}; + +export default function graph({ options }: Props) { + return ( + + + + ); +} diff --git a/source/components/HtmlGraphSaver.ts b/source/components/HtmlGraphSaver.ts new file mode 100644 index 0000000..07aeed3 --- /dev/null +++ b/source/components/HtmlGraphSaver.ts @@ -0,0 +1,117 @@ +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 = ` + + + + + + ReBAC Graph + + + + + + +
Permit ReBAC Graph
+
+ + + +`; + writeFileSync(outputHTMLPath, htmlTemplate, 'utf8'); + console.log(`Graph saved as: ${outputHTMLPath}`); + open(outputHTMLPath); +}; diff --git a/source/components/generateGraphData.ts b/source/components/generateGraphData.ts new file mode 100644 index 0000000..ebe1744 --- /dev/null +++ b/source/components/generateGraphData.ts @@ -0,0 +1,81 @@ +// 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, + roleAssignments: RoleAssignment[] +) => { + const nodes = resources.map((resource) => ({ + data: { id: resource.id, label: `Resource: ${resource.label}` }, + })); + + const edges: { data: { source: string; target: string; label: 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: `Resource: ${relation.value}` } }); + existingNodeIds.add(relation.value); + } + + edges.push({ + data: { source: resourceId, target: relation.value, label: relation.label }, + }); + }); + }); + + // Add role assignments to the graph + roleAssignments.forEach((assignment) => { + // Add user nodes + if (!existingNodeIds.has(assignment.user)) { + nodes.push({ data: { id: assignment.user, label: `User: ${assignment.user}` } }); + existingNodeIds.add(assignment.user); + } + + // Add role nodes + const roleNodeId = `role:${assignment.role}`; + if (!existingNodeIds.has(roleNodeId)) { + nodes.push({ data: { id: roleNodeId, label: `Role: ${assignment.role}` } }); + existingNodeIds.add(roleNodeId); + } + + // Connect user to role + edges.push({ + data: { + source: assignment.user, + target: roleNodeId, + label: `Assigned role`, + }, + }); + + // Connect role to resource instance + edges.push({ + data: { + source: roleNodeId, + target: assignment.resourceInstance, + label: `Grants access`, + }, + }); + }); + + return { nodes, edges }; +}; diff --git a/source/components/graphCommand.tsx b/source/components/graphCommand.tsx new file mode 100644 index 0000000..b992b1a --- /dev/null +++ b/source/components/graphCommand.tsx @@ -0,0 +1,228 @@ +import React, { useEffect, useState } from 'react'; +import { Text } from 'ink'; +import SelectInput from 'ink-select-input'; +import Spinner from 'ink-spinner'; +import { apiCall } from '../lib/api.js'; +import { saveHTMLGraph } from '../components/HtmlGraphSaver.js'; +import { generateGraphData } from '../components/generateGraphData.js'; +import zod from 'zod'; +import { option } from 'pastel'; +import { useAuth } from '../components/AuthProvider.js'; // Import useAuth + +// Define types +type Relationship = { + label: string; + value: string; +}; + +type RoleAssignment = { + user: string; + role: string; + resourceInstance: string; +}; + +export const options = zod.object({ + apiKey: zod + .string() + .optional() + .describe( + option({ + description: 'The API key for the Permit env, project or Workspace', + }), + ), +}); + +type Props = { + options: zod.infer; +}; + +export default function Graph({ options }: Props) { + const { authToken: contextAuthToken, loading: authLoading, error: authError } = useAuth(); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); + const [authToken, setAuthToken] = useState(null); // Store resolved authToken + const [state, setState] = useState<'project' | 'environment' | 'graph'>('project'); + const [projects, setProjects] = useState<[]>([]); + const [environments, setEnvironments] = useState<[]>([]); + const [selectedProject, setSelectedProject] = useState(null); + const [selectedEnvironment, setSelectedEnvironment] = useState(null); + + // Resolve the authToken on mount + useEffect(() => { + const token = contextAuthToken || options.apiKey || null; + if (!token) { + setError('No auth token found. Please log in or provide an API key.'); + } else { + setAuthToken(token); + } + }, [contextAuthToken, options.apiKey]); + + // Fetch projects + useEffect(() => { + const fetchProjects = async () => { + if (!authToken) return; + + try { + setLoading(true); + const { response: projects } = await apiCall('v2/projects', authToken); + setProjects( + projects['map']((project: any) => ({ + label: project.name, + value: project.id, + })) + ); + setLoading(false); + } catch (err) { + console.error('Error fetching projects:', err); + setError('Failed to fetch projects.'); + setLoading(false); + } + }; + + if (state === 'project') { + fetchProjects(); + } + }, [state, authToken]); + + // Fetch environments + useEffect(() => { + const fetchEnvironments = async () => { + if (!authToken || !selectedProject) return; + + try { + setLoading(true); + const { response: environments } = await apiCall( + `v2/projects/${selectedProject.value}/envs`, + authToken + ); + setEnvironments( + environments['map']((env: any) => ({ + label: env.name, + value: env.id, + })) + ); + setLoading(false); + } catch (err) { + console.error('Error fetching environments:', err); + setError('Failed to fetch environments.'); + setLoading(false); + } + }; + + if (state === 'environment') { + fetchEnvironments(); + } + }, [state, authToken, selectedProject]); + + // Fetch graph data + useEffect(() => { + const fetchData = async () => { + if (!authToken || !selectedProject || !selectedEnvironment) return; + + try { + setLoading(true); + + const resourceResponse = await apiCall( + `v2/facts/${selectedProject.value}/${selectedEnvironment.value}/resource_instances?detailed=true`, + authToken + ); + + const resourcesData = resourceResponse.response['map']((res: any) => ({ + label: res.resource, + value: res.id, + id: res.id, + })); + + const relationsMap = new Map(); + resourceResponse.response['forEach']((resource: any) => { + const relationsData = resource.relationships || []; + relationsMap.set( + resource.id, + relationsData.map((relation: any) => ({ + label: `${relation.relation} → ${relation.object}`, + value: relation.object || 'Unknown ID', + })) + ); + }); + + const roleAssignmentsData: RoleAssignment[] = []; + for (const resource of resourcesData) { + const roleResponse = await apiCall( + `v2/facts/${selectedProject.value}/${selectedEnvironment.value}/role_assignments?resource_instance=${resource.id}`, + authToken + ); + + roleAssignmentsData.push( + ...roleResponse.response['map']((role: any) => ({ + user: role.user || 'Unknown User', + role: role.role || 'Unknown Role', + resourceInstance: resource.id, + })) + ); + } + + const graphData = generateGraphData(resourcesData, relationsMap, roleAssignmentsData); + saveHTMLGraph(graphData); + setLoading(false); + } catch (err) { + console.error('Error fetching graph data:', err); + setError('Failed to fetch data. Check network or auth token.'); + setLoading(false); + } + }; + + if (state === 'graph') { + fetchData(); + } + }, [state, authToken, selectedProject, selectedEnvironment]); + + // Loading and error states + if (authLoading || loading) { + return ( + + {authLoading ? 'Authenticating...' : 'Loading Permit Graph...'} + + ); + } + + if (authError || error) { + return {authError || error}; + } + + // State rendering + if (state === 'project' && projects.length > 0) { + return ( + <> + Select a project + { + setSelectedProject(project); + setState('environment'); + }} + /> + + ); + } + + if (state === 'environment' && environments.length > 0) { + return ( + <> + Select an environment + { + setSelectedEnvironment(environment); + setState('graph'); + }} + /> + + ); + } + + if (state === 'graph') { + return Graph generated successfully and saved as HTML!; + } + + return Initializing...; +} diff --git a/source/lib/api.ts b/source/lib/api.ts index ddf0e6f..da90c4c 100644 --- a/source/lib/api.ts +++ b/source/lib/api.ts @@ -1,6 +1,7 @@ import { PERMIT_API_URL } from '../config.js'; interface ApiResponseData { + [x: string]: any; id?: string; name?: string; } From c5a5fe6412f864bc830d9d4b94ce53b6d4b25e50 Mon Sep 17 00:00:00 2001 From: harshtech123 Date: Mon, 9 Dec 2024 14:20:49 +0530 Subject: [PATCH 2/8] Delete permit-graph.html --- permit-graph.html | 238 ---------------------------------------------- 1 file changed, 238 deletions(-) delete mode 100644 permit-graph.html diff --git a/permit-graph.html b/permit-graph.html deleted file mode 100644 index d4c760e..0000000 --- a/permit-graph.html +++ /dev/null @@ -1,238 +0,0 @@ - - - - - - - ReBAC Graph - - - - - - -
Permit ReBAC Graph
-
- - - From 9f5532688f09404e5c7de3df53a5eefd48a4a15d Mon Sep 17 00:00:00 2001 From: harshtech123 Date: Tue, 10 Dec 2024 21:45:55 +0530 Subject: [PATCH 3/8] prettiefy --- source/commands/graph.tsx | 22 +- source/components/HtmlGraphSaver.ts | 10 +- source/components/generateGraphData.ts | 128 ++++---- source/components/graphCommand.tsx | 417 +++++++++++++------------ source/lib/api.ts | 2 +- 5 files changed, 303 insertions(+), 276 deletions(-) diff --git a/source/commands/graph.tsx b/source/commands/graph.tsx index cdf6907..0b5a11b 100644 --- a/source/commands/graph.tsx +++ b/source/commands/graph.tsx @@ -5,19 +5,23 @@ 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' })), + apiKey: string() + .optional() + .describe( + option({ + description: 'The API key for the Permit env, project or Workspace', + }), + ), }); type Props = { - options: zInfer; + options: zInfer; }; export default function graph({ options }: Props) { - return ( - - - - ); + return ( + + + + ); } diff --git a/source/components/HtmlGraphSaver.ts b/source/components/HtmlGraphSaver.ts index 07aeed3..8ad513c 100644 --- a/source/components/HtmlGraphSaver.ts +++ b/source/components/HtmlGraphSaver.ts @@ -3,8 +3,8 @@ 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 = ` + const outputHTMLPath = resolve(process.cwd(), 'permit-graph.html'); + const htmlTemplate = ` @@ -111,7 +111,7 @@ style: { `; - writeFileSync(outputHTMLPath, htmlTemplate, 'utf8'); - console.log(`Graph saved as: ${outputHTMLPath}`); - open(outputHTMLPath); + writeFileSync(outputHTMLPath, htmlTemplate, 'utf8'); + console.log(`Graph saved as: ${outputHTMLPath}`); + open(outputHTMLPath); }; diff --git a/source/components/generateGraphData.ts b/source/components/generateGraphData.ts index ebe1744..59841bc 100644 --- a/source/components/generateGraphData.ts +++ b/source/components/generateGraphData.ts @@ -1,81 +1,91 @@ // Define types type ResourceInstance = { - label: string; - value: string; - id: string; + label: string; + value: string; + id: string; }; type Relationship = { - label: string; - value: string; + label: string; + value: string; }; type RoleAssignment = { - user: string; - role: string; - resourceInstance: string; + user: string; + role: string; + resourceInstance: string; }; - // Generate Graph Data export const generateGraphData = ( - resources: ResourceInstance[], - relationships: Map, - roleAssignments: RoleAssignment[] + resources: ResourceInstance[], + relationships: Map, + roleAssignments: RoleAssignment[], ) => { - const nodes = resources.map((resource) => ({ - data: { id: resource.id, label: `Resource: ${resource.label}` }, - })); + const nodes = resources.map(resource => ({ + data: { id: resource.id, label: `Resource: ${resource.label}` }, + })); - const edges: { data: { source: string; target: string; label: string } }[] = []; - const existingNodeIds = new Set(nodes.map((node) => node.data.id)); + const edges: { data: { source: string; target: string; label: 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: `Resource: ${relation.value}` } }); - existingNodeIds.add(relation.value); - } + relationships.forEach((relations, resourceId) => { + relations.forEach(relation => { + if (!existingNodeIds.has(relation.value)) { + nodes.push({ + data: { id: relation.value, label: `Resource: ${relation.value}` }, + }); + existingNodeIds.add(relation.value); + } - edges.push({ - data: { source: resourceId, target: relation.value, label: relation.label }, - }); - }); - }); + edges.push({ + data: { + source: resourceId, + target: relation.value, + label: relation.label, + }, + }); + }); + }); - // Add role assignments to the graph - roleAssignments.forEach((assignment) => { - // Add user nodes - if (!existingNodeIds.has(assignment.user)) { - nodes.push({ data: { id: assignment.user, label: `User: ${assignment.user}` } }); - existingNodeIds.add(assignment.user); - } + // Add role assignments to the graph + roleAssignments.forEach(assignment => { + // Add user nodes + if (!existingNodeIds.has(assignment.user)) { + nodes.push({ + data: { id: assignment.user, label: `User: ${assignment.user}` }, + }); + existingNodeIds.add(assignment.user); + } - // Add role nodes - const roleNodeId = `role:${assignment.role}`; - if (!existingNodeIds.has(roleNodeId)) { - nodes.push({ data: { id: roleNodeId, label: `Role: ${assignment.role}` } }); - existingNodeIds.add(roleNodeId); - } + // Add role nodes + const roleNodeId = `role:${assignment.role}`; + if (!existingNodeIds.has(roleNodeId)) { + nodes.push({ + data: { id: roleNodeId, label: `Role: ${assignment.role}` }, + }); + existingNodeIds.add(roleNodeId); + } - // Connect user to role - edges.push({ - data: { - source: assignment.user, - target: roleNodeId, - label: `Assigned role`, - }, - }); + // Connect user to role + edges.push({ + data: { + source: assignment.user, + target: roleNodeId, + label: `Assigned role`, + }, + }); - // Connect role to resource instance - edges.push({ - data: { - source: roleNodeId, - target: assignment.resourceInstance, - label: `Grants access`, - }, - }); - }); + // Connect role to resource instance + edges.push({ + data: { + source: roleNodeId, + target: assignment.resourceInstance, + label: `Grants access`, + }, + }); + }); - return { nodes, edges }; + return { nodes, edges }; }; diff --git a/source/components/graphCommand.tsx b/source/components/graphCommand.tsx index b992b1a..89d8626 100644 --- a/source/components/graphCommand.tsx +++ b/source/components/graphCommand.tsx @@ -11,218 +11,231 @@ import { useAuth } from '../components/AuthProvider.js'; // Import useAuth // Define types type Relationship = { - label: string; - value: string; + label: string; + value: string; }; type RoleAssignment = { - user: string; - role: string; - resourceInstance: string; + user: string; + role: string; + resourceInstance: string; }; export const options = zod.object({ - apiKey: zod - .string() - .optional() - .describe( - option({ - description: 'The API key for the Permit env, project or Workspace', - }), - ), + apiKey: zod + .string() + .optional() + .describe( + option({ + description: 'The API key for the Permit env, project or Workspace', + }), + ), }); type Props = { - options: zod.infer; + options: zod.infer; }; export default function Graph({ options }: Props) { - const { authToken: contextAuthToken, loading: authLoading, error: authError } = useAuth(); - const [loading, setLoading] = useState(false); - const [error, setError] = useState(null); - const [authToken, setAuthToken] = useState(null); // Store resolved authToken - const [state, setState] = useState<'project' | 'environment' | 'graph'>('project'); - const [projects, setProjects] = useState<[]>([]); - const [environments, setEnvironments] = useState<[]>([]); - const [selectedProject, setSelectedProject] = useState(null); - const [selectedEnvironment, setSelectedEnvironment] = useState(null); - - // Resolve the authToken on mount - useEffect(() => { - const token = contextAuthToken || options.apiKey || null; - if (!token) { - setError('No auth token found. Please log in or provide an API key.'); - } else { - setAuthToken(token); - } - }, [contextAuthToken, options.apiKey]); - - // Fetch projects - useEffect(() => { - const fetchProjects = async () => { - if (!authToken) return; - - try { - setLoading(true); - const { response: projects } = await apiCall('v2/projects', authToken); - setProjects( - projects['map']((project: any) => ({ - label: project.name, - value: project.id, - })) - ); - setLoading(false); - } catch (err) { - console.error('Error fetching projects:', err); - setError('Failed to fetch projects.'); - setLoading(false); - } - }; - - if (state === 'project') { - fetchProjects(); - } - }, [state, authToken]); - - // Fetch environments - useEffect(() => { - const fetchEnvironments = async () => { - if (!authToken || !selectedProject) return; - - try { - setLoading(true); - const { response: environments } = await apiCall( - `v2/projects/${selectedProject.value}/envs`, - authToken - ); - setEnvironments( - environments['map']((env: any) => ({ - label: env.name, - value: env.id, - })) - ); - setLoading(false); - } catch (err) { - console.error('Error fetching environments:', err); - setError('Failed to fetch environments.'); - setLoading(false); - } - }; - - if (state === 'environment') { - fetchEnvironments(); - } - }, [state, authToken, selectedProject]); - - // Fetch graph data - useEffect(() => { - const fetchData = async () => { - if (!authToken || !selectedProject || !selectedEnvironment) return; - - try { - setLoading(true); - - const resourceResponse = await apiCall( - `v2/facts/${selectedProject.value}/${selectedEnvironment.value}/resource_instances?detailed=true`, - authToken - ); - - const resourcesData = resourceResponse.response['map']((res: any) => ({ - label: res.resource, - value: res.id, - id: res.id, - })); - - const relationsMap = new Map(); - resourceResponse.response['forEach']((resource: any) => { - const relationsData = resource.relationships || []; - relationsMap.set( - resource.id, - relationsData.map((relation: any) => ({ - label: `${relation.relation} → ${relation.object}`, - value: relation.object || 'Unknown ID', - })) - ); - }); - - const roleAssignmentsData: RoleAssignment[] = []; - for (const resource of resourcesData) { - const roleResponse = await apiCall( - `v2/facts/${selectedProject.value}/${selectedEnvironment.value}/role_assignments?resource_instance=${resource.id}`, - authToken - ); - - roleAssignmentsData.push( - ...roleResponse.response['map']((role: any) => ({ - user: role.user || 'Unknown User', - role: role.role || 'Unknown Role', - resourceInstance: resource.id, - })) - ); - } - - const graphData = generateGraphData(resourcesData, relationsMap, roleAssignmentsData); - saveHTMLGraph(graphData); - setLoading(false); - } catch (err) { - console.error('Error fetching graph data:', err); - setError('Failed to fetch data. Check network or auth token.'); - setLoading(false); - } - }; - - if (state === 'graph') { - fetchData(); - } - }, [state, authToken, selectedProject, selectedEnvironment]); - - // Loading and error states - if (authLoading || loading) { - return ( - - {authLoading ? 'Authenticating...' : 'Loading Permit Graph...'} - - ); - } - - if (authError || error) { - return {authError || error}; - } - - // State rendering - if (state === 'project' && projects.length > 0) { - return ( - <> - Select a project - { - setSelectedProject(project); - setState('environment'); - }} - /> - - ); - } - - if (state === 'environment' && environments.length > 0) { - return ( - <> - Select an environment - { - setSelectedEnvironment(environment); - setState('graph'); - }} - /> - - ); - } - - if (state === 'graph') { - return Graph generated successfully and saved as HTML!; - } - - return Initializing...; + const { + authToken: contextAuthToken, + loading: authLoading, + error: authError, + } = useAuth(); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); + const [authToken, setAuthToken] = useState(null); // Store resolved authToken + const [state, setState] = useState<'project' | 'environment' | 'graph'>( + 'project', + ); + const [projects, setProjects] = useState<[]>([]); + const [environments, setEnvironments] = useState<[]>([]); + const [selectedProject, setSelectedProject] = useState(null); + const [selectedEnvironment, setSelectedEnvironment] = useState( + null, + ); + + // Resolve the authToken on mount + useEffect(() => { + const token = contextAuthToken || options.apiKey || null; + if (!token) { + setError('No auth token found. Please log in or provide an API key.'); + } else { + setAuthToken(token); + } + }, [contextAuthToken, options.apiKey]); + + // Fetch projects + useEffect(() => { + const fetchProjects = async () => { + if (!authToken) return; + + try { + setLoading(true); + const { response: projects } = await apiCall('v2/projects', authToken); + setProjects( + projects['map']((project: any) => ({ + label: project.name, + value: project.id, + })), + ); + setLoading(false); + } catch (err) { + console.error('Error fetching projects:', err); + setError('Failed to fetch projects.'); + setLoading(false); + } + }; + + if (state === 'project') { + fetchProjects(); + } + }, [state, authToken]); + + // Fetch environments + useEffect(() => { + const fetchEnvironments = async () => { + if (!authToken || !selectedProject) return; + + try { + setLoading(true); + const { response: environments } = await apiCall( + `v2/projects/${selectedProject.value}/envs`, + authToken, + ); + setEnvironments( + environments['map']((env: any) => ({ + label: env.name, + value: env.id, + })), + ); + setLoading(false); + } catch (err) { + console.error('Error fetching environments:', err); + setError('Failed to fetch environments.'); + setLoading(false); + } + }; + + if (state === 'environment') { + fetchEnvironments(); + } + }, [state, authToken, selectedProject]); + + // Fetch graph data + useEffect(() => { + const fetchData = async () => { + if (!authToken || !selectedProject || !selectedEnvironment) return; + + try { + setLoading(true); + + const resourceResponse = await apiCall( + `v2/facts/${selectedProject.value}/${selectedEnvironment.value}/resource_instances?detailed=true`, + authToken, + ); + + const resourcesData = resourceResponse.response['map']((res: any) => ({ + label: res.resource, + value: res.id, + id: res.id, + })); + + const relationsMap = new Map(); + resourceResponse.response['forEach']((resource: any) => { + const relationsData = resource.relationships || []; + relationsMap.set( + resource.id, + relationsData.map((relation: any) => ({ + label: `${relation.relation} → ${relation.object}`, + value: relation.object || 'Unknown ID', + })), + ); + }); + + const roleAssignmentsData: RoleAssignment[] = []; + for (const resource of resourcesData) { + const roleResponse = await apiCall( + `v2/facts/${selectedProject.value}/${selectedEnvironment.value}/role_assignments?resource_instance=${resource.id}`, + authToken, + ); + + roleAssignmentsData.push( + ...roleResponse.response['map']((role: any) => ({ + user: role.user || 'Unknown User', + role: role.role || 'Unknown Role', + resourceInstance: resource.id, + })), + ); + } + + const graphData = generateGraphData( + resourcesData, + relationsMap, + roleAssignmentsData, + ); + saveHTMLGraph(graphData); + setLoading(false); + } catch (err) { + console.error('Error fetching graph data:', err); + setError('Failed to fetch data. Check network or auth token.'); + setLoading(false); + } + }; + + if (state === 'graph') { + fetchData(); + } + }, [state, authToken, selectedProject, selectedEnvironment]); + + // Loading and error states + if (authLoading || loading) { + return ( + + {' '} + {authLoading ? 'Authenticating...' : 'Loading Permit Graph...'} + + ); + } + + if (authError || error) { + return {authError || error}; + } + + // State rendering + if (state === 'project' && projects.length > 0) { + return ( + <> + Select a project + { + setSelectedProject(project); + setState('environment'); + }} + /> + + ); + } + + if (state === 'environment' && environments.length > 0) { + return ( + <> + Select an environment + { + setSelectedEnvironment(environment); + setState('graph'); + }} + /> + + ); + } + + if (state === 'graph') { + return Graph generated successfully and saved as HTML!; + } + + return Initializing...; } diff --git a/source/lib/api.ts b/source/lib/api.ts index da90c4c..a82e198 100644 --- a/source/lib/api.ts +++ b/source/lib/api.ts @@ -1,7 +1,7 @@ import { PERMIT_API_URL } from '../config.js'; interface ApiResponseData { - [x: string]: any; + [x: string]: any; id?: string; name?: string; } From 5e1feb9a748def46a3d8153f0a3de96837d2e4a8 Mon Sep 17 00:00:00 2001 From: harshtech123 Date: Fri, 27 Dec 2024 20:37:18 +0530 Subject: [PATCH 4/8] fixed styling plus fonts --- source/components/HtmlGraphSaver.ts | 172 ++++++++++++++----------- source/components/generateGraphData.ts | 14 +- source/components/graphCommand.tsx | 23 +++- 3 files changed, 123 insertions(+), 86 deletions(-) diff --git a/source/components/HtmlGraphSaver.ts b/source/components/HtmlGraphSaver.ts index 8ad513c..2f5c357 100644 --- a/source/components/HtmlGraphSaver.ts +++ b/source/components/HtmlGraphSaver.ts @@ -14,34 +14,52 @@ export const saveHTMLGraph = (graphData: { nodes: any[]; edges: any[] }) => { + + @@ -54,63 +72,67 @@ export const saveHTMLGraph = (graphData: { nodes: any[]; edges: any[] }) => { const cy = cytoscape({ container: document.getElementById('cy'), elements: [...graphData.nodes, ...graphData.edges], - style: [ - { - selector: 'edge', -style: { - 'line-color': '#00ffff', - 'width': 2, - 'target-arrow-shape': 'triangle', - 'target-arrow-color': '#00ffff', - 'curve-style': 'taxi', // Correct taxi style - 'taxi-turn': 30, // Adjust turn distance for better visualization - 'taxi-direction': 'downward', // Controls edge direction - 'taxi-turn-min-distance': 20, // Ensures proper separation for multiple edges - 'label': 'data(label)', // Add labels properly - 'color': '#ffffff', - 'font-size': 12, - 'text-background-color': '#1e1e1e', - 'text-background-opacity': 0.7, - 'text-margin-y': -5, -}, - -}, - - { - selector: 'node', - style: { - 'background-color': '#2b2b2b', - 'border-color': '#00ffff', - 'border-width': 1, - 'shape': 'round-rectangle', - 'label': 'data(label)', - 'color': '#ffffff', - 'font-size': 14, - 'text-valign': 'center', - 'text-halign': 'center', - 'width': 'label', - 'height': 'label', - 'padding': 30, - }, - }, -], + style: [ + { + selector: 'edge', + style: { + 'line-color': '#ED5F00', + 'width': 5, + 'target-arrow-shape': 'triangle', + 'target-arrow-color': '#441F04', + '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': '#1e1e1e', + 'text-background-opacity': 0.8, + 'text-background-padding': 8, + 'text-margin-y': -25, + }, + }, + { + selector: 'node', + style: { + 'background-color': 'rgb(43, 20, 0)', + 'border-color': '#ffffff', + 'border-width': 8, + 'shape': 'round-rectangle', + 'label': 'data(label)', + 'color': 'hsl(0, 0%, 100%)', + '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, + }, + }, + ], layout: { - name: 'dagre', - rankDir: 'LR', // Left-to-Right layout - nodeSep: 70, // Spacing between nodes - edgeSep: 50, // Spacing between edges - rankSep: 150, // Spacing between ranks (hierarchical levels) - animate: true, // Animate the layout rendering - fit: true, // Fit graph to the viewport - padding: 20, // Padding around the graph - directed: true, // Keep edges directed - spacingFactor: 1.5, // Increase spacing between elements -}, + name: 'dagre', + rankDir: 'LR', + nodeSep: 70, + edgeSep: 50, + rankSep: 150, + animate: true, + fit: true, + padding: 20, + directed: true, + spacingFactor: 1.5, + }, }); `; + writeFileSync(outputHTMLPath, htmlTemplate, 'utf8'); console.log(`Graph saved as: ${outputHTMLPath}`); open(outputHTMLPath); diff --git a/source/components/generateGraphData.ts b/source/components/generateGraphData.ts index 59841bc..573187d 100644 --- a/source/components/generateGraphData.ts +++ b/source/components/generateGraphData.ts @@ -23,7 +23,7 @@ export const generateGraphData = ( roleAssignments: RoleAssignment[], ) => { const nodes = resources.map(resource => ({ - data: { id: resource.id, label: `Resource: ${resource.label}` }, + data: { id: resource.id, label: ` ${resource.label}` }, })); const edges: { data: { source: string; target: string; label: string } }[] = @@ -34,7 +34,7 @@ export const generateGraphData = ( relations.forEach(relation => { if (!existingNodeIds.has(relation.value)) { nodes.push({ - data: { id: relation.value, label: `Resource: ${relation.value}` }, + data: { id: relation.value, label: `${relation.value}` }, }); existingNodeIds.add(relation.value); } @@ -54,16 +54,16 @@ export const generateGraphData = ( // Add user nodes if (!existingNodeIds.has(assignment.user)) { nodes.push({ - data: { id: assignment.user, label: `User: ${assignment.user}` }, + data: { id: assignment.user, label: `${assignment.user}` }, }); existingNodeIds.add(assignment.user); } // Add role nodes - const roleNodeId = `role:${assignment.role}`; + const roleNodeId = `${assignment.role}`; if (!existingNodeIds.has(roleNodeId)) { nodes.push({ - data: { id: roleNodeId, label: `Role: ${assignment.role}` }, + data: { id: roleNodeId, label: ` ${assignment.role}` }, }); existingNodeIds.add(roleNodeId); } @@ -73,7 +73,7 @@ export const generateGraphData = ( data: { source: assignment.user, target: roleNodeId, - label: `Assigned role`, + label: `Assigned Role`, }, }); @@ -82,7 +82,7 @@ export const generateGraphData = ( data: { source: roleNodeId, target: assignment.resourceInstance, - label: `Grants access`, + label: `Grants Access`, }, }); }); diff --git a/source/components/graphCommand.tsx b/source/components/graphCommand.tsx index 89d8626..0216570 100644 --- a/source/components/graphCommand.tsx +++ b/source/components/graphCommand.tsx @@ -142,14 +142,29 @@ export default function Graph({ options }: Props) { })); const relationsMap = new Map(); + resourceResponse.response['forEach']((resource: any) => { const relationsData = resource.relationships || []; relationsMap.set( resource.id, - relationsData.map((relation: any) => ({ - label: `${relation.relation} → ${relation.object}`, - value: relation.object || 'Unknown ID', - })), + relationsData.map((relation: any) => { + // Ensure relation.object follows the "resource#role" format + let formattedObject = relation.object || 'Unknown ID'; + if (formattedObject.includes(':')) { + const [resourcePart, rolePart] = formattedObject.split(':'); + formattedObject = `${resourcePart}#${rolePart}`; + } + + // Convert relation.relation to uppercase + const relationLabel = relation.relation + ? relation.relation.toUpperCase() + : 'UNKNOWN RELATION'; + + return { + label: `IS ${relationLabel} OF`, + value: formattedObject, + }; + }), ); }); From 2b0246fd5e2a4e24d8abd5329bf0349106e61a34 Mon Sep 17 00:00:00 2001 From: harshtech123 Date: Mon, 6 Jan 2025 18:08:38 +0530 Subject: [PATCH 5/8] updates styles and themes --- source/components/HtmlGraphSaver.ts | 39 +++++++++++------------------ 1 file changed, 14 insertions(+), 25 deletions(-) diff --git a/source/components/HtmlGraphSaver.ts b/source/components/HtmlGraphSaver.ts index 2f5c357..4277032 100644 --- a/source/components/HtmlGraphSaver.ts +++ b/source/components/HtmlGraphSaver.ts @@ -28,7 +28,7 @@ export const saveHTMLGraph = (graphData: { nodes: any[]; edges: any[] }) => { } #title { - text-align: center; + text-align: center; font-size: 30px; font-weight: 600; height: 50px; @@ -41,24 +41,13 @@ export const saveHTMLGraph = (graphData: { nodes: any[]; edges: any[] }) => { } #cy { - flex: 1; - width: 100%; - background-color: #FFF1E7; - padding: 10px; - } - - #cy::before { - content: "NEVER BUILD PERMISSIONS AGAIN"; - position: absolute; - top: 50%; - left: 50%; - transform: translate(-50%, -50%); - font-size: 50px; - font-weight: 600; - color: rgba(43, 20, 0, 0.15); /* Subtle background text color */ - pointer-events: none; /* Ensure this text doesn't block interactions */ - text-align: center; - font-family: 'Manrope', Arial, sans-serif; + 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 */ } @@ -76,10 +65,10 @@ export const saveHTMLGraph = (graphData: { nodes: any[]; edges: any[] }) => { { selector: 'edge', style: { - 'line-color': '#ED5F00', + 'line-color': 'rgb(18, 165, 148)', 'width': 5, 'target-arrow-shape': 'triangle', - 'target-arrow-color': '#441F04', + 'target-arrow-color': 'rgb(18, 165, 148)', 'curve-style': 'taxi', 'taxi-turn': 30, 'taxi-direction': 'downward', @@ -89,7 +78,7 @@ export const saveHTMLGraph = (graphData: { nodes: any[]; edges: any[] }) => { 'font-size': 25, 'font-family': 'Manrope, Arial, sans-serif', 'font-weight': 500, /* Adjusted for edge labels */ - 'text-background-color': '#1e1e1e', + 'text-background-color': 'rgb(18, 165, 148)', 'text-background-opacity': 0.8, 'text-background-padding': 8, 'text-margin-y': -25, @@ -98,12 +87,12 @@ export const saveHTMLGraph = (graphData: { nodes: any[]; edges: any[] }) => { { selector: 'node', style: { - 'background-color': 'rgb(43, 20, 0)', - 'border-color': '#ffffff', + 'background-color': 'rgb(255, 255, 255)', + 'border-color': 'rgb(211, 179, 250)', 'border-width': 8, 'shape': 'round-rectangle', 'label': 'data(label)', - 'color': 'hsl(0, 0%, 100%)', + 'color': 'rgb(151, 78, 242)', 'font-size': 30, 'font-family': 'Manrope, Arial, sans-serif', 'font-weight': 700, /* Adjusted for node labels */ From 1f50d3fb0bd7d1b9f11ca4a90267f5ce0977877b Mon Sep 17 00:00:00 2001 From: harshtech123 Date: Mon, 6 Jan 2025 20:32:34 +0530 Subject: [PATCH 6/8] resolved conflit --- source/lib/api.ts | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/source/lib/api.ts b/source/lib/api.ts index 4d79109..25fa2c4 100644 --- a/source/lib/api.ts +++ b/source/lib/api.ts @@ -1,12 +1,6 @@ import { PERMIT_API_URL } from '../config.js'; -interface ApiResponseData { - [x: string]: any; - id?: string; - name?: string; -} - -type ApiResponse = { +type ApiResponse = { headers: Headers; response: T; status: number; @@ -61,4 +55,4 @@ export const apiCall = async ( error instanceof Error ? error.message : 'Unknown fetch error occurred'; } return defaultResponse; -}; +}; \ No newline at end of file From b38552484e9f1a96d0bda4f39e1895f318d41964 Mon Sep 17 00:00:00 2001 From: harshtech123 Date: Mon, 6 Jan 2025 20:40:57 +0530 Subject: [PATCH 7/8] centered edge labels . --- source/components/HtmlGraphSaver.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/source/components/HtmlGraphSaver.ts b/source/components/HtmlGraphSaver.ts index 4277032..48d64cd 100644 --- a/source/components/HtmlGraphSaver.ts +++ b/source/components/HtmlGraphSaver.ts @@ -81,7 +81,7 @@ export const saveHTMLGraph = (graphData: { nodes: any[]; edges: any[] }) => { 'text-background-color': 'rgb(18, 165, 148)', 'text-background-opacity': 0.8, 'text-background-padding': 8, - 'text-margin-y': -25, + 'text-margin-y': 0, }, }, { From d32d4919ea50ddfe2fe2e3e79fb7896a13d7f34e Mon Sep 17 00:00:00 2001 From: harshtech123 Date: Tue, 7 Jan 2025 12:26:19 +0530 Subject: [PATCH 8/8] fixed functionalities 1 Users is now in orange boxes 2 Resource instance in purple 3 Resource instances is in the following naming convention resource_type#resource_id 4. Green lines is presenting the roles and the connection between a user and resource instance (the pill shows the role assigned) 5 Green dashed lines added it show the implicit (derived) assignment of a user to a resource 6 Orange lines now presents relationships between resource instances (the pill describes the relationship) --- source/components/HtmlGraphSaver.ts | 31 +++++++++++++ source/components/generateGraphData.ts | 61 ++++++++++++++++---------- source/components/graphCommand.tsx | 2 +- 3 files changed, 70 insertions(+), 24 deletions(-) diff --git a/source/components/HtmlGraphSaver.ts b/source/components/HtmlGraphSaver.ts index 48d64cd..6739efe 100644 --- a/source/components/HtmlGraphSaver.ts +++ b/source/components/HtmlGraphSaver.ts @@ -67,6 +67,7 @@ export const saveHTMLGraph = (graphData: { nodes: any[]; edges: any[] }) => { 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', @@ -83,6 +84,21 @@ export const saveHTMLGraph = (graphData: { nodes: any[]; edges: any[] }) => { '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', @@ -102,6 +118,21 @@ export const saveHTMLGraph = (graphData: { nodes: any[]; edges: any[] }) => { '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: { diff --git a/source/components/generateGraphData.ts b/source/components/generateGraphData.ts index 573187d..af30fee 100644 --- a/source/components/generateGraphData.ts +++ b/source/components/generateGraphData.ts @@ -22,12 +22,16 @@ export const generateGraphData = ( relationships: Map, roleAssignments: RoleAssignment[], ) => { - const nodes = resources.map(resource => ({ - data: { id: resource.id, label: ` ${resource.label}` }, - })); + 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 } }[] = - []; + 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) => { @@ -45,47 +49,58 @@ export const generateGraphData = ( target: relation.value, label: relation.label, }, + classes: 'relationship-connection', // Class for orange lines }); }); }); - // Add role assignments to the graph + // add role assignments to the graph roleAssignments.forEach(assignment => { - // Add user nodes + // 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); } - // Add role nodes - const roleNodeId = `${assignment.role}`; - if (!existingNodeIds.has(roleNodeId)) { - nodes.push({ - data: { id: roleNodeId, label: ` ${assignment.role}` }, - }); - existingNodeIds.add(roleNodeId); - } - - // Connect user to role + // Connect user to resource instance edges.push({ data: { source: assignment.user, - target: roleNodeId, - label: `Assigned Role`, + target: assignment.resourceInstance, + label: `${assignment.role}`, }, }); + }); - // Connect role to resource instance + // 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: roleNodeId, + source: assignment.user, target: assignment.resourceInstance, - label: `Grants Access`, + label: 'DERIVES', // Label for dashed green lines }, + classes: 'implicit-role-connection', // Class for styling dashed green lines }); }); - return { nodes, edges }; }; diff --git a/source/components/graphCommand.tsx b/source/components/graphCommand.tsx index 0216570..fe48792 100644 --- a/source/components/graphCommand.tsx +++ b/source/components/graphCommand.tsx @@ -136,7 +136,7 @@ export default function Graph({ options }: Props) { ); const resourcesData = resourceResponse.response['map']((res: any) => ({ - label: res.resource, + label: `${res.resource}#${res.resource_id}`, value: res.id, id: res.id, }));