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