feat: add rbac policy matrix
This commit is contained in:
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
## Unreleased
|
## Unreleased
|
||||||
|
|
||||||
|
- Zentrale RBAC-Policy-Funktion mit Rollen/Aktions-Matrix angelegt.
|
||||||
- Lazy Profil-Sync vom Supabase-JWT nach `public.profiles` im Backend angelegt.
|
- Lazy Profil-Sync vom Supabase-JWT nach `public.profiles` im Backend angelegt.
|
||||||
- Backend-JWT-Middleware mit JWKS-Validierung, lokalem HS256-Fallback und geschuetztem `/me` Endpunkt angelegt.
|
- Backend-JWT-Middleware mit JWKS-Validierung, lokalem HS256-Fallback und geschuetztem `/me` Endpunkt angelegt.
|
||||||
- RLS-Advisor-Cleanup fuer alle uebrigen Public-Tabellen angelegt.
|
- RLS-Advisor-Cleanup fuer alle uebrigen Public-Tabellen angelegt.
|
||||||
|
|||||||
@@ -82,6 +82,11 @@ Arbeitsliste auf Basis von `proxmox-console-entwicklungsplan.md`. Die Entwurfsda
|
|||||||
- [x] `profiles` wird idempotent per `insert ... on conflict` erstellt/aktualisiert
|
- [x] `profiles` wird idempotent per `insert ... on conflict` erstellt/aktualisiert
|
||||||
- [x] Profil-Sync-Middleware mit Unit-Tests angelegt
|
- [x] Profil-Sync-Middleware mit Unit-Tests angelegt
|
||||||
- [x] `/me` fuehrt Profil-Sync vor Handler-Ausfuehrung aus
|
- [x] `/me` fuehrt Profil-Sync vor Handler-Ausfuehrung aus
|
||||||
|
- [x] E4-T02: Policy-/Permission-Funktion
|
||||||
|
- [x] Rollen und Aktionen zentral definiert
|
||||||
|
- [x] `Can(role, action)` angelegt
|
||||||
|
- [x] Tabellengetriebene Unit-Tests fuer Rollen/Aktionen angelegt
|
||||||
|
- [x] `cluster.manage` fuer Tenant-Rollen bewusst verweigert
|
||||||
|
|
||||||
## MVP-Backlog
|
## MVP-Backlog
|
||||||
|
|
||||||
@@ -131,3 +136,4 @@ Arbeitsliste auf Basis von `proxmox-console-entwicklungsplan.md`. Die Entwurfsda
|
|||||||
- 2026-06-10: RLS-Advisor-Cleanup `0009_rls_advisor_cleanup` angelegt; alle Public-Tabellen haben RLS aktiv.
|
- 2026-06-10: RLS-Advisor-Cleanup `0009_rls_advisor_cleanup` angelegt; alle Public-Tabellen haben RLS aktiv.
|
||||||
- 2026-06-10: JWT-Middleware-Tests fuer gueltige, abgelaufene, manipulierte und fehlende Tokens erfolgreich.
|
- 2026-06-10: JWT-Middleware-Tests fuer gueltige, abgelaufene, manipulierte und fehlende Tokens erfolgreich.
|
||||||
- 2026-06-10: Profil-Sync-Tests erfolgreich; lokaler `/me` Request legt genau ein `profiles`-Profil an.
|
- 2026-06-10: Profil-Sync-Tests erfolgreich; lokaler `/me` Request legt genau ein `profiles`-Profil an.
|
||||||
|
- 2026-06-10: RBAC-Policy-Funktion mit tabellengetriebenen Rollen/Aktions-Tests erfolgreich.
|
||||||
|
|||||||
@@ -0,0 +1,83 @@
|
|||||||
|
package rbac
|
||||||
|
|
||||||
|
type Role string
|
||||||
|
|
||||||
|
const (
|
||||||
|
RoleOwner Role = "owner"
|
||||||
|
RoleAdmin Role = "admin"
|
||||||
|
RoleMember Role = "member"
|
||||||
|
RoleViewer Role = "viewer"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Action string
|
||||||
|
|
||||||
|
const (
|
||||||
|
ActionVMRead Action = "vm.read"
|
||||||
|
ActionVMPower Action = "vm.power"
|
||||||
|
ActionVMCreate Action = "vm.create"
|
||||||
|
ActionVMDelete Action = "vm.delete"
|
||||||
|
ActionVMConsole Action = "vm.console"
|
||||||
|
ActionProjectRead Action = "project.read"
|
||||||
|
ActionProjectManage Action = "project.manage"
|
||||||
|
ActionSSHKeyRead Action = "ssh_key.read"
|
||||||
|
ActionSSHKeyManage Action = "ssh_key.manage"
|
||||||
|
ActionAuditRead Action = "audit.read"
|
||||||
|
ActionClusterManage Action = "cluster.manage"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Can(role Role, action Action) bool {
|
||||||
|
allowed, ok := permissions[role]
|
||||||
|
if !ok {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return allowed[action]
|
||||||
|
}
|
||||||
|
|
||||||
|
var permissions = map[Role]map[Action]bool{
|
||||||
|
RoleOwner: allow(
|
||||||
|
ActionVMRead,
|
||||||
|
ActionVMPower,
|
||||||
|
ActionVMCreate,
|
||||||
|
ActionVMDelete,
|
||||||
|
ActionVMConsole,
|
||||||
|
ActionProjectRead,
|
||||||
|
ActionProjectManage,
|
||||||
|
ActionSSHKeyRead,
|
||||||
|
ActionSSHKeyManage,
|
||||||
|
ActionAuditRead,
|
||||||
|
),
|
||||||
|
RoleAdmin: allow(
|
||||||
|
ActionVMRead,
|
||||||
|
ActionVMPower,
|
||||||
|
ActionVMCreate,
|
||||||
|
ActionVMDelete,
|
||||||
|
ActionVMConsole,
|
||||||
|
ActionProjectRead,
|
||||||
|
ActionProjectManage,
|
||||||
|
ActionSSHKeyRead,
|
||||||
|
ActionSSHKeyManage,
|
||||||
|
ActionAuditRead,
|
||||||
|
),
|
||||||
|
RoleMember: allow(
|
||||||
|
ActionVMRead,
|
||||||
|
ActionVMPower,
|
||||||
|
ActionVMCreate,
|
||||||
|
ActionVMConsole,
|
||||||
|
ActionProjectRead,
|
||||||
|
ActionSSHKeyRead,
|
||||||
|
),
|
||||||
|
RoleViewer: allow(
|
||||||
|
ActionVMRead,
|
||||||
|
ActionVMConsole,
|
||||||
|
ActionProjectRead,
|
||||||
|
ActionSSHKeyRead,
|
||||||
|
),
|
||||||
|
}
|
||||||
|
|
||||||
|
func allow(actions ...Action) map[Action]bool {
|
||||||
|
allowed := make(map[Action]bool, len(actions))
|
||||||
|
for _, action := range actions {
|
||||||
|
allowed[action] = true
|
||||||
|
}
|
||||||
|
return allowed
|
||||||
|
}
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
package rbac
|
||||||
|
|
||||||
|
import "testing"
|
||||||
|
|
||||||
|
func TestCan(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
role Role
|
||||||
|
action Action
|
||||||
|
want bool
|
||||||
|
}{
|
||||||
|
{name: "owner can manage project", role: RoleOwner, action: ActionProjectManage, want: true},
|
||||||
|
{name: "owner can read audit", role: RoleOwner, action: ActionAuditRead, want: true},
|
||||||
|
{name: "admin can delete vm", role: RoleAdmin, action: ActionVMDelete, want: true},
|
||||||
|
{name: "admin can manage ssh keys", role: RoleAdmin, action: ActionSSHKeyManage, want: true},
|
||||||
|
{name: "member can create vm", role: RoleMember, action: ActionVMCreate, want: true},
|
||||||
|
{name: "member cannot delete vm", role: RoleMember, action: ActionVMDelete, want: false},
|
||||||
|
{name: "member cannot read audit", role: RoleMember, action: ActionAuditRead, want: false},
|
||||||
|
{name: "viewer can read vm", role: RoleViewer, action: ActionVMRead, want: true},
|
||||||
|
{name: "viewer can open console", role: RoleViewer, action: ActionVMConsole, want: true},
|
||||||
|
{name: "viewer cannot power vm", role: RoleViewer, action: ActionVMPower, want: false},
|
||||||
|
{name: "viewer cannot create vm", role: RoleViewer, action: ActionVMCreate, want: false},
|
||||||
|
{name: "tenant roles cannot manage cluster", role: RoleOwner, action: ActionClusterManage, want: false},
|
||||||
|
{name: "unknown role denied", role: Role("unknown"), action: ActionVMRead, want: false},
|
||||||
|
{name: "unknown action denied", role: RoleOwner, action: Action("unknown.action"), want: false},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
if got := Can(tt.role, tt.action); got != tt.want {
|
||||||
|
t.Fatalf("Can(%q, %q) = %v, want %v", tt.role, tt.action, got, tt.want)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPolicyMatrix(t *testing.T) {
|
||||||
|
roles := []Role{RoleOwner, RoleAdmin, RoleMember, RoleViewer}
|
||||||
|
actions := []Action{
|
||||||
|
ActionVMRead,
|
||||||
|
ActionVMPower,
|
||||||
|
ActionVMCreate,
|
||||||
|
ActionVMDelete,
|
||||||
|
ActionVMConsole,
|
||||||
|
ActionProjectRead,
|
||||||
|
ActionProjectManage,
|
||||||
|
ActionSSHKeyRead,
|
||||||
|
ActionSSHKeyManage,
|
||||||
|
ActionAuditRead,
|
||||||
|
ActionClusterManage,
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, role := range roles {
|
||||||
|
for _, action := range actions {
|
||||||
|
_ = Can(role, action)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user