package policyapi import ( "bytes" "context" "encoding/json" "errors" "net/http" "net/http/httptest" "os" "testing" "github.com/jackc/pgx/v5/pgxpool" "gitea.perlbach24.de/scripte/nexarch/internal/policy" "gitea.perlbach24.de/scripte/nexarch/internal/rbac" ) const testToken = "test-service-token" func setupTest(t *testing.T) *policy.Enforcer { t.Helper() dsn := os.Getenv("TEST_ADMIN_DSN") if dsn == "" { t.Skip("TEST_ADMIN_DSN nicht gesetzt, Integrationstest uebersprungen") } ctx := context.Background() pool, err := pgxpool.New(ctx, dsn) if err != nil { t.Fatalf("pool: %v", err) } t.Cleanup(func() { pool.Close() }) if _, err := pool.Exec(ctx, ` CREATE EXTENSION IF NOT EXISTS pgcrypto; CREATE TABLE IF NOT EXISTS policy_rules ( role TEXT NOT NULL, permission TEXT NOT NULL, granted_by TEXT NOT NULL, granted_at TIMESTAMPTZ NOT NULL DEFAULT now(), PRIMARY KEY (role, permission) ); CREATE TABLE IF NOT EXISTS policy_rule_changes ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), role TEXT NOT NULL, permission TEXT NOT NULL, action TEXT NOT NULL CHECK (action IN ('grant','revoke')), actor TEXT NOT NULL, version INT NOT NULL, changed_at TIMESTAMPTZ NOT NULL DEFAULT now() ); `); err != nil { t.Fatalf("schema: %v", err) } t.Cleanup(func() { _, _ = pool.Exec(context.Background(), `DELETE FROM policy_rule_changes WHERE role LIKE 'rbac06\_%' ESCAPE '\'`) _, _ = pool.Exec(context.Background(), `DELETE FROM policy_rules WHERE role LIKE 'rbac06\_%' ESCAPE '\'`) }) store := policy.NewStore(pool) if err := store.Grant(ctx, rbac.Role("rbac06_erlaubte_rolle"), rbac.Permission("rbac06_recht"), "test"); err != nil { t.Fatalf("grant: %v", err) } return policy.NewEnforcer(store) } func authorize(t *testing.T, server *httptest.Server, role, permission string) (int, bool) { t.Helper() body, _ := json.Marshal(authorizeRequest{Role: role, Permission: permission}) req, _ := http.NewRequest(http.MethodPost, server.URL+"/authorize", bytes.NewReader(body)) req.Header.Set("X-Service-Token", testToken) resp, err := http.DefaultClient.Do(req) if err != nil { t.Fatalf("post: %v", err) } defer func() { _ = resp.Body.Close() }() if resp.StatusCode != http.StatusOK { return resp.StatusCode, false } var out authorizeResponse if err := json.NewDecoder(resp.Body).Decode(&out); err != nil { t.Fatalf("antwort dekodieren: %v", err) } return resp.StatusCode, out.Allowed } // TestAuthorizeHandler_MatchesDirectEnforcerResult ist die geforderte // Pflichtpruefung: Endpunkt-Antwort stimmt in 100 Stichproben exakt mit // dem direkten Enforcer.Authorize-Ergebnis ueberein - real fuer sowohl // erlaubte als auch verbotene role+permission-Kombinationen. func TestAuthorizeHandler_MatchesDirectEnforcerResult(t *testing.T) { enforcer := setupTest(t) mux := http.NewServeMux() Mount(mux, enforcer, testToken) server := httptest.NewServer(mux) defer server.Close() cases := []struct { role, permission string }{ {"rbac06_erlaubte_rolle", "rbac06_recht"}, // erlaubt {"rbac06_unbekannte_rolle", "rbac06_recht"}, // verboten (default-deny) {"rbac06_erlaubte_rolle", "rbac06_anderes"}, // verboten (falsches recht) } for i := 0; i < 100; i++ { c := cases[i%len(cases)] directErr := enforcer.Authorize(context.Background(), rbac.Role(c.role), rbac.Permission(c.permission)) directAllowed := directErr == nil if directErr != nil && !errors.Is(directErr, policy.ErrDenied) { t.Fatalf("stichprobe %d: unerwarteter enforcer-fehler: %v", i, directErr) } status, httpAllowed := authorize(t, server, c.role, c.permission) if status != http.StatusOK { t.Fatalf("stichprobe %d: unerwarteter status %d", i, status) } if httpAllowed != directAllowed { t.Fatalf("stichprobe %d (%s/%s): http=%t, direkt=%t - weichen ab", i, c.role, c.permission, httpAllowed, directAllowed) } } }