package server import ( "context" "encoding/json" "net/http" "net/http/httptest" "path/filepath" "strings" "testing" "log/slog" "github.com/go-chi/chi/v5" "git.wntrmute.dev/kyle/metacrypt/internal/barrier" "git.wntrmute.dev/kyle/metacrypt/internal/config" "git.wntrmute.dev/kyle/metacrypt/internal/crypto" "git.wntrmute.dev/kyle/metacrypt/internal/db" "git.wntrmute.dev/kyle/metacrypt/internal/engine" "git.wntrmute.dev/kyle/metacrypt/internal/policy" "git.wntrmute.dev/kyle/metacrypt/internal/seal" // auth is used indirectly via the server "git.wntrmute.dev/kyle/metacrypt/internal/auth" ) func setupTestServer(t *testing.T) (*Server, *seal.Manager, chi.Router) { t.Helper() dir := t.TempDir() database, err := db.Open(filepath.Join(dir, "test.db")) if err != nil { t.Fatalf("open db: %v", err) } t.Cleanup(func() { _ = database.Close() }) _ = db.Migrate(database) b := barrier.NewAESGCMBarrier(database) sealMgr := seal.NewManager(database, b, slog.Default()) _ = sealMgr.CheckInitialized() // Auth requires MCIAS client which we can't create in tests easily, // so we pass nil and avoid auth-dependent routes in these tests. authenticator := auth.NewAuthenticator(nil, slog.Default()) policyEngine := policy.NewEngine(b) engineRegistry := engine.NewRegistry(b, slog.Default()) cfg := &config.Config{ Server: config.ServerConfig{ ListenAddr: ":0", TLSCert: "cert.pem", TLSKey: "key.pem", }, Database: config.DatabaseConfig{Path: filepath.Join(dir, "test.db")}, MCIAS: config.MCIASConfig{ServerURL: "https://mcias.test"}, Seal: config.SealConfig{ Argon2Time: 1, Argon2Memory: 64 * 1024, Argon2Threads: 1, }, } logger := slog.Default() srv := New(cfg, sealMgr, authenticator, policyEngine, engineRegistry, logger, "test") r := chi.NewRouter() srv.registerRoutes(r) return srv, sealMgr, r } func TestStatusEndpoint(t *testing.T) { _, _, mux := setupTestServer(t) req := httptest.NewRequest(http.MethodGet, "/v1/status", nil) w := httptest.NewRecorder() mux.ServeHTTP(w, req) if w.Code != http.StatusOK { t.Fatalf("status code: got %d, want %d", w.Code, http.StatusOK) } var resp map[string]interface{} _ = json.Unmarshal(w.Body.Bytes(), &resp) if resp["state"] != "uninitialized" { t.Errorf("state: got %q, want %q", resp["state"], "uninitialized") } } func TestInitEndpoint(t *testing.T) { _, _, mux := setupTestServer(t) body := `{"password":"test-password"}` req := httptest.NewRequest(http.MethodPost, "/v1/init", strings.NewReader(body)) w := httptest.NewRecorder() mux.ServeHTTP(w, req) if w.Code != http.StatusOK { t.Fatalf("status code: got %d, want %d. Body: %s", w.Code, http.StatusOK, w.Body.String()) } var resp map[string]interface{} _ = json.Unmarshal(w.Body.Bytes(), &resp) if resp["state"] != "unsealed" { t.Errorf("state: got %q, want %q", resp["state"], "unsealed") } // Second init should fail. req2 := httptest.NewRequest(http.MethodPost, "/v1/init", strings.NewReader(body)) w2 := httptest.NewRecorder() mux.ServeHTTP(w2, req2) if w2.Code != http.StatusConflict { t.Errorf("double init: got %d, want %d", w2.Code, http.StatusConflict) } } func TestUnsealEndpoint(t *testing.T) { _, sealMgr, mux := setupTestServer(t) // Initialize first. params := crypto.Argon2Params{Time: 1, Memory: 64 * 1024, Threads: 1} _ = sealMgr.Initialize(context.Background(), []byte("password"), params) _ = sealMgr.Seal() // Unseal with wrong password. body := `{"password":"wrong"}` req := httptest.NewRequest(http.MethodPost, "/v1/unseal", strings.NewReader(body)) w := httptest.NewRecorder() mux.ServeHTTP(w, req) if w.Code != http.StatusUnauthorized { t.Errorf("wrong password: got %d, want %d", w.Code, http.StatusUnauthorized) } // Unseal with correct password. body = `{"password":"password"}` req = httptest.NewRequest(http.MethodPost, "/v1/unseal", strings.NewReader(body)) w = httptest.NewRecorder() mux.ServeHTTP(w, req) if w.Code != http.StatusOK { t.Fatalf("correct password: got %d, want %d. Body: %s", w.Code, http.StatusOK, w.Body.String()) } } func TestStatusMethodNotAllowed(t *testing.T) { _, _, mux := setupTestServer(t) req := httptest.NewRequest(http.MethodPost, "/v1/status", nil) w := httptest.NewRecorder() mux.ServeHTTP(w, req) if w.Code != http.StatusMethodNotAllowed { t.Errorf("POST /v1/status: got %d, want %d", w.Code, http.StatusMethodNotAllowed) } } func TestRootNotFound(t *testing.T) { _, _, mux := setupTestServer(t) // The vault server no longer serves a web UI at /; that is handled by metacrypt-web. req := httptest.NewRequest(http.MethodGet, "/", nil) w := httptest.NewRecorder() mux.ServeHTTP(w, req) if w.Code != http.StatusNotFound { t.Errorf("root: got %d, want %d", w.Code, http.StatusNotFound) } } func unsealServer(t *testing.T, sealMgr *seal.Manager, _ interface{}) { t.Helper() params := crypto.Argon2Params{Time: 1, Memory: 64 * 1024, Threads: 1} if err := sealMgr.Initialize(context.Background(), []byte("password"), params); err != nil { t.Fatalf("initialize: %v", err) } } func makeEngineRequest(mount, operation string) string { return `{"mount":"` + mount + `","operation":"` + operation + `","data":{}}` } func withTokenInfo(r *http.Request, info *auth.TokenInfo) *http.Request { return r.WithContext(context.WithValue(r.Context(), tokenInfoKey, info)) } // TestEngineRequestPolicyDeniesNonAdmin verifies that a non-admin user without // an explicit allow rule is denied by the policy engine. func TestEngineRequestPolicyDeniesNonAdmin(t *testing.T) { srv, sealMgr, _ := setupTestServer(t) unsealServer(t, sealMgr, nil) body := makeEngineRequest("pki", "list-issuers") req := httptest.NewRequest(http.MethodPost, "/v1/engine/request", strings.NewReader(body)) req = withTokenInfo(req, &auth.TokenInfo{Username: "alice", Roles: []string{"user"}, IsAdmin: false}) w := httptest.NewRecorder() srv.handleEngineRequest(w, req) if w.Code != http.StatusForbidden { t.Errorf("expected 403 Forbidden for non-admin without policy rule, got %d: %s", w.Code, w.Body.String()) } } // TestEngineRequestPolicyAllowsAdmin verifies that admin users bypass policy. func TestEngineRequestPolicyAllowsAdmin(t *testing.T) { srv, sealMgr, _ := setupTestServer(t) unsealServer(t, sealMgr, nil) body := makeEngineRequest("pki", "list-issuers") req := httptest.NewRequest(http.MethodPost, "/v1/engine/request", strings.NewReader(body)) req = withTokenInfo(req, &auth.TokenInfo{Username: "admin", Roles: []string{"admin"}, IsAdmin: true}) w := httptest.NewRecorder() srv.handleEngineRequest(w, req) // Admin bypasses policy; will fail with mount-not-found (404), not forbidden (403). if w.Code == http.StatusForbidden { t.Errorf("admin should not be forbidden by policy, got 403: %s", w.Body.String()) } } // TestEngineRequestPolicyAllowsWithRule verifies that a non-admin user with an // explicit allow rule is permitted to proceed. func TestEngineRequestPolicyAllowsWithRule(t *testing.T) { srv, sealMgr, _ := setupTestServer(t) unsealServer(t, sealMgr, nil) ctx := context.Background() _ = srv.policy.CreateRule(ctx, &policy.Rule{ ID: "allow-user-read", Priority: 100, Effect: policy.EffectAllow, Roles: []string{"user"}, Resources: []string{"engine/*/*"}, Actions: []string{"read"}, }) body := makeEngineRequest("pki", "list-issuers") req := httptest.NewRequest(http.MethodPost, "/v1/engine/request", strings.NewReader(body)) req = withTokenInfo(req, &auth.TokenInfo{Username: "alice", Roles: []string{"user"}, IsAdmin: false}) w := httptest.NewRecorder() srv.handleEngineRequest(w, req) // Policy allows; will fail with mount-not-found (404), not forbidden (403). if w.Code == http.StatusForbidden { t.Errorf("user with allow rule should not be forbidden, got 403: %s", w.Body.String()) } } // TestEngineRequestAdminOnlyBlocksNonAdmin verifies that admin-only operations // via the generic endpoint are rejected for non-admin users. func TestEngineRequestAdminOnlyBlocksNonAdmin(t *testing.T) { srv, sealMgr, _ := setupTestServer(t) unsealServer(t, sealMgr, nil) for _, op := range []string{"create-issuer", "delete-cert", "create-key", "rotate-key", "create-profile", "provision"} { body := makeEngineRequest("test-mount", op) req := httptest.NewRequest(http.MethodPost, "/v1/engine/request", strings.NewReader(body)) req = withTokenInfo(req, &auth.TokenInfo{Username: "alice", Roles: []string{"user"}, IsAdmin: false}) w := httptest.NewRecorder() srv.handleEngineRequest(w, req) if w.Code != http.StatusForbidden { t.Errorf("operation %q: expected 403 for non-admin, got %d", op, w.Code) } } } // TestEngineRequestAdminOnlyAllowsAdmin verifies that admin-only operations // via the generic endpoint are allowed for admin users. func TestEngineRequestAdminOnlyAllowsAdmin(t *testing.T) { srv, sealMgr, _ := setupTestServer(t) unsealServer(t, sealMgr, nil) for _, op := range []string{"create-issuer", "delete-cert", "create-key", "rotate-key", "create-profile", "provision"} { body := makeEngineRequest("test-mount", op) req := httptest.NewRequest(http.MethodPost, "/v1/engine/request", strings.NewReader(body)) req = withTokenInfo(req, &auth.TokenInfo{Username: "admin", Roles: []string{"admin"}, IsAdmin: true}) w := httptest.NewRecorder() srv.handleEngineRequest(w, req) // Admin passes the admin check; will get 404 (mount not found) not 403. if w.Code == http.StatusForbidden { t.Errorf("operation %q: admin should not be forbidden, got 403", op) } } } // TestOperationAction verifies the action classification of operations. func TestOperationAction(t *testing.T) { tests := map[string]string{ // Read operations. "list-issuers": policy.ActionRead, "list-certs": policy.ActionRead, "get-cert": policy.ActionRead, "get-root": policy.ActionRead, "get-chain": policy.ActionRead, "get-issuer": policy.ActionRead, "list-keys": policy.ActionRead, "get-key": policy.ActionRead, "get-public-key": policy.ActionRead, "list-users": policy.ActionRead, "get-profile": policy.ActionRead, "list-profiles": policy.ActionRead, // Granular crypto operations (including batch variants). "encrypt": policy.ActionEncrypt, "batch-encrypt": policy.ActionEncrypt, "decrypt": policy.ActionDecrypt, "batch-decrypt": policy.ActionDecrypt, "sign": policy.ActionSign, "sign-host": policy.ActionSign, "sign-user": policy.ActionSign, "verify": policy.ActionVerify, "hmac": policy.ActionHMAC, // Write operations. "issue": policy.ActionWrite, "renew": policy.ActionWrite, "create-issuer": policy.ActionWrite, "delete-issuer": policy.ActionWrite, "sign-csr": policy.ActionWrite, "revoke": policy.ActionWrite, } for op, want := range tests { if got := operationAction(op); got != want { t.Errorf("operationAction(%q) = %q, want %q", op, got, want) } } } func TestTokenInfoFromContext(t *testing.T) { ctx := context.Background() if info := TokenInfoFromContext(ctx); info != nil { t.Error("expected nil from empty context") } info := &auth.TokenInfo{Username: "test", IsAdmin: true} ctx = context.WithValue(ctx, tokenInfoKey, info) got := TokenInfoFromContext(ctx) if got == nil || got.Username != "test" { t.Error("expected token info from context") } }