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