Role-based access control (RBAC) for workspace-scoped operations. Permissions are enforced on both controller (API) and panel (UI) sides.
OWNER (4) > ADMIN (3) > USER (2) > VIEWER (1)
| Role | Code Value | Description |
|---|---|---|
| OWNER | OWNER |
Full control, workspace deletion, ownership transfer |
| ADMIN | ADMIN |
Manage agents, probes, and members |
| USER | READ_WRITE |
Create and edit agents/probes |
| VIEWER | READ_ONLY |
View-only access |
| Action | VIEWER | USER | ADMIN | OWNER |
|---|---|---|---|---|
| View workspace | ✅ | ✅ | ✅ | ✅ |
| Edit name/description | ❌ | ❌ | ✅ | ✅ |
| Delete workspace | ❌ | ❌ | ❌ | ✅ |
| Transfer ownership | ❌ | ❌ | ❌ | ✅ |
| Action | VIEWER | USER | ADMIN | OWNER |
|---|---|---|---|---|
| View members | ✅ | ✅ | ✅ | ✅ |
| Invite members | ❌ | ❌ | ✅ | ✅ |
| Change role | ❌ | ❌ | ✅* | ✅ |
| Remove members | ❌ | ❌ | ✅* | ✅ |
*ADMIN cannot modify OWNER or other ADMINs
| Action | VIEWER | USER | ADMIN | OWNER |
|---|---|---|---|---|
| View agents | ✅ | ✅ | ✅ | ✅ |
| Create agents | ❌ | ✅ | ✅ | ✅ |
| Edit agents | ❌ | ✅ | ✅ | ✅ |
| Delete agents | ❌ | ❌ | ✅ | ✅ |
| Issue bootstrap PIN | ❌ | ✅ | ✅ | ✅ |
| Action | VIEWER | USER | ADMIN | OWNER |
|---|---|---|---|---|
| View probes/data | ✅ | ✅ | ✅ | ✅ |
| Create probes | ❌ | ✅ | ✅ | ✅ |
| Edit probes | ❌ | ✅ | ✅ | ✅ |
| Delete probes | ❌ | ❌ | ✅ | ✅ |
| Endpoint | Method | Min Role |
|---|---|---|
/workspaces/{id} |
GET | VIEWER |
/workspaces/{id} |
PATCH | ADMIN |
/workspaces/{id} |
DELETE | OWNER |
| Endpoint | Method | Min Role |
|---|---|---|
/workspaces/{id}/members |
GET | VIEWER |
/workspaces/{id}/members |
POST | ADMIN |
/workspaces/{id}/members/{mid} |
PATCH | ADMIN |
/workspaces/{id}/members/{mid} |
DELETE | ADMIN |
| Endpoint | Method | Min Role |
|---|---|---|
/workspaces/{id}/agents |
GET | VIEWER |
/workspaces/{id}/agents |
POST | USER |
/workspaces/{id}/agents/{aid} |
GET | VIEWER |
/workspaces/{id}/agents/{aid} |
PATCH | USER |
/workspaces/{id}/agents/{aid} |
DELETE | ADMIN |
/workspaces/{id}/agents/{aid}/issue-pin |
POST | USER |
| Endpoint | Method | Min Role |
|---|---|---|
/.../probes |
GET | VIEWER |
/.../probes |
POST | USER |
/.../probes/{pid} |
PATCH | USER |
/.../probes/{pid} |
DELETE | ADMIN |
File: controller/web/permissions.go
// Permission level constants
const (
CanView = workspace.RoleReadOnly // VIEWER
CanEdit = workspace.RoleReadWrite // USER
CanManage = workspace.RoleAdmin // ADMIN
CanOwn = workspace.RoleOwner // OWNER
)
// Middleware functions
RequireRole(store, minRole) // Check minimum role
RequireWorkspaceAccess(store) // Check any membershipGET /workspaces/{id} returns user's role:
{
"id": 1,
"name": "Production Network",
"my_role": "ADMIN"
}File: panel/src/composables/usePermissions.ts
const { canView, canEdit, canManage, canOwn } = usePermissions(role);
// Granular permissions
canCreateAgent, canDeleteAgent
canCreateProbe, canDeleteProbe
canInviteMembers, canEditMembers
canDeleteWorkspace, canTransferOwnership
// Helper function
hasMinimumRole(userRole, requiredRole)File: panel/src/router/index.ts
Protected routes use meta.requiresRole:
| Route Pattern | Required Role |
|---|---|
/workspaces/:wID/edit |
ADMIN |
/workspaces/:wID/members/invite |
ADMIN |
/workspaces/:wID/members/edit/:id |
ADMIN |
/workspaces/:wID/members/remove/:id |
ADMIN |
/workspaces/:wID/agents/new |
USER |
/workspaces/:wID/agents/:aID/edit |
USER |
/workspaces/:wID/agents/:aID/delete |
ADMIN |
/workspaces/:wID/agents/:aID/probes/new |
USER |
/workspaces/:wID/agents/:aID/probes/edit |
USER |
/workspaces/:wID/agents/:aID/probes/:pID/delete |
ADMIN |
Unauthorized access redirects to /403 (Forbidden page).
Views use usePermissions composable:
<script setup>
const permissions = computed(() => usePermissions(state.workspace.my_role));
</script>
<template>
<button v-if="permissions.canEdit.value">Edit</button>
<button v-if="permissions.canManage.value">Delete</button>
<span v-if="!permissions.canEdit.value">View only</span>
</template>Guarded Views:
Workspace.vue- Edit button (ADMIN+), Create Agent (USER+)Agent.vue- Edit Probes (USER+), Add Probe (USER+)Members.vue- Invite/Edit/Remove (ADMIN+)
CREATE TABLE workspace_members (
id SERIAL PRIMARY KEY,
workspace_id INT NOT NULL REFERENCES workspaces(id),
user_id INT DEFAULT 0,
email VARCHAR(320) DEFAULT '',
role VARCHAR(20) NOT NULL, -- READ_ONLY, READ_WRITE, ADMIN, OWNER
meta JSONB DEFAULT '{}',
created_at TIMESTAMP,
updated_at TIMESTAMP,
deleted_at TIMESTAMP,
invited_at TIMESTAMP,
accepted_at TIMESTAMP,
revoked_at TIMESTAMP
);{
"error": "insufficient permissions",
"required_role": "ADMIN"
}Redirects to /403 page with "Access Denied" message.