CI / Web-Barrierefreiheitspruefung (axe, Tastatur, Reflow, Textabstaende) (pull_request) Canceled after 0s
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 Supply-Chain-Scan (govulncheck) (pull_request) Canceled after 0s
CI / Web (dms-browser) (pull_request) Canceled after 0s
Core-Pflichttest-Gate / pflichttest-gate (pull_request) Canceled after 0s
Supply-Chain-Scan (npm) / npm-audit (push) Canceled after 0s
CI / Core-Modul (Build/Vet/Test) (pull_request) Canceled after 0s
CI / DMS Security-Test-Gate (AC5, Merge-Gate) (pull_request) 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-Rollenhierarchie-Pflichtgate / rbac-policy-invariant (pull_request) Canceled after 0s
CI / Web (pdf-viewer) (pull_request) Canceled after 0s
CI / Web (shl) (pull_request) Canceled after 0s
Core-Schnittstellen-Vertragstests / contract-tests (pull_request) Canceled after 0s
Supply-Chain-Scan (Go) / govulncheck (pull_request) Canceled after 0s
Supply-Chain-Scan (npm) / npm-audit (pull_request) Canceled after 0s
CI / DMS-Modul (Lint/Build/Test/Coverage/Artefakt) (pull_request) Canceled after 0s
CI / DMS-Modul (Lint/Build/Test/Coverage/Artefakt) (push) Canceled after 0s
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
228 lines
7.1 KiB
Go
228 lines
7.1 KiB
Go
// Package routingruleapi ist die HTTP-Bindung fuer den Routing-Regel-Editor
|
|
// (RTE-03). Nutzeridentitaet uebergangsweise ueber X-User-Id, gleiches
|
|
// Muster wie approvalapi (APR-01) vor der Core-JWT-Anbindung.
|
|
package routingruleapi
|
|
|
|
import (
|
|
"encoding/json"
|
|
"errors"
|
|
"net/http"
|
|
|
|
"gitea.perlbach24.de/scripte/nexarch/workflow/internal/routingrule"
|
|
)
|
|
|
|
var allowedOperators = map[string]bool{"equals": true, "contains": true, "in": true}
|
|
var allowedActionTypes = map[string]bool{"route_to_process": true, "route_to_person": true}
|
|
|
|
type Server struct {
|
|
store *routingrule.Store
|
|
}
|
|
|
|
func NewServer(store *routingrule.Store) *Server {
|
|
return &Server{store: store}
|
|
}
|
|
|
|
func (s *Server) RegisterRoutes(mux *http.ServeMux) {
|
|
mux.HandleFunc("GET /routing-rules", s.requireUser(s.handleList))
|
|
mux.HandleFunc("POST /routing-rules", s.requireUser(s.handleCreate))
|
|
mux.HandleFunc("PUT /routing-rules/{id}", s.requireUser(s.handleUpdate))
|
|
mux.HandleFunc("PUT /routing-rules/{id}/priority", s.requireUser(s.handleSetPriority))
|
|
mux.HandleFunc("PUT /routing-rules/{id}/enabled", s.requireUser(s.handleSetEnabled))
|
|
mux.HandleFunc("POST /routing-rules/test", s.requireUser(s.handleTest))
|
|
}
|
|
|
|
func (s *Server) requireUser(next func(w http.ResponseWriter, r *http.Request, userID string)) http.HandlerFunc {
|
|
return func(w http.ResponseWriter, r *http.Request) {
|
|
userID := r.Header.Get("X-User-Id")
|
|
if userID == "" {
|
|
writeError(w, http.StatusUnauthorized, "X-User-Id fehlt")
|
|
return
|
|
}
|
|
next(w, r, userID)
|
|
}
|
|
}
|
|
|
|
func (s *Server) handleList(w http.ResponseWriter, r *http.Request, _ string) {
|
|
rules, err := s.store.ListAll(r.Context())
|
|
if err != nil {
|
|
writeError(w, http.StatusInternalServerError, "regeln konnten nicht geladen werden")
|
|
return
|
|
}
|
|
writeJSON(w, http.StatusOK, rules)
|
|
}
|
|
|
|
type ruleInput struct {
|
|
Name string `json:"name"`
|
|
Priority int `json:"priority"`
|
|
Conditions []routingrule.Condition `json:"conditions"`
|
|
Actions []routingrule.Action `json:"actions"`
|
|
}
|
|
|
|
// validate lehnt strukturell ungueltige Bedingungen/Aktionen ab, bevor sie
|
|
// gespeichert werden (Pruefung 3: ungueltige Bedingungseingabe wird
|
|
// abgewiesen). Der Store selbst validiert das nicht (RTE-01-Scope), daher
|
|
// hier auf API-Ebene statt den bestehenden Store-Code zu veraendern.
|
|
func validate(in ruleInput) error {
|
|
if in.Name == "" {
|
|
return errors.New("name ist pflicht")
|
|
}
|
|
for _, c := range in.Conditions {
|
|
if c.Field == "" {
|
|
return errors.New("bedingung: feld ist pflicht")
|
|
}
|
|
if !allowedOperators[c.Operator] {
|
|
return errors.New("bedingung: unbekannter operator " + c.Operator)
|
|
}
|
|
if c.Value == nil {
|
|
return errors.New("bedingung: wert ist pflicht")
|
|
}
|
|
}
|
|
for _, a := range in.Actions {
|
|
if !allowedActionTypes[a.Type] {
|
|
return errors.New("aktion: unbekannter typ " + a.Type)
|
|
}
|
|
if a.Target == "" {
|
|
return errors.New("aktion: ziel ist pflicht")
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (s *Server) handleCreate(w http.ResponseWriter, r *http.Request, _ string) {
|
|
var in ruleInput
|
|
if err := json.NewDecoder(r.Body).Decode(&in); err != nil {
|
|
writeError(w, http.StatusBadRequest, "ungueltige anfrage")
|
|
return
|
|
}
|
|
if err := validate(in); err != nil {
|
|
writeError(w, http.StatusBadRequest, err.Error())
|
|
return
|
|
}
|
|
rule, err := s.store.Create(r.Context(), in.Name, in.Priority, in.Conditions, in.Actions)
|
|
if err != nil {
|
|
writeError(w, http.StatusInternalServerError, "regel konnte nicht angelegt werden")
|
|
return
|
|
}
|
|
writeJSON(w, http.StatusCreated, rule)
|
|
}
|
|
|
|
func (s *Server) handleUpdate(w http.ResponseWriter, r *http.Request, _ string) {
|
|
id := r.PathValue("id")
|
|
var in ruleInput
|
|
if err := json.NewDecoder(r.Body).Decode(&in); err != nil {
|
|
writeError(w, http.StatusBadRequest, "ungueltige anfrage")
|
|
return
|
|
}
|
|
if err := validate(in); err != nil {
|
|
writeError(w, http.StatusBadRequest, err.Error())
|
|
return
|
|
}
|
|
rule, err := s.store.Update(r.Context(), id, in.Conditions, in.Actions)
|
|
if err != nil {
|
|
if errors.Is(err, routingrule.ErrNotFound) {
|
|
writeError(w, http.StatusNotFound, "regel nicht gefunden")
|
|
return
|
|
}
|
|
writeError(w, http.StatusInternalServerError, "regel konnte nicht aktualisiert werden")
|
|
return
|
|
}
|
|
writeJSON(w, http.StatusOK, rule)
|
|
}
|
|
|
|
type priorityInput struct {
|
|
Priority int `json:"priority"`
|
|
}
|
|
|
|
func (s *Server) handleSetPriority(w http.ResponseWriter, r *http.Request, _ string) {
|
|
id := r.PathValue("id")
|
|
var in priorityInput
|
|
if err := json.NewDecoder(r.Body).Decode(&in); err != nil {
|
|
writeError(w, http.StatusBadRequest, "ungueltige anfrage")
|
|
return
|
|
}
|
|
if err := s.store.SetPriority(r.Context(), id, in.Priority); err != nil {
|
|
if errors.Is(err, routingrule.ErrNotFound) {
|
|
writeError(w, http.StatusNotFound, "regel nicht gefunden")
|
|
return
|
|
}
|
|
writeError(w, http.StatusInternalServerError, "prioritaet konnte nicht gesetzt werden")
|
|
return
|
|
}
|
|
w.WriteHeader(http.StatusNoContent)
|
|
}
|
|
|
|
type enabledInput struct {
|
|
Enabled bool `json:"enabled"`
|
|
}
|
|
|
|
func (s *Server) handleSetEnabled(w http.ResponseWriter, r *http.Request, _ string) {
|
|
id := r.PathValue("id")
|
|
var in enabledInput
|
|
if err := json.NewDecoder(r.Body).Decode(&in); err != nil {
|
|
writeError(w, http.StatusBadRequest, "ungueltige anfrage")
|
|
return
|
|
}
|
|
if err := s.store.SetEnabled(r.Context(), id, in.Enabled); err != nil {
|
|
if errors.Is(err, routingrule.ErrNotFound) {
|
|
writeError(w, http.StatusNotFound, "regel nicht gefunden")
|
|
return
|
|
}
|
|
writeError(w, http.StatusInternalServerError, "status konnte nicht gesetzt werden")
|
|
return
|
|
}
|
|
w.WriteHeader(http.StatusNoContent)
|
|
}
|
|
|
|
type testInput struct {
|
|
Object map[string]any `json:"object"`
|
|
}
|
|
|
|
type testResult struct {
|
|
Matched bool `json:"matched"`
|
|
RuleID string `json:"rule_id,omitempty"`
|
|
RuleName string `json:"rule_name,omitempty"`
|
|
Actions []routingrule.Action `json:"actions,omitempty"`
|
|
}
|
|
|
|
// handleTest fuehrt eine Regel probeweise gegen ein Beispielobjekt aus
|
|
// (Pruefung 2: Testlauf vor Aktivierung), ohne eine echte Ausfuehrung
|
|
// (RTE-02, routing_decisions) anzustossen — reiner Lesevorgang.
|
|
func (s *Server) handleTest(w http.ResponseWriter, r *http.Request, _ string) {
|
|
var in testInput
|
|
if err := json.NewDecoder(r.Body).Decode(&in); err != nil {
|
|
writeError(w, http.StatusBadRequest, "ungueltige anfrage")
|
|
return
|
|
}
|
|
if in.Object == nil {
|
|
writeError(w, http.StatusBadRequest, "object ist pflicht")
|
|
return
|
|
}
|
|
rules, err := s.store.ListAll(r.Context())
|
|
if err != nil {
|
|
writeError(w, http.StatusInternalServerError, "regeln konnten nicht geladen werden")
|
|
return
|
|
}
|
|
// ListAll sortiert bereits nach Prioritaet absteigend; erste passende
|
|
// (aktivierte) Regel gewinnt (RTE-02-Prinzip), hier nur simuliert.
|
|
for _, rule := range rules {
|
|
if !rule.Enabled {
|
|
continue
|
|
}
|
|
if rule.Matches(in.Object) {
|
|
writeJSON(w, http.StatusOK, testResult{Matched: true, RuleID: rule.ID, RuleName: rule.Name, Actions: rule.Actions})
|
|
return
|
|
}
|
|
}
|
|
writeJSON(w, http.StatusOK, testResult{Matched: false})
|
|
}
|
|
|
|
func writeJSON(w http.ResponseWriter, status int, v any) {
|
|
w.Header().Set("Content-Type", "application/json")
|
|
w.WriteHeader(status)
|
|
_ = json.NewEncoder(w).Encode(v)
|
|
}
|
|
|
|
func writeError(w http.ResponseWriter, status int, msg string) {
|
|
writeJSON(w, status, map[string]string{"error": msg})
|
|
}
|