251 lines
8.1 KiB
Go
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)
|
|
}
|