Add comprehensive implementation guide for ACL management feature in GNS3 Web UI. The guide includes: - Feature overview with core functionality and user flow - Detailed API endpoint specifications for ACE CRUD operations - Data structures and relationships - Frontend implementation guide with component architecture - UI/UX design recommendations and wireframes - Common usage scenarios and error handling patterns - Example code with React components and API service layer This document serves as a reference for frontend developers implementing ACL management interface, covering everything from API integration to user interface design.
30 KiB
ACL Web UI Implementation Guide
Document Status: Design Phase Priority: High Created: 2026-03-06 Target Audience: Frontend Developers
Table of Contents
- Feature Overview
- API Endpoints Details
- Data Structures
- Frontend Implementation Guide
- UI/UX Design Recommendations
- Common Scenarios
- Error Handling
- Example Code
Feature Overview
Objective
Implement ACL (Access Control List) management functionality in the GNS3 Web UI, allowing administrators to manage user permissions through a graphical interface.
Core Features
- View ACL List - Display all ACEs (Access Control Entries)
- Create ACE - Add new access control entries
- Edit ACE - Modify existing ACE properties
- Delete ACE - Remove unwanted access control entries
- Get Available Endpoints - Retrieve list of resources that can be configured with ACL
User Flow
Administrator Login
↓
Navigate to "Permission Management" page
↓
View existing ACL list
↓
Select action:
- Create new ACE → Fill form → Submit
- Edit existing ACE → Modify form → Save
- Delete ACE → Confirm → Delete
API Endpoints Details
Base URL
http://localhost:3080/v3/access/aces
1. Get All ACEs
Endpoint: GET /v3/access/aces
Privilege: ACE.Audit
Request Example:
const response = await fetch('/v3/access/aces', {
headers: {
'Authorization': `Bearer ${token}`
}
});
const aces = await response.json();
Response Example:
[
{
"ace_id": "550e8400-e29b-41d4-a716-446655440000",
"path": "/projects",
"user_id": "650e8400-e29b-41d4-a716-446655440001",
"group_id": null,
"role_id": "750e8400-e29b-41d4-a716-446655440002",
"allowed": true,
"propagate": true,
"ace_type": "user",
"created_at": "2026-03-06T10:00:00Z",
"updated_at": "2026-03-06T10:00:00Z"
}
]
2. Get Single ACE
Endpoint: GET /v3/access/aces/{ace_id}
Privilege: ACE.Audit
Request Example:
const response = await fetch(`/v3/access/aces/${aceId}`, {
headers: {
'Authorization': `Bearer ${token}`
}
});
const ace = await response.json();
3. Create ACE
Endpoint: POST /v3/access/aces
Privilege: ACE.Allocate
Request Example:
const newAce = {
path: "/projects",
user_id: "550e8400-e29b-41d4-a716-446655440000",
role_id: "750e8400-e29b-41d4-a716-446655440002",
allowed: true,
propagate: true
};
const response = await fetch('/v3/access/aces', {
method: 'POST',
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json'
},
body: JSON.stringify(newAce)
});
const createdAce = await response.json();
Response: 201 Created + Created ACE object
4. Update ACE ⭐ Core Feature
Endpoint: PUT /v3/access/aces/{ace_id}
Privilege: ACE.Modify
Request Example:
const updatedAce = {
path: "/projects/updated",
allowed: false,
propagate: false
};
const response = await fetch(`/v3/access/aces/${aceId}`, {
method: 'PUT',
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json'
},
body: JSON.stringify(updatedAce)
});
const updated = await response.json();
Important:
- Supports partial updates - only submit fields to modify
user_idandgroup_idcannot both be present (choose one)pathmust match an existing API endpoint
5. Delete ACE
Endpoint: DELETE /v3/access/aces/{ace_id}
Privilege: ACE.Allocate
Request Example:
const response = await fetch(`/v3/access/aces/${aceId}`, {
method: 'DELETE',
headers: {
'Authorization': `Bearer ${token}`
}
});
// Response: 204 No Content
6. Get Available Endpoints (for Path Selection)
Endpoint: GET /v3/access/aces/endpoints
Privilege: ACE.Audit
Request Example:
const response = await fetch('/v3/access/aces/endpoints', {
headers: {
'Authorization': `Bearer ${token}`
}
});
const endpoints = await response.json();
Response Example:
[
{
"endpoint": "/",
"name": "All endpoints",
"endpoint_type": "root"
},
{
"endpoint": "/projects",
"name": "All projects",
"endpoint_type": "project"
},
{
"endpoint": "/projects/123e4567-e89b-12d3-a456-426614174000",
"name": "Project \"My Project\"",
"endpoint_type": "project"
},
{
"endpoint": "/projects/123e4567-e89b-12d3-a456-426614174000/nodes",
"name": "All nodes in project \"My Project\"",
"endpoint_type": "node"
},
{
"endpoint": "/pools",
"name": "All resource pools",
"endpoint_type": "pool"
}
]
7. Get Related Data
Get Users List
Endpoint: GET /v3/access/users
Privilege: User.Audit
const response = await fetch('/v3/access/users', {
headers: { 'Authorization': `Bearer ${token}` }
});
const users = await response.json();
Get User Groups List
Endpoint: GET /v3/access/groups
Privilege: Group.Audit
const response = await fetch('/v3/access/groups', {
headers: { 'Authorization': `Bearer ${token}` }
});
const groups = await response.json();
Get Roles List
Endpoint: GET /v3/access/roles
Privilege: Role.Audit
const response = await fetch('/v3/access/roles', {
headers: { 'Authorization': `Bearer ${token}` }
});
const roles = await response.json();
Data Structures
ACE Object
interface ACE {
ace_id: string; // UUID
path: string; // Resource path, e.g., "/projects"
user_id?: string; // User ID (choose one with group_id)
group_id?: string; // User group ID (choose one with user_id)
role_id: string; // Role ID
allowed: boolean; // Whether allowed (default true)
propagate: boolean; // Whether to propagate to child paths (default true)
ace_type: "user" | "group"; // "user" or "group"
created_at: string; // ISO 8601 timestamp
updated_at: string; // ISO 8601 timestamp
}
ACECreate (Creating ACE)
interface ACECreate {
path: string;
user_id?: string;
group_id?: string;
role_id: string;
allowed?: boolean;
propagate?: boolean;
}
ACEUpdate (Updating ACE)
interface ACEUpdate {
path?: string;
user_id?: string;
group_id?: string;
role_id?: string;
allowed?: boolean;
propagate?: boolean;
}
Endpoint Object
interface Endpoint {
endpoint: string; // Endpoint path
name: string; // Display name
endpoint_type: string; // "root" | "project" | "node" | "link" | "user" | "group" | "role" | "pool"
}
Frontend Implementation Guide
Recommended Tech Stack
- Framework: React / Vue / Angular
- State Management: Redux / Vuex / NgRx
- UI Components: Material-UI / Ant Design / Element Plus
- HTTP Client: Axios / Fetch API
Core Component Design
1. ACE List Component
<ACLList />
├── <ACLFilter /> # Filter
├── <ACETable /> # ACE table
│ ├── <ACERow />
│ ├── <ACERow />
│ └── ...
└── <ACLCreateButton /> # Create button
2. ACE Form Component (Create/Edit)
<ACEForm mode="create" /> # or mode="edit"
├── <PathSelector /> # Path selector
├── <UserGroupSelector /> # User/group selector
├── <RoleSelector /> # Role selector
├── <AllowedToggle /> # Allow/deny toggle
├── <PropagateToggle /> # Propagate switch
└── <SubmitButton />
3. Endpoint Selector Component
<PathSelector />
├── <EndpointTree /> # Tree structure showing endpoints
│ ├── <ProjectNode />
│ ├── <NodeNode />
│ └── ...
└── <EndpointSearch /> # Search box
State Management
// Redux example
interface ACLState {
aces: ACE[];
endpoints: Endpoint[];
users: User[];
groups: Group[];
roles: Role[];
loading: boolean;
error: string | null;
}
// Actions
const loadACEs = () => async (dispatch) => {
dispatch({ type: 'ACL_LOAD_START' });
try {
const aces = await api.getACEs();
dispatch({ type: 'ACL_LOAD_SUCCESS', payload: aces });
} catch (error) {
dispatch({ type: 'ACL_LOAD_ERROR', payload: error.message });
}
};
const createACE = (ace: ACECreate) => async (dispatch) => {
const newAce = await api.createACE(ace);
dispatch({ type: 'ACL_CREATE_SUCCESS', payload: newAce });
};
const updateACE = (aceId: string, updates: ACEUpdate) => async (dispatch) => {
const updated = await api.updateACE(aceId, updates);
dispatch({ type: 'ACL_UPDATE_SUCCESS', payload: updated });
};
const deleteACE = (aceId: string) => async (dispatch) => {
await api.deleteACE(aceId);
dispatch({ type: 'ACL_DELETE_SUCCESS', payload: aceId });
};
UI/UX Design Recommendations
1. List Page Layout
┌─────────────────────────────────────────────────────────┐
│ Permission Management [New ACE] │
├─────────────────────────────────────────────────────────┤
│ Filters: [User/Group] [Role] [Path Type] [Status] │
├─────────────────────────────────────────────────────────┤
│ ┌───────────────────────────────────────────────────┐ │
│ │ Path │ User/Group │ Role │ Allow │ Action│ │
│ ├───────────────────────────────────────────────────┤ │
│ │ /projects │ user1 │ User │ ✓ │ Edit │ │
│ │ /projects/123 │ group1 │ Auditor│ ✓ │ Edit │ │
│ │ /pools/456 │ user2 │ User │ ✗ │ Edit │ │
│ └───────────────────────────────────────────────────┘ │
│ [Load More] │
└─────────────────────────────────────────────────────────┘
2. Create/Edit Form
┌────────────────────────────────────────────┐
│ New Access Control Entry (ACE) [Cancel]│
├────────────────────────────────────────────┤
│ │
│ Resource Path * │
│ ┌──────────────────────────────────────┐ │
│ │ ▼ Select Endpoint [Search]│ │
│ └──────────────────────────────────────┘ │
│ Hint: Select the resource path to control │
│ │
│ Apply To * │
│ ○ User □ User Group │
│ ┌──────────────────────────────────────┐ │
│ │ ▼ Select User │ │
│ └──────────────────────────────────────┘ │
│ │
│ Role * │
│ ┌──────────────────────────────────────┐ │
│ │ User ▼ │ │
│ └──────────────────────────────────────┘ │
│ Note: This role includes Project.Audit │
│ and other privileges │
│ │
│ Permission Settings │
│ ○ Allow Access ● Deny Access │
│ │
│ □ Propagate to Child Paths │
│ Note: When checked, permission applies │
│ to all child resources │
│ │
│ [Cancel] [Save] │
└────────────────────────────────────────────┘
3. Endpoint Selector (Tree Structure)
┌────────────────────────────────────────────┐
│ Select Resource Path [Search] │
├────────────────────────────────────────────┤
│ 📁 / (All endpoints) │
│ 📁 /projects (All projects) │
│ 📁 My Project │
│ 📁 /nodes (All nodes) │
│ 📄 Router1 │
│ 📄 Switch1 │
│ 📁 /links (All links) │
│ 📁 /pools (All resource pools) │
│ 📄 Production Pool │
│ 📁 /templates (All templates) │
└────────────────────────────────────────────┘
4. Interaction Design Points
Loading States
// Show skeleton screen
<ACESkeleton />
// Or progress bar
<LinearProgress />
Error Messages
// Form validation error
<FormHelperText error={true}>
Path cannot be empty
</FormHelperText>
// API error
<Snackbar
open={true}
message="Creation failed: Path does not match any endpoint"
severity="error"
/>
Confirmation Dialog
// Delete confirmation
<Dialog>
<DialogTitle>Confirm Delete</DialogTitle>
<DialogContent>
Are you sure you want to delete this ACE? This action cannot be undone.
</DialogContent>
<DialogActions>
<Button onClick={cancel}>Cancel</Button>
<Button onClick={confirm} color="error">Delete</Button>
</DialogActions>
</Dialog>
Common Scenarios
Scenario 1: Create User-Level ACE
async function createUserACE() {
const newAce = {
path: "/projects",
user_id: selectedUserId,
role_id: userRoleId,
allowed: true,
propagate: true
};
try {
const ace = await api.createACE(newAce);
showSuccess('ACE created successfully');
loadACEList(); // Refresh list
} catch (error) {
showError(`Creation failed: ${error.message}`);
}
}
Scenario 2: Create Group-Level ACE
async function createGroupACE() {
const newAce = {
path: "/templates",
group_id: usersGroupId,
role_id: userRoleId,
allowed: true,
propagate: false
};
try {
const ace = await api.createACE(newAce);
showSuccess('Group ACE created successfully');
loadACEList();
} catch (error) {
showError(`Creation failed: ${error.message}`);
}
}
Scenario 3: Edit Existing ACE
async function editACE(aceId, updates) {
try {
const updated = await api.updateACE(aceId, updates);
showSuccess('ACE updated successfully');
loadACEList();
} catch (error) {
showError(`Update failed: ${error.message}`);
}
}
// Example: Toggle allowed status
async function toggleAllowed(ace) {
await editACE(ace.ace_id, {
allowed: !ace.allowed
});
}
// Example: Change path
async function changePath(aceId, newPath) {
await editACE(aceId, {
path: newPath
});
}
Scenario 4: Delete ACE
async function deleteACE(aceId) {
if (!confirm('Are you sure you want to delete this ACE?')) {
return;
}
try {
await api.deleteACE(aceId);
showSuccess('ACE deleted successfully');
loadACEList();
} catch (error) {
showError(`Deletion failed: ${error.message}`);
}
}
Scenario 5: Batch Operations
// Batch delete
async function batchDelete(aceIds) {
const promises = aceIds.map(id => api.deleteACE(id));
await Promise.all(promises);
showSuccess(`Deleted ${aceIds.length} ACEs`);
loadACEList();
}
// Batch toggle allowed status
async function batchToggleAllowed(aceIds, allowed) {
const promises = aceIds.map(id =>
api.updateACE(id, { allowed })
);
await Promise.all(promises);
showSuccess(`Updated ${aceIds.length} ACEs`);
loadACEList();
}
Error Handling
Common Error Types
| HTTP Status | Error Description | Handling |
|---|---|---|
| 400 Bad Request | Path doesn't match any endpoint | Prompt user to select valid path |
| 400 Bad Request | Both user_id and group_id present | Prompt user to choose one |
| 403 Forbidden | Insufficient privileges | Prompt user to contact administrator |
| 404 Not Found | ACE doesn't exist | Refresh list, notify resource deleted |
| 422 Unprocessable Entity | Validation failed | Display specific field errors |
Error Handling Example
async function handleAPICall(apiFunction) {
try {
const result = await apiFunction();
return { success: true, data: result };
} catch (error) {
if (error.response) {
const { status, data } = error.response;
switch (status) {
case 400:
return {
success: false,
error: data.detail || 'Invalid request parameters'
};
case 403:
return {
success: false,
error: 'Insufficient privileges, please contact administrator'
};
case 404:
return {
success: false,
error: 'Resource not found, may have been deleted'
};
case 422:
return {
success: false,
error: formatValidationError(data.detail)
};
default:
return {
success: false,
error: 'Unknown error, please try again later'
};
}
}
return {
success: false,
error: 'Network error, please check connection'
};
}
}
function formatValidationError(errors) {
// Format validation error messages
return errors.map(err => err.msg).join(', ');
}
Example Code
Complete React Component Example
import React, { useState, useEffect } from 'react';
import {
Table,
Button,
Dialog,
DialogTitle,
DialogContent,
DialogActions,
Select,
MenuItem,
Switch,
FormControlLabel,
TextField,
IconButton
} from '@mui/material';
import { Edit, Delete, Add } from '@mui/icons-material';
// API Client
const api = {
async getACEs(token) {
const res = await fetch('/v3/access/aces', {
headers: { 'Authorization': `Bearer ${token}` }
});
if (!res.ok) throw new Error(res.statusText);
return res.json();
},
async getEndpoints(token) {
const res = await fetch('/v3/access/aces/endpoints', {
headers: { 'Authorization': `Bearer ${token}` }
});
if (!res.ok) throw new Error(res.statusText);
return res.json();
},
async getUsers(token) {
const res = await fetch('/v3/access/users', {
headers: { 'Authorization': `Bearer ${token}` }
});
if (!res.ok) throw new Error(res.statusText);
return res.json();
},
async getGroups(token) {
const res = await fetch('/v3/access/groups', {
headers: { 'Authorization': `Bearer ${token}` }
});
if (!res.ok) throw new Error(res.statusText);
return res.json();
},
async getRoles(token) {
const res = await fetch('/v3/access/roles', {
headers: { 'Authorization': `Bearer ${token}` }
});
if (!res.ok) throw new Error(res.statusText);
return res.json();
},
async createACE(token, data) {
const res = await fetch('/v3/access/aces', {
method: 'POST',
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json'
},
body: JSON.stringify(data)
});
if (!res.ok) {
const error = await res.json();
throw new Error(error.detail || 'Creation failed');
}
return res.json();
},
async updateACE(token, aceId, data) {
const res = await fetch(`/v3/access/aces/${aceId}`, {
method: 'PUT',
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json'
},
body: JSON.stringify(data)
});
if (!res.ok) {
const error = await res.json();
throw new Error(error.detail || 'Update failed');
}
return res.json();
},
async deleteACE(token, aceId) {
const res = await fetch(`/v3/access/aces/${aceId}`, {
method: 'DELETE',
headers: { 'Authorization': `Bearer ${token}` }
});
if (!res.ok) throw new Error('Deletion failed');
}
};
// ACE Form Component
function ACEForm({ token, ace, onSave, onCancel }) {
const [endpoints, setEndpoints] = useState([]);
const [users, setUsers] = useState([]);
const [groups, setGroups] = useState([]);
const [roles, setRoles] = useState([]);
const [formData, setFormData] = useState({
path: ace?.path || '',
user_id: ace?.user_id || '',
group_id: ace?.group_id || '',
role_id: ace?.role_id || '',
allowed: ace?.allowed ?? true,
propagate: ace?.propagate ?? true,
ace_type: ace?.ace_type || 'user'
});
useEffect(() => {
Promise.all([
api.getEndpoints(token),
api.getUsers(token),
api.getGroups(token),
api.getRoles(token)
]).then(([endpoints, users, groups, roles]) => {
setEndpoints(endpoints);
setUsers(users);
setGroups(groups);
setRoles(roles);
});
}, [token]);
const handleSubmit = async (e) => {
e.preventDefault();
const data = {
...formData,
[formData.ace_type === 'user' ? 'user_id' : 'group_id']:
formData.ace_type === 'user' ? formData.user_id : formData.group_id
};
delete data.ace_type;
delete data.user_id;
delete data.group_id;
if (formData.ace_type === 'user') {
data.user_id = formData.user_id;
} else {
data.group_id = formData.group_id;
}
try {
if (ace) {
await api.updateACE(token, ace.ace_id, data);
} else {
await api.createACE(token, data);
}
onSave();
} catch (error) {
alert(error.message);
}
};
return (
<form onSubmit={handleSubmit}>
<DialogContent>
{/* Path Selection */}
<TextField
select
label="Resource Path"
value={formData.path}
onChange={(e) => setFormData({ ...formData, path: e.target.value })}
fullWidth
margin="normal"
required
>
{endpoints.map((ep) => (
<MenuItem key={ep.endpoint} value={ep.endpoint}>
{ep.name}
</MenuItem>
))}
</TextField>
{/* User/Group Selection */}
<Select
value={formData.ace_type}
onChange={(e) => setFormData({ ...formData, ace_type: e.target.value })}
fullWidth
>
<MenuItem value="user">User</MenuItem>
<MenuItem value="group">User Group</MenuItem>
</Select>
{formData.ace_type === 'user' ? (
<TextField
select
label="User"
value={formData.user_id}
onChange={(e) => setFormData({ ...formData, user_id: e.target.value })}
fullWidth
margin="normal"
required
>
{users.map((user) => (
<MenuItem key={user.user_id} value={user.user_id}>
{user.username}
</MenuItem>
))}
</TextField>
) : (
<TextField
select
label="User Group"
value={formData.group_id}
onChange={(e) => setFormData({ ...formData, group_id: e.target.value })}
fullWidth
margin="normal"
required
>
{groups.map((group) => (
<MenuItem key={group.user_group_id} value={group.user_group_id}>
{group.name}
</MenuItem>
))}
</TextField>
)}
{/* Role Selection */}
<TextField
select
label="Role"
value={formData.role_id}
onChange={(e) => setFormData({ ...formData, role_id: e.target.value })}
fullWidth
margin="normal"
required
>
{roles.map((role) => (
<MenuItem key={role.role_id} value={role.role_id}>
{role.name}
</MenuItem>
))}
</TextField>
{/* Allow/Deny */}
<FormControlLabel
control={
<Switch
checked={formData.allowed}
onChange={(e) => setFormData({ ...formData, allowed: e.target.checked })}
/>
}
label={formData.allowed ? 'Allow Access' : 'Deny Access'}
/>
{/* Propagate */}
<FormControlLabel
control={
<Switch
checked={formData.propagate}
onChange={(e) => setFormData({ ...formData, propagate: e.target.checked })}
/>
}
label="Propagate to Child Paths"
/>
</DialogContent>
<DialogActions>
<Button onClick={onCancel}>Cancel</Button>
<Button type="submit" variant="contained">
{ace ? 'Save' : 'Create'}
</Button>
</DialogActions>
</form>
);
}
// ACE List Component
export default function ACLManagement({ token }) {
const [aces, setAces] = useState([]);
const [loading, setLoading] = useState(false);
const [dialogOpen, setDialogOpen] = useState(false);
const [editingACE, setEditingACE] = useState(null);
const loadACEs = async () => {
setLoading(true);
try {
const data = await api.getACEs(token);
setAces(data);
} catch (error) {
alert(`Loading failed: ${error.message}`);
} finally {
setLoading(false);
}
};
useEffect(() => {
loadACEs();
}, [token]);
const handleCreate = () => {
setEditingACE(null);
setDialogOpen(true);
};
const handleEdit = (ace) => {
setEditingACE(ace);
setDialogOpen(true);
};
const handleDelete = async (aceId) => {
if (!confirm('Are you sure you want to delete this ACE?')) return;
try {
await api.deleteACE(token, aceId);
loadACEs();
} catch (error) {
alert(`Deletion failed: ${error.message}`);
}
};
const handleSave = () => {
setDialogOpen(false);
loadACEs();
};
return (
<div>
<div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: 16 }}>
<h2>Access Control List (ACL)</h2>
<Button
variant="contained"
startIcon={<Add />}
onClick={handleCreate}
>
New ACE
</Button>
</div>
{loading ? (
<p>Loading...</p>
) : (
<Table>
<thead>
<tr>
<th>Path</th>
<th>User/Group</th>
<th>Role</th>
<th>Allowed</th>
<th>Propagate</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
{aces.map((ace) => (
<tr key={ace.ace_id}>
<td>{ace.path}</td>
<td>{ace.ace_type === 'user' ? ace.user_id : ace.group_id}</td>
<td>{ace.role_id}</td>
<td>{ace.allowed ? '✓' : '✗'}</td>
<td>{ace.propagate ? '✓' : '✗'}</td>
<td>
<IconButton onClick={() => handleEdit(ace)}>
<Edit />
</IconButton>
<IconButton onClick={() => handleDelete(ace.ace_id)}>
<Delete />
</IconButton>
</td>
</tr>
))}
</tbody>
</Table>
)}
<Dialog open={dialogOpen} onClose={() => setDialogOpen(false)} maxWidth="md" fullWidth>
<DialogTitle>{editingACE ? 'Edit ACE' : 'New ACE'}</DialogTitle>
<ACEForm
token={token}
ace={editingACE}
onSave={handleSave}
onCancel={() => setDialogOpen(false)}
/>
</Dialog>
</div>
);
}
Implementation Checklist
Basic Features
- Fetch and display ACE list
- Create new ACE
- Edit existing ACE
- Delete ACE
- Fetch and display available endpoints
Form Features
- Path selector (dropdown/tree)
- User/group selector
- Role selector
- Allow/deny toggle
- Propagate switch
- Form validation
User Experience
- Loading state display
- Error notifications
- Success notifications
- Delete confirmation dialog
- List filtering/search
Advanced Features
- Batch operations
- ACE export/import
- Permission preview
- Change history
References
Related Documentation
API Specifications
- OpenAPI/Swagger:
/static/swagger-ui-bundle.js - ReDoc:
/static/redoc.standalone.js
Test Data
// Example ACEs for testing
const exampleACEs = [
{
path: "/projects",
ace_type: "user",
user_id: "admin-user-id",
role_id: "user-role-id",
allowed: true,
propagate: true
},
{
path: "/projects/sensitive",
ace_type: "group",
group_id: "auditors-group-id",
role_id: "auditor-role-id",
allowed: false,
propagate: false
}
];
Document Version: 1.0 Last Updated: 2026-03-06