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
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>
102 lines
3.2 KiB
Go
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)
|
|
}
|