A Kubernetes operator that bridges Hardware Security Module (HSM) data storage with Kubernetes Secrets, providing true secret portability th
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}