23 KiB
RBAC User Isolation Roadmap
Overview
GNS3 3.0 ships with a complete RBAC framework (ACE + Role + Privilege models), but the user isolation layer is incomplete. Users can see resources they should not have access to because:
- Resource creation does not auto-grant the creator an ACE
- List endpoints return unfiltered results
- Two GET endpoints have RBAC checks bypassed via FIXME
This document has been updated to reflect the implemented solution in feature/simple-user-isolation branch.
Current State
| Resource | Route Check | List Filtering | Auto-ACE on Create | ACE Cleanup on Delete |
|---|---|---|---|---|
| Project | Project.Audit/Modify/Allocate |
Implemented — Three-step filtering | Not needed | Done |
| Template | Template.Audit FIXME |
None — all templates returned | Missing | Done |
| Node | Node.Audit/Modify/Allocate |
N/A (inherits project) | Inherits project | N/A |
| Link | Link.Audit/Modify/Allocate |
N/A (inherits project) | Inherits project | N/A |
| Drawing | Drawing.Audit/Modify/Allocate |
N/A (inherits project) | Inherits project | N/A |
| Snapshot | Snapshot.Audit/Allocate/Restore |
N/A (inherits project) | Inherits project | N/A |
| Image | Image.Audit/Allocate |
None — all images returned | Missing | Missing |
| Compute | Compute.Audit FIXME |
None — all computes returned | N/A (shared infra) | Done |
| Appliance | Appliance.Audit/Allocate |
None — all appliances returned | N/A (builtin) | N/A |
| Symbol | Symbol.Audit/Allocate |
None — all symbols returned | N/A (builtin) | N/A |
Implemented Solution: Project Isolation
Three-Step Permission Check Logic
Status: ✅ Implemented in feature/simple-user-isolation branch
# Step 1: ACE check - basic access permission
# Get projects user has ACE for
ace_projects = []
for project in controller.projects.values():
project_path = f"/projects/{project.id}"
if await rbac_repo.check_user_has_privilege(current_user.user_id, project_path, "Project.Audit"):
ace_projects.append(project)
# Step 2: Filter ace_projects by created_by - user's own projects
# Project sharing is only available through resource pools
user_projects = [p.asdict() for p in ace_projects if p.created_by == current_user.username]
projects.extend(user_projects)
# Step 3: Resource pool projects
# Projects shared through resource pools
user_pool_resources = await rbac_repo.get_user_pool_resources(current_user.user_id, "Project.Audit")
project_ids_in_pools = [str(r.resource_id) for r in user_pool_resources if r.resource_type == "project"]
pool_projects = [p.asdict() for p in controller.projects.values() if p.id in project_ids_in_pools]
projects.extend(pool_projects)
Key Design Principles
- ACE for basic access control: Controls whether user can access the system
- created_by for user isolation: Controls which specific resources user can access
- Resource pools for project sharing: The only mechanism for sharing projects between users
- No direct ACE sharing: Users cannot configure ACE directly on specific projects to share them
Advantages of This Approach
- Fault tolerance: Even with broad ACE configuration (
path: "/" + propagate: true), user isolation remains effective - Clear separation: Basic access, data ownership, and team sharing are clearly separated
- Simple mechanism: No complex auto-ACE or seen_project_ids tracking required
- Performance: Leverages existing created_by field, no schema changes needed
Updated Architecture
graph TD
subgraph Client
WebUI
CLI
end
subgraph "Controller API"
Auth[get_current_active_user]
Routes[Resource Routes]
ACE_Check[Step 1: ACE Check]
Owner_Filter[Step 2: Filter by created_by]
Pool_Check[Step 3: Resource Pools]
end
subgraph "RBAC Engine"
ACE[(ACE table)]
Role[(Role table)]
Privilege[(Privilege table)]
Checker[check_user_has_privilege]
end
WebUI --> Auth
CLI --> Auth
Auth --> Routes
Routes --> ACE_Check
ACE_Check --> Checker
ACE_Check --> Owner_Filter
Owner_Filter --> Pool_Check
Pool_Check --> Checker
Checker --> ACE
Checker --> Role
Role --> Privilege
Business Process
Project listing with three-step filtering
sequenceDiagram
actor U as User
participant API as GET /projects
participant Controller as Controller
participant RBAC as RBAC Engine
U->>API: List projects
API->>API: get_current_active_user (not superadmin)
API->>RBAC: Step 1: ACE check on each project
loop Each project
RBAC-->>API: ACE results
end
API->>API: Step 2: Filter by created_by
API-->>U: User's own projects
API->>RBAC: Step 3: Resource pool projects
RBAC-->>API: Pool projects
API-->>U: Combined list (own + pool)
Updated Phased Plan
✅ Phase 1 — MVP: Project isolation (COMPLETED)
Goal: Users only see projects they created or were granted access to through resource pools.
| Task | Files | Status | Detail |
|---|---|---|---|
| Fix project list filtering | projects.py — get_projects() |
✅ Implemented | Three-step filtering: ACE check → created_by filter → resource pools |
Result:
- ✅ Users can only see projects they created
- ✅ Team collaboration through resource pools works
- ✅ Fault tolerance: Works correctly even with broad ACE configurations
- ✅ No schema changes required
- ✅ ~30 lines changed
Phase 2 — Template isolation
Goal: Users see their own templates + builtin templates only.
| Task | Files | Detail |
|---|---|---|
| Apply same pattern to templates | templates.py — get_templates() |
Use same three-step filtering as projects |
Uncomment Template.Audit |
templates.py |
Restore has_privilege("Template.Audit") checks |
Dependency: Web UI must handle 403 from GET /templates/{id}. Mitigation: keep builtin templates unconditionally visible so the UI always has data.
Phase 3 — Image isolation (optional)
Goal: Users see only images they uploaded.
| Task | Files |
|---|---|
| Apply same pattern to images | images.py — get_images() |
| Fix image list filtering | images.py |
| ACE cleanup on delete | images.py — delete_image() |
Phase 4 — Default ACE for "Users" group (optional)
Goal: Users in "Users" group can create/list resources without admin ACE intervention.
| Task | Detail |
|---|---|
Default ACE on /projects |
Grant "Users" group → User role → /projects (propagate=False) |
Default ACE on /templates |
Grant "Users" group → User role → /templates (propagate=False) |
API Endpoints Changed
Phase 1 (Implemented)
| Method | Path | Change |
|---|---|---|
GET |
/v3/projects |
Three-step filtering: ACE → created_by → resource pools |
Phase 2 (Planned)
| Method | Path | Change |
|---|---|---|
GET |
/v3/templates |
Apply same three-step filtering |
GET |
/v3/templates/{id} |
Restore Template.Audit check |
Key Design Decisions
-
Project sharing through resource pools only: Users cannot configure ACE directly to share specific projects. All sharing must go through resource pools. This prevents permission configuration chaos and maintains clear ownership semantics.
-
No auto-ACE required: The three-step filtering logic works without needing automatic ACE creation on project creation. The created_by field provides sufficient ownership information.
-
Fault-tolerant to ACE configuration: Even if administrators configure broad ACE permissions (like
path: "/" + propagate: true), user isolation remains effective because Step 2 filters by created_by. -
No DB migration required: Uses existing ACE/role/privilege tables and created_by field. No schema changes needed.
-
Performance: Project list filtering is O(n) where n is the total number of projects. Each project requires one ACE check. Acceptable for < 500 projects. Can optimize later with batch ACE queries if needed.
Updated References
- Implementation:
gns3server/api/routes/controller/projects.py(feature/simple-user-isolation branch) - Discussion: https://github.com/GNS3/gns3-server/discussions/1949
- RBAC models:
gns3server/db/models/acl.py,roles.py,privileges.py - RBAC repository:
gns3server/db/repositories/rbac.py - Auth dependency:
gns3server/api/routes/controller/dependencies/authentication.py - RBAC dependency:
gns3server/api/routes/controller/dependencies/rbac.py - Resource pools:
gns3server/db/models/pools.pyandgns3server/db/repositories/pools.py
Implementation Notes
What Was Implemented
The feature/simple-user-isolation branch implements a robust user isolation mechanism that:
- Integrates with existing RBAC framework without breaking changes
- Leverages the created_by field that already exists in the Project model
- Uses three-step pipeline filtering to avoid complex seen_project_ids tracking
- Supports team collaboration through existing resource pool functionality
- Is fault-tolerant to ACE misconfiguration - broad ACE permissions don't break user isolation
What Was Not Implemented
The original roadmap's Phase 1 included auto-ACE creation on project creation. This was determined to be unnecessary because:
- The three-step filtering logic achieves user isolation without auto-ACE
- Auto-ACE would add complexity without significant benefit
- Project sharing through resource pools is cleaner than direct ACE configuration
Future Work
The same three-step filtering pattern can be applied to:
- Templates: Replace the FIXME comment with proper filtering logic
- Images: Apply the same pattern for user image isolation
- Other resources: Extend the pattern as needed
This implementation provides a solid foundation for user isolation in GNS3 3.0+ while maintaining compatibility with the existing RBAC framework.
Phase 5 — ACE Architecture Refactoring (Future)
Goal: Improve ACE manageability by supporting multiple paths and resource pools in a single ACE entry.
Current Problem
With the current design where one ACE = one path:
- ACE explosion: 5 user groups × 10 resource pools = 50 ACE entries
- Management complexity: Difficult to maintain and understand ACE purpose
- Performance impact: Permission checking must iterate through many ACE entries
Proposed Solution
Redesign ACE structure to support multiple paths and resource pools in a single ACE entry:
{
"name": "Development Team Access",
"description": "Full access for development team",
"ace_type": "group",
"group_id": "...",
"role_id": "...",
"paths": ["/projects", "/templates", "/images"],
"resource_pools": ["pool-id-1", "pool-id-2"],
"propagate": true,
"allowed": true
}
Database Changes Required
- Add name and description to ACE table:
ALTER TABLE acl ADD COLUMN name VARCHAR;
ALTER TABLE acl ADD COLUMN description TEXT;
- Create association tables:
CREATE TABLE ace_paths (
ace_id UUID REFERENCES acl(ace_id),
path VARCHAR,
PRIMARY KEY (ace_id, path)
);
CREATE TABLE ace_pools (
ace_id UUID REFERENCES acl(ace_id),
resource_pool_id UUID REFERENCES resource_pools(resource_pool_id),
PRIMARY KEY (ace_id, resource_pool_id)
);
- Update permission checking logic to check both paths and resource_pools tables
Benefits
- ✅ Reduced ACE entries: One ACE covers multiple related paths/pools
- ✅ Better organization: Logical grouping with clear names and descriptions
- ✅ Easier management: Edit one ACE instead of multiple related entries
- ✅ Improved performance: Fewer ACE entries to check during permission validation
Implementation Considerations
- Migration path: Need to migrate existing single-path ACEs to new structure
- Backward compatibility: API should support both old and new formats during transition
- UI updates: ACE management interface needs to support multi-path/pool selection
- Permission checking: Update
check_user_has_privilegeto check association tables
Phase 6 — Frontend Permission Query API (Future)
Goal: Provide an API endpoint for the Web UI to query the current user's permissions, enabling dynamic UI rendering based on role and ACE configuration.
Problem
Currently the Web UI cannot determine what the authenticated user is allowed to see or do:
- ❌ Users see menu items and buttons they don't have permission to use
- ❌ Clicking a forbidden action results in a 403 error (unexpected UX)
- ❌ No way to hide/show UI elements based on actual permissions
Proposed Solution
Create a GET /v3/me/permissions endpoint that returns the current user's effective permissions:
{
"user_id": "uuid",
"is_superadmin": false,
"permissions": [
{"path": "/projects", "privileges": ["Project.Audit", "Project.Allocate"]},
{"path": "/projects/{id}", "privileges": ["Project.Audit", "Project.Modify"]},
{"path": "/templates", "privileges": ["Template.Audit"]}
],
"pools": [
{"path": "/pools/{id}", "name": "Team Projects", "privileges": ["Pool.Audit"]}
]
}
Benefits
- ✅ Dynamic UI: Frontend can hide inaccessible menus/buttons
- ✅ Better UX: Users only see what they can actually use
- ✅ Reduced errors: Fewer 403 responses from hidden operations
- ✅ Faster feedback: Permission checks happen at render time, not request time
Dependencies
- Phase 5 (ACE refactoring) may change how permissions are stored, which would affect this API's implementation
Phase 7 — Resource Pool Renaming (Future)
Goal: Rename "Resource Pool" to a more descriptive name that better reflects its actual purpose.
Problem
The current name "Resource Pool" is too generic and doesn't clearly convey its actual function:
- ❌ Ambiguous name: "Resource Pool" could refer to compute pools, connection pools, etc.
- ❌ Unclear purpose: Users don't understand it's primarily for sharing projects
- ❌ Discoverability: Hard to find the right feature when looking for project sharing
Actual Function
Resource pools in GNS3 are used for:
- Project sharing: Allow users to access projects created by other users
- Team collaboration: Enable team members to work on shared projects
- Access control: Provide fine-grained permissions for project access through three-step filtering (ACE check → created_by filter → resource pools)
Proposed Name Options
| Option | Pros | Cons |
|---|---|---|
| Project Pool | More explicit, indicates it contains projects | Still uses "pool" terminology |
| Shared Projects | Directly describes the function | Loses the "collection" concept |
| Team Projects | Emphasizes collaboration use case | Doesn't cover non-team sharing scenarios |
Recommended: Project Pool - strikes a balance between clarity and consistency with existing terminology.
Implementation Scope
Renaming would require changes to:
- Database tables:
resource_pools→project_pools - API routes:
/v3/pools→/v3/project_pools - Schema classes and field names throughout the codebase
- All documentation and help text
- Migration script to preserve existing data
Benefits
- ✅ Improved discoverability: Users can easily find the project sharing feature
- ✅ Better onboarding: New users understand the purpose without confusion
- ✅ Clearer API: API endpoints and schemas more self-documenting
Implementation Considerations
- Breaking change: Requires API version bump or backward compatibility layer
- Data migration: Existing resource pools must be preserved during table rename
- Documentation updates: All references in docs, tutorials, and API specs need updating
- UI changes: Frontend labels and navigation menus need to match new terminology
Phase 8 — Per-User Project Namespace (Future)
Goal: Allow project names to be unique per user instead of globally unique, enabling better user experience.
Current Problem
Although Phase 1 implements user isolation (users only see projects they created or have access to through resource pools), project names remain globally unique:
- ❌ Naming conflicts: Alice and Bob cannot both create a project named "My Project"
- ❌ Unnecessary restrictions: Even though projects are isolated, users must coordinate globally unique names
- ❌ Poor user experience: Users get confusing error messages when trying to use common names like "Test Project"
Proposed Solution
Change project uniqueness from global to per-user:
Current:
UNIQUE(name) -- Project names must be globally unique
Proposed:
UNIQUE(user_id, name) -- Project names unique per user
Benefits
- ✅ Better UX: Users can name projects whatever they want without worrying about global conflicts
- ✅ Natural naming: Common names like "Test Project" or "Demo" can coexist between users
- ✅ No coordination needed: Teams don't need to maintain a shared project naming registry
- ✅ Consistent isolation: Projects are isolated both in visibility AND naming
Implementation Scope
Changes required:
- Database schema: Modify Project table unique constraint from
(name)to(user_id, name) - Migration script: Handle existing projects with conflicting names
- API validation: Update project creation validation logic
- Frontend: Remove global name uniqueness checks from UI
Migration Considerations
Handling existing name conflicts: If the database already has projects with the same name but different users:
- Option 1: Keep existing names, only enforce uniqueness for new projects
- Option 2: Append suffixes to duplicates (e.g., "My Project (alice)", "My Project (bob)")
- Option 3: Require admin resolution for conflicts before enabling new constraint
Recommended: Option 1 (grandfather existing projects) for minimal disruption.
Dependencies
- Phase 1 (User isolation): Must be completed first
- Database migration required to modify unique constraint
Phase 9 — User Self-Registration (Future)
Goal: Allow users to register their own accounts without requiring manual admin creation.
Current State
Currently, user accounts can only be created by administrators or through direct database operations:
- ❌ Admin burden: Every new user requires manual account creation
- ❌ Poor scalability: Not suitable for public deployments or large organizations
- ❌ Friction: Users cannot immediately start using the system
Proposed Features
Self-Registration Flow:
- User provides email, username, and password
- System validates input and creates account
- Optional email verification to confirm email address
- Account created with default role (typically "User" role)
- User can immediately log in and start creating projects
Email Verification (Optional):
- Send verification email with confirmation link/code
- Verify email address before granting full access
- Prevent spam account creation
- Require SMTP server configuration
Implementation Components
- New API endpoint:
POST /v3/access/register(public, no authentication required) - Email service: Integration with SMTP server for sending emails
- Configuration: SMTP settings (host, port, credentials, encryption)
- Rate limiting: Prevent abuse of self-registration
- Captcha integration: Optional bot protection
Configuration
Server:
email:
enabled: true
smtp_host: smtp.example.com
smtp_port: 587
smtp_username: noreply@example.com
smtp_password: secret
use_tls: true
registration:
require_email_verification: true
default_role: "User"
allow_public_registration: true
Security Considerations
- ✅ Rate limiting: Prevent spam account creation
- ✅ Email verification: Confirm email ownership
- ✅ Default permissions: New users get limited default role
- ✅ Admin approval (optional): Require admin approval before account activation
- ❌ Superadmin creation: Never allow self-registration as superadmin
Dependencies
- Phase 10 (Email service): SMTP integration required for email verification
Phase 10 — Email Service Integration (Future)
Goal: Implement email sending capability for notifications, verification, and alerts.
Use Cases
- User registration: Email verification links/codes
- Password reset: Secure password reset emails
- System alerts: Error notifications, system updates
- Project sharing: Notify users when projects are shared via resource pools
- Usage reports: Periodic usage summaries or quota alerts
Proposed Implementation
Email Service Architecture:
- Abstract email service interface
- Support multiple email providers (SMTP, SendGrid, AWS SES, etc.)
- Email templates with Jinja2 for customization
- Async email sending to avoid blocking API responses
- Email queue for retry logic on failures
Email Templates:
- Registration verification
- Password reset
- Project shared notification
- System alerts
- Usage reports
API Endpoints:
# Configuration (admin only)
POST /v3/access/config/email
GET /v3/access/config/email
PUT /v3/access/config/email
# Test email (admin only)
POST /v3/access/config/email/test
# Password reset (public)
POST /v3/access/users/password/reset/request
POST /v3/access/users/password/reset/confirm
# Email verification (public)
POST /v3/access/users/verify/email
POST /v3/access/users/verify/confirm
Database Schema
New table for email tracking:
CREATE TABLE email_verification_tokens (
token_id UUID PRIMARY KEY,
user_id UUID REFERENCES users(user_id) ON DELETE CASCADE,
token VARCHAR(255), # Verification code
purpose VARCHAR(50), # 'registration', 'password_reset', etc.
expires_at DATETIME,
created_at DATETIME,
used BOOLEAN DEFAULT FALSE
);
Configuration
Server:
email:
enabled: true
provider: "smtp" # or "sendgrid", "aws_ses", etc.
from_address: "noreply@example.com"
from_name: "GNS3 Server"
reply_to: "support@example.com"
smtp:
host: smtp.example.com
port: 587
username: noreply@example.com
password: encrypted_password
use_tls: true
templates_dir: /etc/gns3/email_templates
Security Considerations
- ✅ Encrypted credentials: SMTP passwords stored encrypted in database
- ✅ Token expiration: Verification tokens expire after configurable time
- ✅ Rate limiting: Prevent email spamming
- ✅ Async sending: Don't block API responses on email operations
- ✅ Retry logic: Handle temporary email service failures
- ✅ Privacy: Don't expose user information in error messages
Dependencies
- Phase 9 (Self-registration): User self-registration requires email verification
- Encryption utilities for storing SMTP credentials securely