RET-08: ret-06-api auf rbac-06 migrieren

- internal/rbacclient: HTTP-Client fuer Core RBAC-06 (POST /authorize)
- retentionapi.RequireRole (Header-Provisorium) ersetzt durch
  RequireRBAC, echter Aufruf gegen RBAC-06, fail-closed bei Fehlern
- Mount nimmt jetzt rbacclient.Client entgegen
- alle bestehenden RET-06-API-Tests weiterhin gruen
- neue Tests: verweigerte Rolle (403 gegen echte RBAC-06-Antwort),
  erlaubte Rolle (200), RBAC-06 nicht erreichbar -> fail-closed (403)
- real deployed auf 131, end-zu-ende per curl nachgewiesen (403/403/200/403)

Pruefungen siehe archive/docs/RET-08-PRUEFPROTOKOLL.md
This commit is contained in:
sysops
2026-08-30 08:43:00 +02:00
parent 21278f1405
commit b30e16cb4f
7 changed files with 288 additions and 55 deletions
+32 -30
View File
@@ -6,43 +6,45 @@
package retentionapi
import (
"errors"
"net/http"
"strings"
"gitea.perlbach24.de/scripte/nexarch/archive/internal/rbacclient"
)
// requiredRoleHeader ist der Header-Name des PROVISORISCHEN Rollen-
// Checks (siehe RequireRole-Dokumentation).
const requiredRoleHeader = "X-Admin-Roles"
// requestedRoleHeader trägt die vom Aufrufer beanspruchte Rolle. Die
// eigentliche Autorisierungsentscheidung trifft NICHT dieser Header,
// sondern Core RBAC-06 (siehe RequireRBAC) — der Header ist nur die
// Behauptung, RBAC-06 prüft sie gegen die echten Policy-Rules.
const requestedRoleHeader = "X-User-Role"
// RequireRole ist ein PROVISORISCHER Rollen-Check, KEIN RBAC-02-Aufruf.
//
// Core RBAC-02 (internal/policy, Enforcer.Authorize) ist reiner
// Go-Code innerhalb des Core-Moduls, hat keine HTTP-Schnittstelle, über
// die Archive (physisch getrenntes Go-Modul) es aufrufen könnte —
// derselbe "gefunden, aber nicht modulübergreifend verdrahtet"-Befund
// wie bei Core FDN-03/FDN-09. Bis ein Core-seitiger HTTP-Wrapper um
// RBAC-02 existiert (eigenes, künftiges Core-Ticket, z. B. RBAC-06),
// prüft dieser Middleware NUR einen einfachen, selbst gesetzten Header
// (`X-Admin-Roles`, kommagetrennt) auf das Vorhandensein der
// geforderten Rolle — KEINE echte Autorisierung gegen Core, leicht zu
// fälschen von jedem, der den Header selbst setzen kann. Muss ersetzt
// werden, sobald der Core-HTTP-Wrapper existiert.
func RequireRole(requiredRole string, next http.HandlerFunc) http.HandlerFunc {
// RequireRBAC ist RET-08: die Ablösung des früheren, header-basierten
// Provisoriums (siehe archive/docs/RET-06-API-PRUEFPROTOKOLL.md) durch
// einen echten Aufruf von Core RBAC-06 (POST /authorize). Fail-closed:
// jeder Fehler beim Aufruf von RBAC-06 (Transport, Timeout, Ablehnung)
// führt zur Verweigerung, niemals zu stillschweigendem Zugriff.
func RequireRBAC(client *rbacclient.Client, permission string, next http.HandlerFunc) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
roles := r.Header.Get(requiredRoleHeader)
if !containsRole(roles, requiredRole) {
http.Error(w, "zugriff verweigert: rolle "+requiredRole+" erforderlich (provisorischer check, siehe RequireRole)", http.StatusForbidden)
role := r.Header.Get(requestedRoleHeader)
if role == "" {
http.Error(w, "zugriff verweigert: header "+requestedRoleHeader+" erforderlich", http.StatusForbidden)
return
}
allowed, err := client.Authorize(r.Context(), role, permission)
if err != nil {
if errors.Is(err, rbacclient.ErrDenied) {
http.Error(w, "zugriff verweigert: rolle "+role+" hat kein recht "+permission, http.StatusForbidden)
return
}
// RBAC-06 nicht erreichbar oder fehlerhafte Antwort:
// fail-closed, kein Fallback auf offenen Zugriff.
http.Error(w, "zugriff verweigert: autorisierungsdienst nicht erreichbar", http.StatusForbidden)
return
}
if !allowed {
http.Error(w, "zugriff verweigert: rolle "+role+" hat kein recht "+permission, http.StatusForbidden)
return
}
next(w, r)
}
}
func containsRole(commaSeparated, role string) bool {
for _, r := range strings.Split(commaSeparated, ",") {
if strings.TrimSpace(r) == role {
return true
}
}
return false
}