Files
nexarch/internal/policyapi/handler.go
T
patrickandClaude Sonnet 5 b3ba0fdfa6
CI / Web-Barrierefreiheitspruefung (axe, Tastatur, Reflow, Textabstaende) (push) Canceled after 0s
CI / Core-Modul (Build/Vet/Test) (push) Canceled after 0s
CI / DMS Security-Test-Gate (AC5, Merge-Gate) (push) Canceled after 0s
CI / DMS-Modul (Lint/Build/Test/Coverage/Artefakt) (push) Canceled after 0s
CI / DMS-Modul Supply-Chain-Scan (govulncheck) (push) Canceled after 0s
CI / Web (dms-browser) (push) Canceled after 0s
CI / Web (pdf-viewer) (push) Canceled after 0s
CI / Web (shl) (push) Canceled after 0s
Supply-Chain-Scan (Go) / govulncheck (push) Canceled after 0s
RBAC-10: mandant und modulaktivierung in authorize
POST /authorize nimmt optional den Mandanten entgegen und wertet modulgebundene Rechte ueber AuthorizeForTenant aus (fail closed). Mandant ist Pflicht fuer Rechte mit Modul-Bindung, das Service-Token laesst sich auf Mandanten einschraenken. Migration 0012 bindet die dms.*-Rechte an das Flag dms. Der DMS-policyclient sendet den Mandanten mit und trennt den Cache je Mandant.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-21 23:38:02 +02:00

102 lines
3.2 KiB
Go

package policyapi
import (
"encoding/json"
"errors"
"net/http"
"gitea.perlbach24.de/scripte/nexarch/internal/flag"
"gitea.perlbach24.de/scripte/nexarch/internal/policy"
"gitea.perlbach24.de/scripte/nexarch/internal/rbac"
"gitea.perlbach24.de/scripte/nexarch/internal/tenant"
)
// Option konfiguriert Mount (RBAC-10).
type Option func(*server)
// WithAllowedTenants bindet das Service-Token an eine Mandantenliste: ein
// Request fuer einen anderen Mandanten erhaelt 403. Ohne Option ist jeder
// gueltige Mandant zulaessig.
func WithAllowedTenants(slugs []string) Option {
return func(s *server) {
s.allowed = map[string]bool{}
for _, slug := range slugs {
s.allowed[slug] = true
}
}
}
type server struct {
enforcer *policy.Enforcer
flags *flag.Service
allowed map[string]bool
}
// Mount registriert den Autorisierungs-Endpunkt auf mux, hinter dem
// Service-Token-Check (Akzeptanzkriterium 2).
func Mount(mux *http.ServeMux, enforcer *policy.Enforcer, flags *flag.Service, serviceToken string, opts ...Option) {
s := &server{enforcer: enforcer, flags: flags}
for _, o := range opts {
o(s)
}
mux.HandleFunc("POST /authorize", RequireServiceToken(serviceToken, s.authorize))
}
type authorizeRequest struct {
Role string `json:"role"`
Permission string `json:"permission"`
Tenant string `json:"tenant,omitempty"`
}
type authorizeResponse struct {
Allowed bool `json:"allowed"`
// Reason erklaert eine Ablehnung: denied, module_inactive, tenant_required.
Reason string `json:"reason,omitempty"`
}
// authorize ruft AUSSCHLIESSLICH enforcer.AuthorizeRequest auf — keine
// eigene, zweite Autorisierungsentscheidung (Akzeptanzkriterium 1 aus
// RBAC-06: die HTTP-Antwort stimmt mit dem direkten Enforcer-Ergebnis
// ueberein, das ist strukturell garantiert, weil es derselbe Aufruf ist).
func (s *server) authorize(w http.ResponseWriter, r *http.Request) {
var req authorizeRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, "ungültiger request-body: "+err.Error(), http.StatusBadRequest)
return
}
if req.Role == "" || req.Permission == "" {
http.Error(w, "role und permission sind pflichtfelder", http.StatusBadRequest)
return
}
if req.Tenant != "" {
if err := tenant.ValidateSlug(req.Tenant); err != nil {
http.Error(w, "ungültiger mandant: "+err.Error(), http.StatusBadRequest)
return
}
if s.allowed != nil && !s.allowed[req.Tenant] {
http.Error(w, "mandant für dieses service-token nicht zugelassen", http.StatusForbidden)
return
}
}
err := s.enforcer.AuthorizeRequest(r.Context(), s.flags, req.Tenant, rbac.Role(req.Role), rbac.Permission(req.Permission))
resp := authorizeResponse{Allowed: err == nil}
switch {
case err == nil:
case errors.Is(err, policy.ErrModuleInactive):
resp.Reason = "module_inactive"
case errors.Is(err, policy.ErrTenantRequired):
resp.Reason = "tenant_required"
case errors.Is(err, policy.ErrDenied):
resp.Reason = "denied"
default:
// Ein anderer Fehler als "verboten" (z.B. DB-Fehler) ist kein
// gueltiges Allow/Deny-Ergebnis, sondern ein echter Fehlerfall.
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/json")
_ = json.NewEncoder(w).Encode(resp)
}