workflow: svcauth, tenantregistryclient aus common v0.1.0 #389

Merged
patrick merged 1 commits from refactor/workflow-common into main 2026-10-28 10:41:46 +01:00
15 changed files with 36 additions and 102 deletions

No files matched your search

+1 -1
View File
@@ -16,6 +16,7 @@ import (
"github.com/jackc/pgx/v5/pgxpool"
"gitea.perlbach24.de/scripte/nexarch/common/tenantregistryclient"
"gitea.perlbach24.de/scripte/nexarch/workflow/internal/approval"
"gitea.perlbach24.de/scripte/nexarch/workflow/internal/approvalapi"
"gitea.perlbach24.de/scripte/nexarch/workflow/internal/backchannel"
@@ -33,7 +34,6 @@ import (
"gitea.perlbach24.de/scripte/nexarch/workflow/internal/routingruleapi"
"gitea.perlbach24.de/scripte/nexarch/workflow/internal/routingsuggestclient"
"gitea.perlbach24.de/scripte/nexarch/workflow/internal/task"
"gitea.perlbach24.de/scripte/nexarch/workflow/internal/tenantregistryclient"
)
func requireEnv(name string) string {
+4 -1
View File
@@ -2,7 +2,10 @@ module gitea.perlbach24.de/scripte/nexarch/workflow
go 1.25.0
require github.com/jackc/pgx/v5 v5.9.2
require (
gitea.perlbach24.de/scripte/nexarch/common v0.1.0
github.com/jackc/pgx/v5 v5.9.2
)
require (
github.com/jackc/pgpassfile v1.0.0 // indirect
+2
View File
@@ -1,3 +1,5 @@
gitea.perlbach24.de/scripte/nexarch/common v0.1.0 h1:biNzFr2LOMZC7b1leY3smQtrMmSMp6IW80OvR4LSjLY=
gitea.perlbach24.de/scripte/nexarch/common v0.1.0/go.mod h1:CiEImG++QMt1xZqSMeDJhPiB7sfWTLZOwjS6OY/Iguc=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
@@ -302,4 +302,3 @@ func TestForObject_RegistryTransportError_FailsClosed(t *testing.T) {
t.Fatalf("erwartet ErrForbidden bei registry-transportfehler (fail-closed), bekommen %v", err)
}
}
@@ -35,10 +35,10 @@ type Detail struct {
type KanbanColumn string
const (
ColumnOpen KanbanColumn = "open"
ColumnThisWeek KanbanColumn = "this_week"
ColumnOverdue KanbanColumn = "overdue"
ColumnDone KanbanColumn = "done"
ColumnOpen KanbanColumn = "open"
ColumnThisWeek KanbanColumn = "this_week"
ColumnOverdue KanbanColumn = "overdue"
ColumnDone KanbanColumn = "done"
)
// KanbanItem ist eine Wiedervorlage mitsamt der Spalte, in der sie gerade
@@ -5,8 +5,8 @@ import (
"net/http"
"net/http/httptest"
"os"
"strings"
"path/filepath"
"strings"
"testing"
"time"
@@ -174,13 +174,13 @@ func (d *Dispatcher) Dispatch(ctx context.Context) (int, error) {
// dasselbe Ereignis reserviert nicht erneut und legt daher auch
// keine zweite Aufgabe an (Akzeptanzkriterium 2).
// 2. Aufgabe/Genehmigung wird angelegt.
// 3a. Erfolg: die Reservierung wird mit der erzeugten Objekt-ID
// bestaetigt und module_events als verarbeitet markiert.
// 3b. Fehlschlag: die Reservierung wird wieder geloescht, damit ein
// spaeterer Dispatch-Lauf es erneut versuchen kann -- das
// verbleibende Restrisiko ist ein Absturz exakt zwischen 2 und 3a,
// das Ereignis bliebe dann dauerhaft reserviert ohne erzeugtes
// Objekt (dokumentiert in ENG-09-PRUEFPROTOKOLL.md).
// 3a. Erfolg: die Reservierung wird mit der erzeugten Objekt-ID
// bestaetigt und module_events als verarbeitet markiert.
// 3b. Fehlschlag: die Reservierung wird wieder geloescht, damit ein
// spaeterer Dispatch-Lauf es erneut versuchen kann -- das
// verbleibende Restrisiko ist ein Absturz exakt zwischen 2 und 3a,
// das Ereignis bliebe dann dauerhaft reserviert ohne erzeugtes
// Objekt (dokumentiert in ENG-09-PRUEFPROTOKOLL.md).
func (d *Dispatcher) dispatchOne(ctx context.Context, e pendingEvent) (bool, error) {
rule, ok, err := d.rules.ruleFor(ctx, d.pool, e.ModuleName, e.ObjectType, e.Trigger)
if err != nil {
+1 -1
View File
@@ -10,7 +10,7 @@ import (
"github.com/jackc/pgx/v5/pgxpool"
"gitea.perlbach24.de/scripte/nexarch/workflow/internal/svcauth"
"gitea.perlbach24.de/scripte/nexarch/common/svcauth"
)
// TenantVerifier prueft einen vom aufrufenden Modul mitgelieferten
@@ -17,9 +17,7 @@ import (
// sensitivePathPatterns beschreibt die Bereiche aus
// workflow/docs/TESTSTRATEGIE.md Abschnitt 4.
var sensitivePathPatterns = []*regexp.Regexp{
regexp.MustCompile(`(^|/)workflow/internal/svcauth/`),
regexp.MustCompile(`(^|/)workflow/internal/moduleadapter/`),
regexp.MustCompile(`(^|/)workflow/internal/tenantregistryclient/`),
regexp.MustCompile(`(^|/)workflow/internal/backchannel/`),
regexp.MustCompile(`(^|/)workflow/internal/backchannelapi/`),
regexp.MustCompile(`(^|/)workflow/internal/dualcheck/`),
@@ -1,24 +1,24 @@
// Negativtest des Gates selbst (QA-01 Pruefung 1): ein Diff mit
// geaenderter workflow/internal/svcauth/svcauth.go ohne begleitende
// geaenderter workflow/internal/moduleadapter/moduleadapter.go ohne begleitende
// Testdatei muss als Verstoss erkannt werden.
package pflichttestgate
import "testing"
func TestCheckDiff_FlagsSensitiveChangeWithoutTest(t *testing.T) {
violations := CheckDiff([]string{"workflow/internal/svcauth/svcauth.go"})
violations := CheckDiff([]string{"workflow/internal/moduleadapter/moduleadapter.go"})
if len(violations) != 1 {
t.Fatalf("erwartet genau 1 verstoss, habe %d: %+v", len(violations), violations)
}
if violations[0].Package != "workflow/internal/svcauth" {
if violations[0].Package != "workflow/internal/moduleadapter" {
t.Fatalf("falsches package gemeldet: %+v", violations[0])
}
}
func TestCheckDiff_PassesWhenTestFileAccompanies(t *testing.T) {
violations := CheckDiff([]string{
"workflow/internal/svcauth/svcauth.go",
"workflow/internal/svcauth/svcauth_test.go",
"workflow/internal/moduleadapter/moduleadapter.go",
"workflow/internal/moduleadapter/moduleadapter_test.go",
})
if len(violations) != 0 {
t.Fatalf("erwartet keine verstoesse, habe: %+v", violations)
@@ -34,9 +34,7 @@ func TestCheckDiff_IgnoresNonSensitivePaths(t *testing.T) {
func TestCheckDiff_CoversAllSensitivePackages(t *testing.T) {
files := []string{
"workflow/internal/svcauth/x.go",
"workflow/internal/moduleadapter/x.go",
"workflow/internal/tenantregistryclient/x.go",
"workflow/internal/backchannel/x.go",
"workflow/internal/backchannelapi/x.go",
"workflow/internal/dualcheck/x.go",
@@ -19,12 +19,12 @@ import (
// Decision ist das Ergebnis einer Ausfuehrung: entweder eine passende Regel
// mit ihrer Aktion, oder keine Regel (Klaerungskorb).
type Decision struct {
Matched bool
RuleID string
RuleName string
Action routingrule.Action
InBasket bool
BasketID string
Matched bool
RuleID string
RuleName string
Action routingrule.Action
InBasket bool
BasketID string
}
type Engine struct {
+2 -2
View File
@@ -64,8 +64,8 @@ func (s *Server) handleList(w http.ResponseWriter, r *http.Request, _ string) {
}
type ruleInput struct {
Name string `json:"name"`
Priority int `json:"priority"`
Name string `json:"name"`
Priority int `json:"priority"`
Conditions []routingrule.Condition `json:"conditions"`
Actions []routingrule.Action `json:"actions"`
}
+3 -3
View File
@@ -13,9 +13,9 @@ import (
// TestCase ist ein Objekt aus dem Testkorpus, optional mit einer erwarteten
// Regel-ID (leer = "keine Regel soll greifen").
type TestCase struct {
Label string
Object map[string]any
ExpectedRuleID string
Label string
Object map[string]any
ExpectedRuleID string
}
// Result ist das Simulationsergebnis fuer einen TestCase: tatsaechliche
-19
View File
@@ -1,19 +0,0 @@
// Package svcauth vergleicht Service-Token in konstanter Zeit (Muster aus
// archive/internal/svcauth, Lehre aus RET-14/F-08: ohne diesen Schutz
// beim ersten Entwurf des Adapters einbauen statt nachzuruesten).
package svcauth
import (
"crypto/sha256"
"crypto/subtle"
)
// Equal vergleicht die SHA-256-Werte beider Eingaben in konstanter Zeit.
// Ein leeres erwartetes Token stimmt nie.
func Equal(got, want string) bool {
if want == "" {
return false
}
g, w := sha256.Sum256([]byte(got)), sha256.Sum256([]byte(want))
return subtle.ConstantTimeCompare(g[:], w[:]) == 1
}
@@ -1,47 +0,0 @@
// Package tenantregistryclient ist ein schlanker HTTP-Client fuer Core TEN-01
// (Tenant-Registry); Workflow ist ein eigenes Go-Modul und kann Cores Pakete
// nicht importieren. Gleiches Muster wie dms/internal/tenantregistryclient
// und archive/internal/tenantregistryclient.
package tenantregistryclient
import (
"context"
"fmt"
"net/http"
"net/url"
"time"
)
type Client struct {
BaseURL string
ServiceToken string
HTTPClient *http.Client
}
func New(baseURL, serviceToken string) *Client {
return &Client{BaseURL: baseURL, ServiceToken: serviceToken, HTTPClient: &http.Client{Timeout: 10 * time.Second}}
}
// VerifyTenant fragt Core, ob tenantSlug als Tenant gefuehrt wird. Transport-
// und Serverfehler kommen als Fehler zurueck, nie als "gueltig": der Aufrufer
// muss fail-closed reagieren.
func (c *Client) VerifyTenant(ctx context.Context, tenantSlug string) (bool, error) {
req, err := http.NewRequestWithContext(ctx, http.MethodGet, c.BaseURL+"/tenants/"+url.PathEscape(tenantSlug), nil)
if err != nil {
return false, fmt.Errorf("tenantregistryclient: request bauen: %w", err)
}
req.Header.Set("X-Service-Token", c.ServiceToken)
resp, err := c.HTTPClient.Do(req)
if err != nil {
return false, fmt.Errorf("tenantregistryclient: aufruf fehlgeschlagen: %w", err)
}
defer func() { _ = resp.Body.Close() }()
switch resp.StatusCode {
case http.StatusOK:
return true, nil
case http.StatusNotFound:
return false, nil
default:
return false, fmt.Errorf("tenantregistryclient: unerwarteter status %d", resp.StatusCode)
}
}