internal/policyapi: POST /authorize wrapt policy.Enforcer.Authorize (RBAC-02) fuer physisch getrennte Module (DMS, Mail, Archive) - reiner Wrapper, keine zweite Autorisierungslogik, 100 Stichproben beweisen Uebereinstimmung mit dem direkten Enforcer-Aufruf. Service-Auth ueber schlanken, timing-safe verglichenen Token statt moduleregistrys schwererem Credential-System (unnoetige internal/flag-Abhaengigkeit, nie mit internal/policy gemergt). Ersetzt spaeter das dokumentierte RET-06-API-Provisorium (Archive, eigenes Folgeticket). Reale Rechtevergabe-Luecke auf policy_rules gefunden und behoben (Tabelle von frueherem Testlauf unter anderem Owner). Real auf 131 deployed, beide Pfade (Allow/Deny) per curl end-to-end verifiziert.
117 lines
3.8 KiB
Go
117 lines
3.8 KiB
Go
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)
|
|
}
|
|
}
|
|
}
|