Files
BetterDesk/docs/features/RBAC_PHASE52.md
T
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

7.6 KiB

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:

-- 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

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

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")