# ACL Web UI Implementation Guide **Document Status**: Design Phase **Priority**: High **Created**: 2026-03-06 **Target Audience**: Frontend Developers --- ## Table of Contents - [Feature Overview](#feature-overview) - [API Endpoints Details](#api-endpoints-details) - [Data Structures](#data-structures) - [Frontend Implementation Guide](#frontend-implementation-guide) - [UI/UX Design Recommendations](#uiux-design-recommendations) - [Common Scenarios](#common-scenarios) - [Error Handling](#error-handling) - [Example Code](#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 1. **View ACL List** - Display all ACEs (Access Control Entries) 2. **Create ACE** - Add new access control entries 3. **Edit ACE** - Modify existing ACE properties 4. **Delete ACE** - Remove unwanted access control entries 5. **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**: ```javascript const response = await fetch('/v3/access/aces', { headers: { 'Authorization': `Bearer ${token}` } }); const aces = await response.json(); ``` **Response Example**: ```json [ { "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**: ```javascript 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**: ```javascript 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**: ```javascript 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_id` and `group_id` cannot both be present (choose one) - `path` must match an existing API endpoint --- ### 5. Delete ACE **Endpoint**: `DELETE /v3/access/aces/{ace_id}` **Privilege**: `ACE.Allocate` **Request Example**: ```javascript 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**: ```javascript const response = await fetch('/v3/access/aces/endpoints', { headers: { 'Authorization': `Bearer ${token}` } }); const endpoints = await response.json(); ``` **Response Example**: ```json [ { "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` ```javascript 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` ```javascript 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` ```javascript const response = await fetch('/v3/access/roles', { headers: { 'Authorization': `Bearer ${token}` } }); const roles = await response.json(); ``` --- ## Data Structures ### ACE Object ```typescript 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) ```typescript interface ACECreate { path: string; user_id?: string; group_id?: string; role_id: string; allowed?: boolean; propagate?: boolean; } ``` ### ACEUpdate (Updating ACE) ```typescript interface ACEUpdate { path?: string; user_id?: string; group_id?: string; role_id?: string; allowed?: boolean; propagate?: boolean; } ``` ### Endpoint Object ```typescript 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 ``` ├── # Filter ├── # ACE table │ ├── │ ├── │ └── ... └── # Create button ``` #### 2. ACE Form Component (Create/Edit) ``` # or mode="edit" ├── # Path selector ├── # User/group selector ├── # Role selector ├── # Allow/deny toggle ├── # Propagate switch └── ``` #### 3. Endpoint Selector Component ``` ├── # Tree structure showing endpoints │ ├── │ ├── │ └── ... └── # Search box ``` --- ### State Management ```typescript // 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 ```javascript // Show skeleton screen // Or progress bar ``` #### Error Messages ```javascript // Form validation error Path cannot be empty // API error ``` #### Confirmation Dialog ```javascript // Delete confirmation Confirm Delete Are you sure you want to delete this ACE? This action cannot be undone. ``` --- ## Common Scenarios ### Scenario 1: Create User-Level ACE ```javascript 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 ```javascript 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 ```javascript 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 ```javascript 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 ```javascript // 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 ```javascript 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 ```jsx 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 (
{/* Path Selection */} setFormData({ ...formData, path: e.target.value })} fullWidth margin="normal" required > {endpoints.map((ep) => ( {ep.name} ))} {/* User/Group Selection */} {formData.ace_type === 'user' ? ( setFormData({ ...formData, user_id: e.target.value })} fullWidth margin="normal" required > {users.map((user) => ( {user.username} ))} ) : ( setFormData({ ...formData, group_id: e.target.value })} fullWidth margin="normal" required > {groups.map((group) => ( {group.name} ))} )} {/* Role Selection */} setFormData({ ...formData, role_id: e.target.value })} fullWidth margin="normal" required > {roles.map((role) => ( {role.name} ))} {/* Allow/Deny */} setFormData({ ...formData, allowed: e.target.checked })} /> } label={formData.allowed ? 'Allow Access' : 'Deny Access'} /> {/* Propagate */} setFormData({ ...formData, propagate: e.target.checked })} /> } label="Propagate to Child Paths" />
); } // 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 (

Access Control List (ACL)

{loading ? (

Loading...

) : ( {aces.map((ace) => ( ))}
Path User/Group Role Allowed Propagate Actions
{ace.path} {ace.ace_type === 'user' ? ace.user_id : ace.group_id} {ace.role_id} {ace.allowed ? '✓' : '✗'} {ace.propagate ? '✓' : '✗'} handleEdit(ace)}> handleDelete(ace.ace_id)}>
)} setDialogOpen(false)} maxWidth="md" fullWidth> {editingACE ? 'Edit ACE' : 'New ACE'} setDialogOpen(false)} />
); } ``` --- ## 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 - [RBAC + ACL Permission System Implementation Guide](../gns3-server/rbac-acl-权限系统实现说明.md) - [RBAC + ACL Implementation Guide](../gns3-server/rbac-acl-implementation-guide.md) ### API Specifications - OpenAPI/Swagger: `/static/swagger-ui-bundle.js` - ReDoc: `/static/redoc.standalone.js` ### Test Data ```javascript // 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 --- ## License **Copyright © 2025 Yue Guobin (岳国宾)** This work is licensed under the [Creative Commons Attribution-ShareAlike 4.0 International License (CC BY-SA 4.0)](https://creativecommons.org/licenses/by-sa/4.0/). ![CC BY-SA 4.0](https://i.creativecommons.org/l/by-sa/4.0/88x31.png) ### Summary You are free to: - **Share** — Copy and redistribute the material in any medium or format - **Adapt** — Remix, transform, and build upon the material for any purpose Under the following terms: - **Attribution** — You must give appropriate credit to **Yue Guobin (岳国宾)**, provide a link to the license, and indicate if changes were made. - **ShareAlike** — If you remix, transform, or build upon the material, you must distribute your contributions under the **same license** (CC BY-SA 4.0). Full license text: [DESIGN_DOCS_LICENSE](../DESIGN_DOCS_LICENSE.md)