gns3-server/docs/gns3-copilot/todo/acl-web-ui-implementation-guide.md
YueGuobin 13a032ea2c chore: update author name and copyright headers
Updated the author name and copyright statements across the
gns3_copilot module. The name has been standardized from
"Guobin Yue" to "Yue Guobin (岳国宾)" to reflect the correct
author attribution including Chinese characters.
2026-03-09 11:46:28 +08:00

1236 lines
31 KiB
Markdown

# 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
```
<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
```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
<ACESkeleton />
// Or progress bar
<LinearProgress />
```
#### Error Messages
```javascript
// 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
```javascript
// 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
```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 (
<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
- [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)