A Kubernetes operator that bridges Hardware Security Module (HSM) data storage with Kubernetes Secrets, providing true secret portability th
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

at main 237 lines 8.4 kB view raw
1/* 2Copyright 2025. 3 4Licensed under the Apache License, Version 2.0 (the "License"); 5you may not use this file except in compliance with the License. 6You may obtain a copy of the License at 7 8 http://www.apache.org/licenses/LICENSE-2.0 9 10Unless required by applicable law or agreed to in writing, software 11distributed under the License is distributed on an "AS IS" BASIS, 12WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13See the License for the specific language governing permissions and 14limitations under the License. 15*/ 16 17package api 18 19import ( 20 "bytes" 21 "encoding/json" 22 "net/http" 23 "net/http/httptest" 24 "testing" 25 26 "github.com/stretchr/testify/assert" 27 "github.com/stretchr/testify/require" 28 "k8s.io/apimachinery/pkg/runtime" 29 "k8s.io/client-go/kubernetes/fake" 30 fakeclient "sigs.k8s.io/controller-runtime/pkg/client/fake" 31 "sigs.k8s.io/controller-runtime/pkg/log" 32 33 "github.com/evanjarrett/hsm-secrets-operator/internal/agent" 34 "github.com/evanjarrett/hsm-secrets-operator/internal/security" 35) 36 37func TestJWTAuthenticationIntegration(t *testing.T) { 38 // Set up test dependencies 39 scheme := runtime.NewScheme() 40 41 // Create fake Kubernetes client 42 k8sInterface := fake.NewSimpleClientset() 43 44 // Create controller-runtime fake client 45 fakeClient := fakeclient.NewClientBuilder().WithScheme(scheme).Build() 46 47 // Create agent manager 48 agentManager := agent.NewManager(fakeClient, "test-namespace", "test-agent:latest", nil) 49 50 // Create API server 51 logger := log.Log.WithName("test") 52 server := NewServer(fakeClient, agentManager, "test-namespace", k8sInterface, 8090, logger) 53 54 t.Run("Invalid JWT Token", func(t *testing.T) { 55 // Test with invalid JWT token 56 req := httptest.NewRequest("GET", "/api/v1/hsm/info", nil) 57 req.Header.Set("Authorization", "Bearer invalid-token") 58 w := httptest.NewRecorder() 59 60 server.router.ServeHTTP(w, req) 61 62 // Should be unauthorized 63 assert.Equal(t, http.StatusUnauthorized, w.Code) 64 65 var response map[string]any 66 err := json.Unmarshal(w.Body.Bytes(), &response) 67 require.NoError(t, err) 68 errorObj, ok := response["error"].(map[string]any) 69 require.True(t, ok, "error should be an object") 70 assert.Contains(t, errorObj["message"], "invalid") 71 }) 72 73 t.Run("Missing Authorization Header", func(t *testing.T) { 74 // Test with no authorization header 75 req := httptest.NewRequest("GET", "/api/v1/hsm/info", nil) 76 w := httptest.NewRecorder() 77 78 server.router.ServeHTTP(w, req) 79 80 // Should be unauthorized 81 assert.Equal(t, http.StatusUnauthorized, w.Code) 82 83 var response map[string]any 84 err := json.Unmarshal(w.Body.Bytes(), &response) 85 require.NoError(t, err) 86 errorObj, ok := response["error"].(map[string]any) 87 require.True(t, ok, "error should be an object") 88 assert.Contains(t, errorObj["message"], "missing authorization header") 89 }) 90 91 t.Run("Malformed Authorization Header", func(t *testing.T) { 92 // Test with malformed authorization header (no Bearer prefix) 93 req := httptest.NewRequest("GET", "/api/v1/hsm/info", nil) 94 req.Header.Set("Authorization", "invalid-format-token") 95 w := httptest.NewRecorder() 96 97 server.router.ServeHTTP(w, req) 98 99 // Should be unauthorized 100 assert.Equal(t, http.StatusUnauthorized, w.Code) 101 102 var response map[string]any 103 err := json.Unmarshal(w.Body.Bytes(), &response) 104 require.NoError(t, err) 105 errorObj, ok := response["error"].(map[string]any) 106 require.True(t, ok, "error should be an object") 107 assert.Contains(t, errorObj["message"], "invalid authorization header format") 108 }) 109 110 t.Run("Health Endpoint Accessible Without Auth", func(t *testing.T) { 111 // Health endpoint should not require authentication 112 req := httptest.NewRequest("GET", "/api/v1/health", nil) 113 w := httptest.NewRecorder() 114 115 server.router.ServeHTTP(w, req) 116 117 // Should succeed without authentication 118 assert.Equal(t, http.StatusOK, w.Code) 119 120 var response map[string]any 121 err := json.Unmarshal(w.Body.Bytes(), &response) 122 require.NoError(t, err) 123 assert.Equal(t, true, response["success"]) 124 }) 125 126 t.Run("Auth Token Endpoint Accessible Without Auth", func(t *testing.T) { 127 // Token generation endpoint should not require authentication 128 tokenRequest := security.TokenRequest{ 129 K8sToken: "test-token", 130 } 131 requestBody, err := json.Marshal(tokenRequest) 132 require.NoError(t, err) 133 134 req := httptest.NewRequest("POST", "/api/v1/auth/token", bytes.NewBuffer(requestBody)) 135 req.Header.Set("Content-Type", "application/json") 136 w := httptest.NewRecorder() 137 138 server.router.ServeHTTP(w, req) 139 140 // The endpoint should be accessible but will return 401 due to invalid K8s token validation 141 // This verifies the endpoint doesn't require JWT auth but still validates the K8s token 142 assert.Equal(t, http.StatusUnauthorized, w.Code, "Should fail due to invalid K8s token, not missing JWT") 143 144 // Verify it's a token validation error, not auth middleware error 145 var response map[string]any 146 err = json.Unmarshal(w.Body.Bytes(), &response) 147 require.NoError(t, err) 148 149 // Should contain details about the K8s token failure, not JWT auth failure 150 errorObj, ok := response["error"].(map[string]any) 151 require.True(t, ok, "error should be an object") 152 assert.Contains(t, errorObj["message"], "failed to generate") 153 }) 154 155 t.Run("JWT Authentication Enabled", func(t *testing.T) { 156 // Verify that the server has JWT authentication enabled 157 assert.NotNil(t, server.authenticator, "API server should have JWT authenticator enabled") 158 159 // Test that protected endpoints are actually protected 160 protectedEndpoints := []string{ 161 "/api/v1/hsm/info", 162 "/api/v1/hsm/status", 163 "/api/v1/hsm/secrets", 164 } 165 166 for _, endpoint := range protectedEndpoints { 167 req := httptest.NewRequest("GET", endpoint, nil) 168 w := httptest.NewRecorder() 169 170 server.router.ServeHTTP(w, req) 171 172 assert.Equal(t, http.StatusUnauthorized, w.Code, 173 "Endpoint %s should require authentication", endpoint) 174 } 175 }) 176} 177 178func TestWebUIJWTWorkflow(t *testing.T) { 179 t.Run("Web UI Static Files and Routing", func(t *testing.T) { 180 // Test that the web UI is properly served and routed 181 scheme := runtime.NewScheme() 182 183 k8sInterface := fake.NewSimpleClientset() 184 fakeClient := fakeclient.NewClientBuilder().WithScheme(scheme).Build() 185 agentManager := agent.NewManager(fakeClient, "test-namespace", "test-agent:latest", nil) 186 logger := log.Log.WithName("test-webui") 187 server := NewServer(fakeClient, agentManager, "test-namespace", k8sInterface, 8090, logger) 188 189 // Test redirect from root to web UI 190 req := httptest.NewRequest("GET", "/", nil) 191 w := httptest.NewRecorder() 192 193 server.router.ServeHTTP(w, req) 194 195 assert.Equal(t, http.StatusFound, w.Code, "Should redirect to web UI") 196 assert.Equal(t, "/web/", w.Header().Get("Location"), "Should redirect to /web/") 197 198 t.Logf("✅ Web UI routing test completed successfully") 199 t.Logf("✅ Root path redirects to: %s", w.Header().Get("Location")) 200 }) 201 202 t.Run("Authentication Structure", func(t *testing.T) { 203 // Test the authentication structure that the web UI expects 204 scheme := runtime.NewScheme() 205 206 k8sInterface := fake.NewSimpleClientset() 207 fakeClient := fakeclient.NewClientBuilder().WithScheme(scheme).Build() 208 agentManager := agent.NewManager(fakeClient, "test-namespace", "test-agent:latest", nil) 209 logger := log.Log.WithName("test-auth-structure") 210 server := NewServer(fakeClient, agentManager, "test-namespace", k8sInterface, 8090, logger) 211 212 // Test auth token endpoint structure (should accept JSON) 213 tokenRequest := map[string]string{ 214 "k8s_token": "invalid-but-proper-format", 215 } 216 requestBody, err := json.Marshal(tokenRequest) 217 require.NoError(t, err) 218 219 req := httptest.NewRequest("POST", "/api/v1/auth/token", bytes.NewBuffer(requestBody)) 220 req.Header.Set("Content-Type", "application/json") 221 w := httptest.NewRecorder() 222 223 server.router.ServeHTTP(w, req) 224 225 // Should process the request (will fail on token validation, but that's expected) 226 assert.NotEqual(t, http.StatusNotFound, w.Code, "Auth endpoint should exist") 227 assert.NotEqual(t, http.StatusMethodNotAllowed, w.Code, "POST should be allowed") 228 229 // Should return JSON error 230 var response map[string]any 231 err = json.Unmarshal(w.Body.Bytes(), &response) 232 require.NoError(t, err, "Response should be valid JSON") 233 234 t.Logf("✅ Authentication structure test completed") 235 t.Logf("✅ Auth endpoint accepts JSON and returns structured errors") 236 }) 237}