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.
31 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
License
Copyright © 2025 Yue Guobin (岳国宾)
This work is licensed under the Creative Commons Attribution-ShareAlike 4.0 International License (CC BY-SA 4.0).
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
