Files
nexarch/internal/rbac/handler.go
T

251 lines
8.1 KiB
Go

package rbac
import (
"encoding/json"
"net/http"
"strings"
"gitea.perlbach24.de/scripte/nexarch/internal/auth"
)
// Handler stellt Rollen-/Rechte- und Gruppenverwaltung als HTTP-Endpunkte
// bereit (RBAC-05). Duenner Client der bestehenden Store/GroupStore-Schicht —
// keine eigene Rechtelogik hier (siehe Ticket-Produkt-DNA).
type Handler struct {
roles *Store
groups *GroupStore
}
func NewHandler(roles *Store, groups *GroupStore) *Handler {
return &Handler{roles: roles, groups: groups}
}
// requireManageUsers prueft, ob der aufrufende Benutzer ueberhaupt
// Rechteverwaltung ausueben darf — technisch durchgesetzt hier zusaetzlich
// zu RBAC-02 (Policy-Enforcement-Schicht), da diese Kachel ein eigener,
// besonders sicherheitskritischer HTTP-Einstiegspunkt ist.
func requireManageUsers(w http.ResponseWriter, r *http.Request, roles *Store) (callerUserID string, ok bool) {
claims, authOK := auth.ClaimsFromContext(r.Context())
if !authOK {
http.Error(w, "nicht angemeldet", http.StatusUnauthorized)
return "", false
}
assignment, err := roles.Get(r.Context(), claims.UserID)
if err != nil || !HasPermission(assignment.Role, PermManageUsers) {
http.Error(w, "keine Berechtigung zur Rechteverwaltung", http.StatusForbidden)
return "", false
}
return claims.UserID, true
}
type roleInfo struct {
Role Role `json:"role"`
Permissions []Permission `json:"permissions"`
}
// ListRoles liefert die (im Tenant-Geltungsbereich zuweisbaren) Rollen mit
// ihren vollstaendig aufgeloesten Rechten (Akzeptanzkriterium 1).
func (h *Handler) ListRoles(w http.ResponseWriter, r *http.Request) {
if _, ok := requireManageUsers(w, r, h.roles); !ok {
return
}
out := make([]roleInfo, 0, len(assignableRoles))
for role := range assignableRoles {
out = append(out, roleInfo{Role: role, Permissions: EffectivePermissions(role)})
}
w.Header().Set("Content-Type", "application/json")
_ = json.NewEncoder(w).Encode(out)
}
type assignRoleRequest struct {
UserID string `json:"user_id"`
Role Role `json:"role"`
}
// roleRank ordnet die Rollenhierarchie fuer den Selbst-Eskalations-Vergleich
// (Akzeptanzkriterium 2 / Pruefung 2) — hoehere Zahl = mehr Rechte. Basiert
// auf derselben parent-Kette wie EffectivePermissions, nicht dupliziert als
// eigenstaendiges Modell.
func roleRank(role Role) int {
rank := 0
for r, ok := role, true; ok; r, ok = parent[r] {
rank++
}
return rank
}
// AssignRole weist einem Benutzer eine Rolle zu (Akzeptanzkriterium 1).
// Verweigert explizit, dass sich der aufrufende Benutzer selbst eine
// hoehere Rolle zuweist (Akzeptanzkriterium 2 / Pruefung 2) — unabhaengig
// davon, ob RBAC-02 dieselbe Aktion ohnehin ueber die allgemeine
// Policy-Schicht verhindern wuerde: Selbst-Eskalation ist ein derart
// sicherheitskritischer Einzelfall, dass er hier zusaetzlich explizit und
// unuebersehbar geprueft wird, statt sich allein auf eine generische
// Regel an anderer Stelle zu verlassen.
func (h *Handler) AssignRole(w http.ResponseWriter, r *http.Request) {
callerUserID, ok := requireManageUsers(w, r, h.roles)
if !ok {
return
}
var req assignRoleRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, "ungueltige Anfrage", http.StatusBadRequest)
return
}
if req.UserID == callerUserID {
callerAssignment, err := h.roles.Get(r.Context(), callerUserID)
callerRole := RoleUser
if err == nil {
callerRole = callerAssignment.Role
}
if roleRank(req.Role) > roleRank(callerRole) {
http.Error(w, "Sie können sich nicht selbst eine höhere Rolle zuweisen.", http.StatusForbidden)
return
}
}
assignment, err := h.roles.Assign(r.Context(), req.UserID, req.Role, callerUserID)
if err != nil {
http.Error(w, "Rolle konnte nicht zugewiesen werden.", http.StatusBadRequest)
return
}
w.Header().Set("Content-Type", "application/json")
_ = json.NewEncoder(w).Encode(assignment)
}
// RoleHistory liefert die Zuweisungshistorie eines Benutzers — der in
// Akzeptanzkriterium 3 geforderte Verweis auf das Audit-Log (die
// role_assignment_history-Tabelle selbst IST die Nachvollziehbarkeits-Spur;
// eine vollstaendige Anbindung an Core AUD-01 ist nicht Teil dieser Kachel).
func (h *Handler) RoleHistory(w http.ResponseWriter, r *http.Request) {
if _, ok := requireManageUsers(w, r, h.roles); !ok {
return
}
userID := strings.TrimPrefix(r.URL.Path, "/rbac/users/")
userID = strings.TrimSuffix(userID, "/history")
if userID == "" {
http.Error(w, "user_id fehlt", http.StatusBadRequest)
return
}
history, err := h.roles.History(r.Context(), userID)
if err != nil {
http.Error(w, "historie konnte nicht geladen werden", http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/json")
_ = json.NewEncoder(w).Encode(history)
}
type createGroupRequest struct {
Name string `json:"name"`
}
// CreateGroup legt eine neue Gruppe an (Akzeptanzkriterium 2).
func (h *Handler) CreateGroup(w http.ResponseWriter, r *http.Request) {
if _, ok := requireManageUsers(w, r, h.roles); !ok {
return
}
var req createGroupRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, "ungueltige Anfrage", http.StatusBadRequest)
return
}
group, err := h.groups.CreateGroup(r.Context(), req.Name)
if err != nil {
http.Error(w, "Gruppe konnte nicht angelegt werden.", http.StatusBadRequest)
return
}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusCreated)
_ = json.NewEncoder(w).Encode(group)
}
type groupWithMembers struct {
Group
Members []string `json:"members"`
}
// ListGroups liefert alle Gruppen inkl. Mitgliederliste (Akzeptanzkriterium 2).
func (h *Handler) ListGroups(w http.ResponseWriter, r *http.Request) {
if _, ok := requireManageUsers(w, r, h.roles); !ok {
return
}
groups, err := h.groups.ListGroups(r.Context())
if err != nil {
http.Error(w, "Gruppen konnten nicht geladen werden.", http.StatusInternalServerError)
return
}
out := make([]groupWithMembers, 0, len(groups))
for _, g := range groups {
members, err := h.groups.Members(r.Context(), g.ID)
if err != nil {
http.Error(w, "Mitglieder konnten nicht geladen werden.", http.StatusInternalServerError)
return
}
out = append(out, groupWithMembers{Group: g, Members: members})
}
w.Header().Set("Content-Type", "application/json")
_ = json.NewEncoder(w).Encode(out)
}
type groupActionRequest struct {
GroupID string `json:"group_id"`
UserID string `json:"user_id"`
Role Role `json:"role"`
}
// SetGroupRole weist einer Gruppe eine Rolle zu (Akzeptanzkriterium 2).
func (h *Handler) SetGroupRole(w http.ResponseWriter, r *http.Request) {
if _, ok := requireManageUsers(w, r, h.roles); !ok {
return
}
var req groupActionRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, "ungueltige Anfrage", http.StatusBadRequest)
return
}
if err := h.groups.SetGroupRole(r.Context(), req.GroupID, req.Role); err != nil {
http.Error(w, "Gruppenrolle konnte nicht gesetzt werden.", http.StatusBadRequest)
return
}
w.WriteHeader(http.StatusOK)
}
// AddGroupMember nimmt einen Benutzer in eine Gruppe auf (Akzeptanzkriterium 2).
func (h *Handler) AddGroupMember(w http.ResponseWriter, r *http.Request) {
if _, ok := requireManageUsers(w, r, h.roles); !ok {
return
}
var req groupActionRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, "ungueltige Anfrage", http.StatusBadRequest)
return
}
if err := h.groups.AddMember(r.Context(), req.GroupID, req.UserID); err != nil {
http.Error(w, "Mitglied konnte nicht hinzugefügt werden.", http.StatusBadRequest)
return
}
w.WriteHeader(http.StatusOK)
}
// RemoveGroupMember entfernt einen Benutzer aus einer Gruppe.
func (h *Handler) RemoveGroupMember(w http.ResponseWriter, r *http.Request) {
if _, ok := requireManageUsers(w, r, h.roles); !ok {
return
}
var req groupActionRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, "ungueltige Anfrage", http.StatusBadRequest)
return
}
if err := h.groups.RemoveMember(r.Context(), req.GroupID, req.UserID); err != nil {
http.Error(w, "Mitglied konnte nicht entfernt werden.", http.StatusBadRequest)
return
}
w.WriteHeader(http.StatusOK)
}