Merge pull request 'connect: auditclient, httpclient, policyclient aus common v0.1.0' (#388) from refactor/connect-common into main
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 / Connect Security-Test-Gate (QA-01 AC4, 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
Connect-Schnittstellen-Vertragstests / contract-tests (push) Canceled after 0s
Supply-Chain-Scan (Go) / govulncheck (push) 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 / Connect Security-Test-Gate (QA-01 AC4, 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
Connect-Schnittstellen-Vertragstests / contract-tests (push) Canceled after 0s
Supply-Chain-Scan (Go) / govulncheck (push) Canceled after 0s
This commit was merged in pull request #388.
This commit is contained in:
commit
95582ad09d
33 files changed
+61
-399
No files matched your search
+5
-2
@@ -4,12 +4,15 @@ go 1.25.0
|
||||
|
||||
toolchain go1.25.13
|
||||
|
||||
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
|
||||
golang.org/x/text v0.29.0
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/jackc/pgpassfile v1.0.0 // indirect
|
||||
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
|
||||
github.com/jackc/puddle/v2 v2.2.2 // indirect
|
||||
golang.org/x/sync v0.17.0 // indirect
|
||||
golang.org/x/text v0.29.0 // indirect
|
||||
)
|
||||
@@ -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=
|
||||
|
||||
@@ -12,7 +12,7 @@ import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"gitea.perlbach24.de/scripte/nexarch/connect/internal/httpclient"
|
||||
"gitea.perlbach24.de/scripte/nexarch/common/httpclient"
|
||||
)
|
||||
|
||||
type Client struct {
|
||||
|
||||
@@ -1,71 +0,0 @@
|
||||
// Package auditclient ist ein schlanker HTTP-Client fuer Core AUD-08
|
||||
// (internal/audit, POST /audit/ingest). Connect ist ein physisch
|
||||
// getrenntes Go-Modul und kann Cores internal/audit daher nicht direkt
|
||||
// importieren — nur ueber dessen HTTP-Schnittstelle. Gleiches Muster wie
|
||||
// archive/internal/auditclient.
|
||||
package auditclient
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"gitea.perlbach24.de/scripte/nexarch/connect/internal/httpclient"
|
||||
)
|
||||
|
||||
type Client struct {
|
||||
BaseURL string
|
||||
ServiceToken string
|
||||
HTTPClient *http.Client
|
||||
}
|
||||
|
||||
func New(baseURL, serviceToken string) *Client {
|
||||
return &Client{BaseURL: baseURL, ServiceToken: serviceToken, HTTPClient: httpclient.Default()}
|
||||
}
|
||||
|
||||
type ingestRequest struct {
|
||||
TenantSlug string `json:"tenant_slug"`
|
||||
Actor string `json:"actor"`
|
||||
Action string `json:"action"`
|
||||
Target string `json:"target"`
|
||||
Metadata map[string]any `json:"metadata"`
|
||||
}
|
||||
|
||||
// Ingest meldet EIN Audit-Ereignis an Core. Jeder Fehler (Transport,
|
||||
// Timeout, unerwarteter Status) wird als Fehler zurueckgegeben — der
|
||||
// Aufrufer entscheidet, ob/wie er das nachbehandelt (siehe
|
||||
// connect/internal/audittrail fuer den ausfalltoleranten Wrapper).
|
||||
func (c *Client) Ingest(ctx context.Context, tenantSlug, actor, action, target string, metadata map[string]any) error {
|
||||
body, err := json.Marshal(ingestRequest{
|
||||
TenantSlug: tenantSlug, Actor: actor, Action: action, Target: target, Metadata: metadata,
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("auditclient: request kodieren: %w", err)
|
||||
}
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodPost, c.BaseURL+"/audit/ingest", bytes.NewReader(body))
|
||||
if err != nil {
|
||||
return fmt.Errorf("auditclient: request bauen: %w", err)
|
||||
}
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
req.Header.Set("X-Service-Token", c.ServiceToken)
|
||||
|
||||
resp, err := c.httpClient().Do(req)
|
||||
if err != nil {
|
||||
return fmt.Errorf("auditclient: aufruf fehlgeschlagen: %w", err)
|
||||
}
|
||||
defer func() { _ = resp.Body.Close() }()
|
||||
|
||||
if resp.StatusCode != http.StatusAccepted {
|
||||
return fmt.Errorf("auditclient: unerwarteter status %d", resp.StatusCode)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Client) httpClient() *http.Client {
|
||||
if c.HTTPClient != nil {
|
||||
return c.HTTPClient
|
||||
}
|
||||
return httpclient.Default()
|
||||
}
|
||||
@@ -1,57 +0,0 @@
|
||||
package auditclient
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestIngest_SendsTenantSlugAndServiceToken(t *testing.T) {
|
||||
var gotBody ingestRequest
|
||||
var gotToken string
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
gotToken = r.Header.Get("X-Service-Token")
|
||||
if err := json.NewDecoder(r.Body).Decode(&gotBody); err != nil {
|
||||
t.Fatalf("body dekodieren: %v", err)
|
||||
}
|
||||
w.WriteHeader(http.StatusAccepted)
|
||||
}))
|
||||
defer srv.Close()
|
||||
|
||||
c := New(srv.URL, "svc-token-123")
|
||||
err := c.Ingest(context.Background(), "acme", "user-1", "connector.sync", "erp:kunde:42", map[string]any{"count": 3})
|
||||
if err != nil {
|
||||
t.Fatalf("Ingest: %v", err)
|
||||
}
|
||||
|
||||
if gotToken != "svc-token-123" {
|
||||
t.Fatalf("Service-Token nicht gesendet, erhalten: %q", gotToken)
|
||||
}
|
||||
if gotBody.TenantSlug != "acme" {
|
||||
t.Fatalf("TenantSlug fehlt/falsch im Audit-Ereignis: %q", gotBody.TenantSlug)
|
||||
}
|
||||
if gotBody.Action != "connector.sync" {
|
||||
t.Fatalf("Action falsch: %q", gotBody.Action)
|
||||
}
|
||||
}
|
||||
|
||||
func TestIngest_UnexpectedStatus_ReturnsError(t *testing.T) {
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
}))
|
||||
defer srv.Close()
|
||||
|
||||
c := New(srv.URL, "svc-token-123")
|
||||
if err := c.Ingest(context.Background(), "acme", "user-1", "connector.sync", "erp:kunde:42", nil); err == nil {
|
||||
t.Fatal("erwarteter Fehler bei Status 500 blieb aus")
|
||||
}
|
||||
}
|
||||
|
||||
func TestIngest_TransportError_ReturnsError(t *testing.T) {
|
||||
c := New("http://127.0.0.1:1", "svc-token-123")
|
||||
if err := c.Ingest(context.Background(), "acme", "user-1", "connector.sync", "erp:kunde:42", nil); err == nil {
|
||||
t.Fatal("erwarteter Transport-Fehler blieb aus")
|
||||
}
|
||||
}
|
||||
@@ -19,7 +19,7 @@ import (
|
||||
|
||||
"github.com/jackc/pgx/v5/pgxpool"
|
||||
|
||||
"gitea.perlbach24.de/scripte/nexarch/connect/internal/auditclient"
|
||||
"gitea.perlbach24.de/scripte/nexarch/common/auditclient"
|
||||
)
|
||||
|
||||
type Recorder struct {
|
||||
|
||||
@@ -11,7 +11,7 @@ import (
|
||||
|
||||
"github.com/jackc/pgx/v5/pgxpool"
|
||||
|
||||
"gitea.perlbach24.de/scripte/nexarch/connect/internal/auditclient"
|
||||
"gitea.perlbach24.de/scripte/nexarch/common/auditclient"
|
||||
)
|
||||
|
||||
func setupOutboxTest(t *testing.T) *pgxpool.Pool {
|
||||
|
||||
@@ -26,8 +26,8 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"gitea.perlbach24.de/scripte/nexarch/common/httpclient"
|
||||
"gitea.perlbach24.de/scripte/nexarch/connect/internal/crypto"
|
||||
"gitea.perlbach24.de/scripte/nexarch/connect/internal/httpclient"
|
||||
"gitea.perlbach24.de/scripte/nexarch/connect/internal/registry"
|
||||
)
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@ import (
|
||||
|
||||
"github.com/jackc/pgx/v5/pgxpool"
|
||||
|
||||
"gitea.perlbach24.de/scripte/nexarch/connect/internal/auditclient"
|
||||
"gitea.perlbach24.de/scripte/nexarch/common/auditclient"
|
||||
"gitea.perlbach24.de/scripte/nexarch/connect/internal/audittrail"
|
||||
"gitea.perlbach24.de/scripte/nexarch/connect/internal/credential"
|
||||
connectcrypto "gitea.perlbach24.de/scripte/nexarch/connect/internal/crypto"
|
||||
|
||||
@@ -14,8 +14,8 @@ import (
|
||||
"io"
|
||||
"net/http"
|
||||
|
||||
"gitea.perlbach24.de/scripte/nexarch/common/httpclient"
|
||||
"gitea.perlbach24.de/scripte/nexarch/connect/internal/credential"
|
||||
"gitea.perlbach24.de/scripte/nexarch/connect/internal/httpclient"
|
||||
"gitea.perlbach24.de/scripte/nexarch/connect/internal/registry"
|
||||
)
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@ import (
|
||||
"net/http"
|
||||
"net/url"
|
||||
|
||||
"gitea.perlbach24.de/scripte/nexarch/connect/internal/httpclient"
|
||||
"gitea.perlbach24.de/scripte/nexarch/common/httpclient"
|
||||
)
|
||||
|
||||
// tenantKEKResponse mirrors Core internal/kek.tenantKEKResponse (JSON
|
||||
|
||||
@@ -54,14 +54,14 @@ type triggerRequest struct {
|
||||
}
|
||||
|
||||
type runResponse struct {
|
||||
ID string `json:"id"`
|
||||
PeriodStart string `json:"period_start"`
|
||||
PeriodEnd string `json:"period_end"`
|
||||
EntryCount int `json:"entry_count"`
|
||||
RetentionClass string `json:"retention_class"`
|
||||
CreatedAt string `json:"created_at"`
|
||||
TransferStatus string `json:"transfer_status,omitempty"`
|
||||
TransferError string `json:"transfer_error,omitempty"`
|
||||
ID string `json:"id"`
|
||||
PeriodStart string `json:"period_start"`
|
||||
PeriodEnd string `json:"period_end"`
|
||||
EntryCount int `json:"entry_count"`
|
||||
RetentionClass string `json:"retention_class"`
|
||||
CreatedAt string `json:"created_at"`
|
||||
TransferStatus string `json:"transfer_status,omitempty"`
|
||||
TransferError string `json:"transfer_error,omitempty"`
|
||||
}
|
||||
|
||||
func (s *Server) handleTrigger(w http.ResponseWriter, r *http.Request, tenantSlug string) {
|
||||
|
||||
@@ -34,13 +34,13 @@ var (
|
||||
// (a deliberate subset, not the full ~116-column spec — DAT-02/DAT-03
|
||||
// extend format coverage, not this ticket).
|
||||
type Entry struct {
|
||||
Amount float64 // Umsatz (ohne Soll/Haben-Kennzeichen), immer positiv
|
||||
DebitCredit string // "S" (Soll) oder "H" (Haben)
|
||||
Account string // Konto
|
||||
CounterAccount string // Gegenkonto (ohne BU-Schlüssel)
|
||||
Date time.Time // Belegdatum
|
||||
Text string // Buchungstext
|
||||
DocumentRef string // Belegfeld 1 (Rechnungs-/Belegnummer)
|
||||
Amount float64 // Umsatz (ohne Soll/Haben-Kennzeichen), immer positiv
|
||||
DebitCredit string // "S" (Soll) oder "H" (Haben)
|
||||
Account string // Konto
|
||||
CounterAccount string // Gegenkonto (ohne BU-Schlüssel)
|
||||
Date time.Time // Belegdatum
|
||||
Text string // Buchungstext
|
||||
DocumentRef string // Belegfeld 1 (Rechnungs-/Belegnummer)
|
||||
}
|
||||
|
||||
func (e Entry) validate() error {
|
||||
|
||||
@@ -89,7 +89,7 @@ func TestExport_StructuralFormatCheck(t *testing.T) {
|
||||
// AC2: fehlendes Pflichtfeld im Beleg führt zur klaren Abweisung.
|
||||
func TestExport_MissingMandatoryField_Rejected(t *testing.T) {
|
||||
cases := []struct {
|
||||
name string
|
||||
name string
|
||||
break_ func(e Entry) Entry
|
||||
}{
|
||||
{"Umsatz", func(e Entry) Entry { e.Amount = 0; return e }},
|
||||
|
||||
@@ -30,8 +30,8 @@ type Run struct {
|
||||
}
|
||||
|
||||
type Store struct {
|
||||
Pool *pgxpool.Pool
|
||||
Archive *archiveclient.Client
|
||||
Pool *pgxpool.Pool
|
||||
Archive *archiveclient.Client
|
||||
// CallbackURL is where Archive would call back on deletion — Connect
|
||||
// has no export-deletion path yet (DAT-02/03), so this points at a
|
||||
// stub endpoint that always 501s, same pattern as other modules'
|
||||
|
||||
@@ -161,4 +161,3 @@ func countRuns(t *testing.T, pool *pgxpool.Pool, tenantSlug string) int {
|
||||
}
|
||||
return n
|
||||
}
|
||||
|
||||
@@ -34,7 +34,7 @@ func TestApplyMapping_UsesGivenMappingNotSuggestion(t *testing.T) {
|
||||
doc := SourceDocument{
|
||||
PK: "1",
|
||||
RawFields: map[string]string{
|
||||
"title": "Originaltitel",
|
||||
"title": "Originaltitel",
|
||||
"custom_title_field": "Korrigierter Titel",
|
||||
},
|
||||
}
|
||||
|
||||
@@ -241,4 +241,3 @@ func DetectFields(docs []SourceDocument) []string {
|
||||
}
|
||||
return order
|
||||
}
|
||||
|
||||
@@ -19,9 +19,9 @@ import (
|
||||
// assistant in fieldmapping.Store — one mapping per tenant, since a
|
||||
// tenant realistically migrates from one third-party system at a time.
|
||||
const (
|
||||
connectorName = "dms2_import"
|
||||
connectorInstanceID = "paperless_ngx"
|
||||
maxUploadBytes int64 = 200 << 20 // 200 MiB: generous for a test/migration export, not unbounded
|
||||
connectorName = "dms2_import"
|
||||
connectorInstanceID = "paperless_ngx"
|
||||
maxUploadBytes int64 = 200 << 20 // 200 MiB: generous for a test/migration export, not unbounded
|
||||
)
|
||||
|
||||
type Server struct {
|
||||
@@ -55,9 +55,9 @@ func (s *Server) requireIdentity(next func(w http.ResponseWriter, r *http.Reques
|
||||
}
|
||||
|
||||
type previewResponse struct {
|
||||
DocumentCount int `json:"document_count"`
|
||||
AvailableFields []string `json:"available_fields"`
|
||||
SuggestedMapping []fieldmapping.Entry `json:"suggested_mapping"`
|
||||
DocumentCount int `json:"document_count"`
|
||||
AvailableFields []string `json:"available_fields"`
|
||||
SuggestedMapping []fieldmapping.Entry `json:"suggested_mapping"`
|
||||
Schema []fieldmapping.FieldDef `json:"schema"`
|
||||
}
|
||||
|
||||
|
||||
@@ -14,8 +14,8 @@ import (
|
||||
"io"
|
||||
"net/http"
|
||||
|
||||
"gitea.perlbach24.de/scripte/nexarch/common/httpclient"
|
||||
"gitea.perlbach24.de/scripte/nexarch/connect/internal/credential"
|
||||
"gitea.perlbach24.de/scripte/nexarch/connect/internal/httpclient"
|
||||
"gitea.perlbach24.de/scripte/nexarch/connect/internal/registry"
|
||||
)
|
||||
|
||||
|
||||
@@ -49,16 +49,16 @@ type Ref struct {
|
||||
|
||||
// Export is one persisted Beleg export attempt.
|
||||
type Export struct {
|
||||
ID string
|
||||
Ref Ref
|
||||
BelegID string
|
||||
Status Status
|
||||
Attempt int
|
||||
LastError string
|
||||
RetryAt *time.Time
|
||||
ERPRef string
|
||||
CreatedAt time.Time
|
||||
UpdatedAt time.Time
|
||||
ID string
|
||||
Ref Ref
|
||||
BelegID string
|
||||
Status Status
|
||||
Attempt int
|
||||
LastError string
|
||||
RetryAt *time.Time
|
||||
ERPRef string
|
||||
CreatedAt time.Time
|
||||
UpdatedAt time.Time
|
||||
}
|
||||
|
||||
var ErrNotFound = errors.New("erpexport: export nicht gefunden")
|
||||
|
||||
@@ -1,25 +0,0 @@
|
||||
// Package httpclient liefert den HTTP-Client fuer ausgehende Dienstaufrufe
|
||||
// (Muster aus archive/internal/httpclient, QA-15 F-11): Zeitlimit statt
|
||||
// endlosem Warten, keine Weiterleitungen (ein Service-Token wuerde sonst an
|
||||
// das Weiterleitungsziel mitgesendet).
|
||||
package httpclient
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"time"
|
||||
)
|
||||
|
||||
const DefaultTimeout = 15 * time.Second
|
||||
|
||||
var shared = New(DefaultTimeout)
|
||||
|
||||
func Default() *http.Client { return shared }
|
||||
|
||||
func New(timeout time.Duration) *http.Client {
|
||||
return &http.Client{
|
||||
Timeout: timeout,
|
||||
CheckRedirect: func(*http.Request, []*http.Request) error {
|
||||
return http.ErrUseLastResponse
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -1,54 +0,0 @@
|
||||
package httpclient
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestDefaultHasTimeout(t *testing.T) {
|
||||
if Default().Timeout != DefaultTimeout || DefaultTimeout <= 0 {
|
||||
t.Fatalf("timeout = %s", Default().Timeout)
|
||||
}
|
||||
}
|
||||
|
||||
func TestClientGivesUpOnHangingServer(t *testing.T) {
|
||||
release := make(chan struct{})
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { <-release }))
|
||||
defer srv.Close()
|
||||
defer close(release)
|
||||
|
||||
start := time.Now()
|
||||
resp, err := New(200 * time.Millisecond).Get(srv.URL)
|
||||
if err == nil {
|
||||
_ = resp.Body.Close()
|
||||
t.Fatal("kein fehler bei haengendem server")
|
||||
}
|
||||
if elapsed := time.Since(start); elapsed > 5*time.Second {
|
||||
t.Fatalf("aufruf dauerte %s", elapsed)
|
||||
}
|
||||
}
|
||||
|
||||
func TestClientDoesNotFollowRedirects(t *testing.T) {
|
||||
target := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
t.Error("weiterleitungsziel wurde aufgerufen (Service-Token-Leck)")
|
||||
}))
|
||||
defer target.Close()
|
||||
redirector := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
http.Redirect(w, r, target.URL, http.StatusTemporaryRedirect)
|
||||
}))
|
||||
defer redirector.Close()
|
||||
|
||||
req, _ := http.NewRequest(http.MethodPost, redirector.URL, strings.NewReader("x"))
|
||||
req.Header.Set("X-Service-Token", "geheim")
|
||||
resp, err := Default().Do(req)
|
||||
if err != nil {
|
||||
t.Fatalf("do: %v", err)
|
||||
}
|
||||
defer func() { _ = resp.Body.Close() }()
|
||||
if resp.StatusCode != http.StatusTemporaryRedirect {
|
||||
t.Fatalf("status = %d, erwartet 307", resp.StatusCode)
|
||||
}
|
||||
}
|
||||
@@ -18,8 +18,8 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"gitea.perlbach24.de/scripte/nexarch/common/httpclient"
|
||||
"gitea.perlbach24.de/scripte/nexarch/connect/internal/credential"
|
||||
"gitea.perlbach24.de/scripte/nexarch/connect/internal/httpclient"
|
||||
"gitea.perlbach24.de/scripte/nexarch/connect/internal/registry"
|
||||
)
|
||||
|
||||
|
||||
@@ -1,52 +0,0 @@
|
||||
// Package policyclient fragt Core (RBAC-06, POST /authorize) nach der
|
||||
// Rechteentscheidung; Fehler und Ausfall fuehren immer zur Ablehnung.
|
||||
package policyclient
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
type Client struct {
|
||||
BaseURL string
|
||||
ServiceToken string
|
||||
HTTP *http.Client
|
||||
}
|
||||
|
||||
func New(baseURL, serviceToken string) *Client {
|
||||
return &Client{BaseURL: strings.TrimRight(baseURL, "/"), ServiceToken: serviceToken, HTTP: &http.Client{Timeout: 5 * time.Second}}
|
||||
}
|
||||
|
||||
func (c *Client) Allowed(ctx context.Context, tenant, role, permission string) (bool, error) {
|
||||
body := map[string]string{"role": role, "permission": permission}
|
||||
if tenant != "" {
|
||||
body["tenant"] = tenant
|
||||
}
|
||||
payload, _ := json.Marshal(body)
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodPost, c.BaseURL+"/authorize", bytes.NewReader(payload))
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
req.Header.Set("X-Service-Token", c.ServiceToken)
|
||||
resp, err := c.HTTP.Do(req)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return false, fmt.Errorf("policy: status %d", resp.StatusCode)
|
||||
}
|
||||
var out struct {
|
||||
Allowed *bool `json:"allowed"`
|
||||
}
|
||||
if err := json.NewDecoder(http.MaxBytesReader(nil, resp.Body, 1<<16)).Decode(&out); err != nil || out.Allowed == nil {
|
||||
return false, fmt.Errorf("policy: antwort ohne entscheidung")
|
||||
}
|
||||
return *out.Allowed, nil
|
||||
}
|
||||
@@ -1,80 +0,0 @@
|
||||
package policyclient
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestAllowed_TenantScopedRequest_TrueOnAllow(t *testing.T) {
|
||||
var gotBody map[string]string
|
||||
var gotToken string
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
gotToken = r.Header.Get("X-Service-Token")
|
||||
if err := json.NewDecoder(r.Body).Decode(&gotBody); err != nil {
|
||||
t.Fatalf("body dekodieren: %v", err)
|
||||
}
|
||||
json.NewEncoder(w).Encode(map[string]bool{"allowed": true})
|
||||
}))
|
||||
defer srv.Close()
|
||||
|
||||
c := New(srv.URL, "svc-token-123")
|
||||
allowed, err := c.Allowed(context.Background(), "acme", "editor", "connector.write")
|
||||
if err != nil {
|
||||
t.Fatalf("Allowed: %v", err)
|
||||
}
|
||||
if !allowed {
|
||||
t.Fatal("erwartet allowed=true")
|
||||
}
|
||||
if gotToken != "svc-token-123" {
|
||||
t.Fatalf("Service-Token nicht gesendet, erhalten: %q", gotToken)
|
||||
}
|
||||
if gotBody["tenant"] != "acme" {
|
||||
t.Fatalf("Tenant fehlt/falsch in Anfrage: %q", gotBody["tenant"])
|
||||
}
|
||||
}
|
||||
|
||||
func TestAllowed_DeniedResponse_False(t *testing.T) {
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
json.NewEncoder(w).Encode(map[string]bool{"allowed": false})
|
||||
}))
|
||||
defer srv.Close()
|
||||
|
||||
c := New(srv.URL, "svc-token-123")
|
||||
allowed, err := c.Allowed(context.Background(), "acme", "viewer", "connector.write")
|
||||
if err != nil {
|
||||
t.Fatalf("Allowed: %v", err)
|
||||
}
|
||||
if allowed {
|
||||
t.Fatal("erwartet allowed=false")
|
||||
}
|
||||
}
|
||||
|
||||
func TestAllowed_ServiceFailure_DeniesInsteadOfSilentAllow(t *testing.T) {
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
}))
|
||||
defer srv.Close()
|
||||
|
||||
c := New(srv.URL, "svc-token-123")
|
||||
allowed, err := c.Allowed(context.Background(), "acme", "editor", "connector.write")
|
||||
if err == nil {
|
||||
t.Fatal("erwarteter Fehler bei Status 500 blieb aus")
|
||||
}
|
||||
if allowed {
|
||||
t.Fatal("Ausfall darf nie zu allowed=true fuehren")
|
||||
}
|
||||
}
|
||||
|
||||
func TestAllowed_TransportFailure_DeniesInsteadOfSilentAllow(t *testing.T) {
|
||||
c := New("http://127.0.0.1:1", "svc-token-123")
|
||||
allowed, err := c.Allowed(context.Background(), "acme", "editor", "connector.write")
|
||||
if err == nil {
|
||||
t.Fatal("erwarteter Transport-Fehler blieb aus")
|
||||
}
|
||||
if allowed {
|
||||
t.Fatal("Ausfall darf nie zu allowed=true fuehren")
|
||||
}
|
||||
}
|
||||
@@ -11,7 +11,7 @@ import (
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"gitea.perlbach24.de/scripte/nexarch/connect/internal/policyclient"
|
||||
"gitea.perlbach24.de/scripte/nexarch/common/policyclient"
|
||||
"gitea.perlbach24.de/scripte/nexarch/connect/internal/syncjob"
|
||||
)
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ import (
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"gitea.perlbach24.de/scripte/nexarch/connect/internal/policyclient"
|
||||
"gitea.perlbach24.de/scripte/nexarch/common/policyclient"
|
||||
)
|
||||
|
||||
// newFakePolicyAPI steht fuer Core's echten POST /authorize-Vertrag (nimmt
|
||||
|
||||
@@ -9,7 +9,7 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"gitea.perlbach24.de/scripte/nexarch/connect/internal/auditclient"
|
||||
"gitea.perlbach24.de/scripte/nexarch/common/auditclient"
|
||||
"gitea.perlbach24.de/scripte/nexarch/connect/internal/audittrail"
|
||||
"gitea.perlbach24.de/scripte/nexarch/connect/internal/credential"
|
||||
"gitea.perlbach24.de/scripte/nexarch/connect/internal/erpconnector"
|
||||
|
||||
@@ -13,7 +13,7 @@ import (
|
||||
|
||||
"github.com/jackc/pgx/v5/pgxpool"
|
||||
|
||||
"gitea.perlbach24.de/scripte/nexarch/connect/internal/auditclient"
|
||||
"gitea.perlbach24.de/scripte/nexarch/common/auditclient"
|
||||
"gitea.perlbach24.de/scripte/nexarch/connect/internal/audittrail"
|
||||
"gitea.perlbach24.de/scripte/nexarch/connect/internal/migrate"
|
||||
"gitea.perlbach24.de/scripte/nexarch/connect/internal/testdb"
|
||||
|
||||
@@ -85,11 +85,11 @@ func writeCRM(dir string) error {
|
||||
contact := map[string]any{
|
||||
"id": "51023",
|
||||
"properties": map[string]any{
|
||||
"firstname": "Anna",
|
||||
"lastname": "Testperson",
|
||||
"email": "anna.testperson@musterhandel-nordlicht.example",
|
||||
"company": "Musterhandel Nordlicht GmbH",
|
||||
"phone": "+49 431 5550100",
|
||||
"firstname": "Anna",
|
||||
"lastname": "Testperson",
|
||||
"email": "anna.testperson@musterhandel-nordlicht.example",
|
||||
"company": "Musterhandel Nordlicht GmbH",
|
||||
"phone": "+49 431 5550100",
|
||||
"lifecyclestage": "customer",
|
||||
},
|
||||
"createdAt": "2025-11-02T09:14:00Z",
|
||||
|
||||
@@ -15,7 +15,7 @@ import (
|
||||
"net/url"
|
||||
"time"
|
||||
|
||||
"gitea.perlbach24.de/scripte/nexarch/connect/internal/httpclient"
|
||||
"gitea.perlbach24.de/scripte/nexarch/common/httpclient"
|
||||
)
|
||||
|
||||
type Client struct {
|
||||
|
||||
@@ -10,9 +10,7 @@ set -euo pipefail
|
||||
SECURITY_PACKAGES=(
|
||||
"internal/crypto"
|
||||
"internal/credential"
|
||||
"internal/auditclient"
|
||||
"internal/audittrail"
|
||||
"internal/policyclient"
|
||||
"internal/archiveclient"
|
||||
"internal/datevexport"
|
||||
"internal/datevtransfer"
|
||||
|
||||
Reference in new issue
Block a user