@@ -335,3 +335,147 @@ func TestHandleQuickTest_UnreachableInstance(t *testing.T) {
335335 t .Error ("expected non-empty error field for unreachable instance" )
336336 }
337337}
338+
339+ // ── Config endpoints redact secrets over the wire ──────────────────────
340+ //
341+ // These are the end-to-end proof that "never send a raw secret to the
342+ // client" actually holds across the HTTP boundary, not just inside
343+ // configmgmt: each test seeds a real secret directly via ConfigStore,
344+ // then asserts the HTTP JSON response differs from what a second,
345+ // direct ConfigStore.Current() call shows is actually stored.
346+
347+ const secretConfigFixture = `{"identifiers":[{"name":"a","type":"hmac","config":{"keys":{"k1":{"secret":"super-secret-hmac-key"}}}}]}`
348+
349+ func TestHandleGetConfig_RedactsSecret (t * testing.T ) {
350+ s := newTestServer (t )
351+ registerInstance (s , "inst" , "local" , "default" , "http://upstream" )
352+ if _ , err := s .ConfigStore .PushConfig (t .Context (), "local" , "inst" , secretConfigFixture , "op" , "seed" ); err != nil {
353+ t .Fatalf ("seed config: %v" , err )
354+ }
355+
356+ req := httptest .NewRequest (http .MethodGet , "/v1/controlplane/instances/local/inst/config" , nil )
357+ req .SetPathValue ("cluster" , "local" )
358+ req .SetPathValue ("name" , "inst" )
359+ rr := httptest .NewRecorder ()
360+ s .Mux .ServeHTTP (rr , req )
361+
362+ if rr .Code != http .StatusOK {
363+ t .Fatalf ("expected 200, got %d: %s" , rr .Code , rr .Body .String ())
364+ }
365+ var resp configmgmt.ConfigVersion
366+ if err := json .NewDecoder (rr .Body ).Decode (& resp ); err != nil {
367+ t .Fatalf ("decode response: %v" , err )
368+ }
369+
370+ stored , ok := s .ConfigStore .Current ("local" , "inst" )
371+ if ! ok {
372+ t .Fatal ("expected a stored version" )
373+ }
374+ if resp .Content == stored .Content {
375+ t .Error ("HTTP response content matches the raw stored content — secret was not redacted" )
376+ }
377+ if strings .Contains (resp .Content , "super-secret-hmac-key" ) {
378+ t .Error ("raw secret value leaked into the HTTP response body" )
379+ }
380+ if ! strings .Contains (stored .Content , "super-secret-hmac-key" ) {
381+ t .Error ("expected the real secret to still be present in the store internally" )
382+ }
383+ }
384+
385+ func TestHandlePushConfig_ResponseRedactedStorePersistsReal (t * testing.T ) {
386+ s := newTestServer (t )
387+ registerInstance (s , "inst" , "local" , "default" , "http://upstream" )
388+
389+ body , _ := json .Marshal (map [string ]string {"content" : secretConfigFixture , "author" : "op" })
390+ req := httptest .NewRequest (http .MethodPost , "/v1/controlplane/instances/local/inst/config" , strings .NewReader (string (body )))
391+ req .SetPathValue ("cluster" , "local" )
392+ req .SetPathValue ("name" , "inst" )
393+ req .Header .Set ("Content-Type" , "application/json" )
394+ rr := httptest .NewRecorder ()
395+ s .Mux .ServeHTTP (rr , req )
396+
397+ if rr .Code != http .StatusCreated {
398+ t .Fatalf ("expected 201, got %d: %s" , rr .Code , rr .Body .String ())
399+ }
400+ if strings .Contains (rr .Body .String (), "super-secret-hmac-key" ) {
401+ t .Error ("raw secret value leaked into the push response body" )
402+ }
403+
404+ stored , ok := s .ConfigStore .Current ("local" , "inst" )
405+ if ! ok {
406+ t .Fatal ("expected a stored version" )
407+ }
408+ if ! strings .Contains (stored .Content , "super-secret-hmac-key" ) {
409+ t .Error ("expected the real secret to be persisted, push response redaction must not affect what's stored" )
410+ }
411+ }
412+
413+ func TestHandleConfigHistory_RedactsEveryVersion (t * testing.T ) {
414+ s := newTestServer (t )
415+ registerInstance (s , "inst" , "local" , "default" , "http://upstream" )
416+ if _ , err := s .ConfigStore .PushConfig (t .Context (), "local" , "inst" , secretConfigFixture , "op" , "v1" ); err != nil {
417+ t .Fatalf ("seed v1: %v" , err )
418+ }
419+ v2 := `{"identifiers":[{"name":"a","type":"hmac","config":{"keys":{"k1":{"secret":"second-secret-value"}}}}]}`
420+ if _ , err := s .ConfigStore .PushConfig (t .Context (), "local" , "inst" , v2 , "op" , "v2" ); err != nil {
421+ t .Fatalf ("seed v2: %v" , err )
422+ }
423+
424+ req := httptest .NewRequest (http .MethodGet , "/v1/controlplane/instances/local/inst/config/history" , nil )
425+ req .SetPathValue ("cluster" , "local" )
426+ req .SetPathValue ("name" , "inst" )
427+ rr := httptest .NewRecorder ()
428+ s .Mux .ServeHTTP (rr , req )
429+
430+ if rr .Code != http .StatusOK {
431+ t .Fatalf ("expected 200, got %d: %s" , rr .Code , rr .Body .String ())
432+ }
433+ var history []configmgmt.ConfigVersion
434+ if err := json .NewDecoder (rr .Body ).Decode (& history ); err != nil {
435+ t .Fatalf ("decode response: %v" , err )
436+ }
437+ if len (history ) != 2 {
438+ t .Fatalf ("expected 2 versions, got %d" , len (history ))
439+ }
440+ for _ , v := range history {
441+ if strings .Contains (v .Content , "super-secret-hmac-key" ) || strings .Contains (v .Content , "second-secret-value" ) {
442+ t .Errorf ("version %d: raw secret leaked into history response" , v .Version )
443+ }
444+ }
445+ }
446+
447+ func TestHandleConfigRollback_ResponseRedacted (t * testing.T ) {
448+ s := newTestServer (t )
449+ registerInstance (s , "inst" , "local" , "default" , "http://upstream" )
450+ v1 , err := s .ConfigStore .PushConfig (t .Context (), "local" , "inst" , secretConfigFixture , "op" , "v1" )
451+ if err != nil {
452+ t .Fatalf ("seed v1: %v" , err )
453+ }
454+ v2 := `{"identifiers":[{"name":"a","type":"hmac","config":{"keys":{"k1":{"secret":"second-secret-value"}}}}]}`
455+ if _ , err := s .ConfigStore .PushConfig (t .Context (), "local" , "inst" , v2 , "op" , "v2" ); err != nil {
456+ t .Fatalf ("seed v2: %v" , err )
457+ }
458+
459+ body , _ := json .Marshal (map [string ]any {"version" : v1 .Version , "author" : "op" })
460+ req := httptest .NewRequest (http .MethodPost , "/v1/controlplane/instances/local/inst/config/rollback" , strings .NewReader (string (body )))
461+ req .SetPathValue ("cluster" , "local" )
462+ req .SetPathValue ("name" , "inst" )
463+ req .Header .Set ("Content-Type" , "application/json" )
464+ rr := httptest .NewRecorder ()
465+ s .Mux .ServeHTTP (rr , req )
466+
467+ if rr .Code != http .StatusOK {
468+ t .Fatalf ("expected 200, got %d: %s" , rr .Code , rr .Body .String ())
469+ }
470+ if strings .Contains (rr .Body .String (), "super-secret-hmac-key" ) {
471+ t .Error ("raw secret value leaked into the rollback response body" )
472+ }
473+
474+ stored , ok := s .ConfigStore .Current ("local" , "inst" )
475+ if ! ok {
476+ t .Fatal ("expected a stored version" )
477+ }
478+ if ! strings .Contains (stored .Content , "super-secret-hmac-key" ) {
479+ t .Error ("expected the rolled-back real secret to be persisted internally" )
480+ }
481+ }
0 commit comments