Files
UNITRONIX d96efad802 fix(rbac): block deleting last super_admin and clarify org owner label
Align Go delete-user guard with update/demotion logic (Discussion #99), add tests, fix pro-role docs, and label org owner as Org Admin in all locales.
2026-06-09 05:07:00 +02:00

166 lines
7.6 KiB
Markdown

# RBAC Phase 52 — Granular Permissions, 6-Role Hierarchy & Data Scoping
> Implemented: 2026-04-10 | Updated: 2026-04-10 (6-role hierarchy) | Discussion: #99
## Overview
Phase 52 implements the full RBAC overhaul proposed in Discussion #99:
- **Phase 1**: JWT org context + org-scoped data filtering
- **Phase 2**: 28 granular permissions replacing role-level gates
- **Phase 3**: Super admin protection (self-demotion, role boundary, server admin)
- **Phase 4**: 6-role hierarchy (super_admin, server_admin, global_admin + legacy admin/operator/viewer/pro)
- **Phase 5**: Org role boundary enforcement + privilege escalation prevention
- **Phase 6**: Peer org-scoping on single-device endpoints
## 6-Role Hierarchy (Branched)
```
super_admin ← Full access to everything, manages other super admins
├── server_admin ← Server infrastructure only, read-only user visibility
├── global_admin ← All-org user/device management, NO server access
└── admin ← Legacy alias, equivalent to super_admin
├── operator ← Day-to-day device ops + chat
├── viewer ← Read-only dashboards
└── pro ← API-only device view
```
**Key design**: `server_admin` and `global_admin` are parallel roles — not one above the other.
They share the same privilege level (4) but have DIFFERENT permission sets.
Use `auth.CanAssignRole()` for role assignment boundaries instead of RoleLevel comparison.
## Changes
### New Files
| File | Description |
|------|-------------|
| `auth/permissions.go` | 28 permission constants, 7 default role maps, helpers |
### Modified Files
| File | Changes |
|------|---------|
| `auth/roles.go` | 7 role constants (super_admin, server_admin, global_admin, admin, operator, viewer, pro), branched `RoleLevel()`, `IsSuperAdminRole()`, `IsServerLevel()`, `CanAssignRole()` |
| `auth/jwt.go` | `OrgID` field in Claims, `GenerateOrgToken()` method |
| `db/database.go` | `User.IsServerAdmin`, `RolePermission` struct, `OrgRoleLevel()`, `OrgCanAssignRole()`, `ValidOrgRole()`, 5 new interface methods |
| `db/sqlite.go` | `role_permissions` table, `is_server_admin` column, 5 method implementations |
| `db/postgres.go` | Same as sqlite — table, column, 5 method implementations |
| `api/auth_handlers.go` | `requirePermission()` with super_admin bypass, `requireOrgMembership()` with global_admin bypass, `peerOrgScopeCheck()`, `CanAssignRole()` in create/update user, last-admin demotion guard |
| `api/server.go` | ~30 routes migrated to `requirePermission`, peer org scope checks on 7 single-device endpoints |
| `api/org_handlers.go` | Org login embeds `org_id` in JWT, org role boundary in create/update user, org user visibility scoping |
| `web-nodejs/middleware/auth.js` | 7 role permission maps, `isSuperAdminRole()`, updated `requireAdmin()` + `requireRole()` + `requirePermission()` |
## Permission System
### 28 Granular Permissions
| Category | Permissions |
|----------|------------|
| Device | `device.view`, `device.connect`, `device.edit`, `device.delete`, `device.ban`, `device.change_id` |
| User | `user.view`, `user.create`, `user.edit`, `user.delete` |
| Server | `server.config`, `server.keys` |
| Organization | `org.create`, `org.edit`, `org.delete`, `org.manage_users`, `org.manage_devices` |
| Audit | `audit.view`, `metrics.view`, `blocklist.edit` |
| CDAP | `cdap.view`, `cdap.command`, `cdap.terminal`, `cdap.files` |
| Enrollment | `enrollment.manage`, `enrollment.approve` |
| Other | `chat.access`, `branding.edit` |
### Default Role Mappings
| Role | # Permissions | Key Permissions |
|------|--------------|----------------|
| `super_admin` | All 28 | Everything |
| `admin` | All 28 | Legacy alias for super_admin |
| `server_admin` | 8 | server.config, server.keys, blocklist.edit, user.view (read-only!), device.view, audit.view, metrics.view, enrollment.manage |
| `global_admin` | 22 | user.*, org.*, device.*, audit.view, metrics.view, cdap.view/.command, chat.access, enrollment.*, branding.edit — **NO** server.config/server.keys |
| `operator` | 12 | device.view/.connect/.edit, user.view, audit.view, metrics.view, cdap.view/.command, enrollment.approve, chat.access, org.manage_devices |
| `viewer` | 5 | device.view, audit.view, metrics.view, cdap.view, chat.access |
| `pro` | 0 | API-only RustDesk PRO activation — no panel permissions |
### Role Assignment Boundaries
| Caller | Can Assign |
|--------|-----------|
| `super_admin` / `admin` | Any role |
| `global_admin` | `operator`, `viewer`, `pro` only |
| `server_admin` | Cannot assign any roles |
| `operator` / `viewer` / `pro` | Cannot assign any roles |
### Org Role Boundaries
| Caller Org Role | Can Assign |
|----------------|-----------|
| `owner` | `admin`, `operator`, `user` (not another `owner`) |
| `admin` | `operator`, `user` |
| `operator` / `user` | Cannot assign any roles |
### Custom Permission Overrides
The `role_permissions` table allows overriding defaults:
```sql
-- Grant operators the ability to delete devices
INSERT INTO role_permissions (role, permission, granted) VALUES ('operator', 'device.delete', true);
-- Revoke chat from viewers
INSERT INTO role_permissions (role, permission, granted) VALUES ('viewer', 'chat.access', false);
```
The `requirePermission` middleware checks DB overrides first, then falls back to defaults.
## Security Protections
| Protection | Description |
|-----------|-------------|
| Self-demotion prevention | Admins cannot lower their own role |
| Role boundary enforcement | `CanAssignRole()` enforces branched hierarchy — server_admin can't assign, global_admin only below GA |
| Server admin flag | `is_server_admin` field — only server admins can modify other server admins |
| Last-admin deletion guard | Cannot delete the sole remaining admin |
| Last-admin demotion guard | Cannot demote the last super_admin/admin (409 Conflict) |
| Org role boundary | `OrgCanAssignRole()` enforces org-level hierarchy (owner → admin/op/user, admin → op/user) |
| Org self-modification block | Cannot change own org role |
| Org authority check | Cannot modify users at or above own org-level authority |
| Peer org scoping | Org-scoped users can only access devices assigned to their org |
| Org user visibility | Org users can only see themselves in the user list |
## Data Scoping
| Endpoint | Scoping |
|----------|---------|
| `GET /api/peers` | Org-scoped users see only their org's devices via `ListPeersForOrg()` |
| `GET /api/peers/{id}` | `peerOrgScopeCheck()` — verifies peer belongs to caller's org |
| `DELETE /api/peers/{id}` | Same peer org scope check |
| `PATCH /api/peers/{id}` | Same peer org scope check |
| `POST /api/peers/{id}/ban` | Same peer org scope check |
| `POST /api/peers/{id}/unban` | Same peer org scope check |
| `POST /api/peers/{id}/change-id` | Same peer org scope check |
| `GET /api/peers/{id}/metrics` | Same peer org scope check |
| `GET /api/org/{id}/users` | Org users only see themselves; org admin/operator see all |
| `GET /api/orgs` | Non-admin users only see orgs they belong to |
## Database Schema
### New Table: `role_permissions`
```sql
CREATE TABLE role_permissions (
id INTEGER PRIMARY KEY AUTOINCREMENT,
role TEXT NOT NULL,
permission TEXT NOT NULL,
granted INTEGER NOT NULL DEFAULT 1,
UNIQUE(role, permission)
);
```
### New Column: `users.is_server_admin`
```sql
ALTER TABLE users ADD COLUMN is_server_admin INTEGER DEFAULT 0;
```
## API Response Changes
- `GET /api/auth/me` — now includes `is_server_admin` field
- `GET /api/users` — user list now includes `is_server_admin` field
- Error responses include specific permission names (e.g., `"Permission denied: device.delete"`)