diff --git a/lib/client/ca_export.go b/lib/client/ca_export.go index ad5de7e995e34..98bfe9d34da6d 100644 --- a/lib/client/ca_export.go +++ b/lib/client/ca_export.go @@ -59,7 +59,7 @@ type ExportedAuthority struct { // Data is the output of the exported authority. // May be an SSH authorized key, an SSH known hosts entry, a DER or a PEM, // depending on the type of the exported authority. - Data []byte + Data []byte `json:"data"` } // ExportAllAuthorities exports public keys of all authorities of a particular diff --git a/lib/web/ca_export.go b/lib/web/ca_export.go index 99e548f84ece1..eab9c894f6b35 100644 --- a/lib/web/ca_export.go +++ b/lib/web/ca_export.go @@ -19,6 +19,7 @@ package web import ( "archive/zip" "bytes" + "encoding/json" "fmt" "net/http" "time" @@ -52,12 +53,6 @@ func (h *Handler) authExportPublicError(w http.ResponseWriter, r *http.Request, query := r.URL.Query() caType := query.Get("type") // validated by ExportAllAuthorities - format := query.Get("format") - - const formatZip = "zip" - if format != "" && format != formatZip { - return trace.BadParameter("unsupported format %q", format) - } ctx := r.Context() authorities, err := client.ExportAllAuthorities( @@ -72,11 +67,23 @@ func (h *Handler) authExportPublicError(w http.ResponseWriter, r *http.Request, return trace.Wrap(err) } - if format == formatZip { + format := query.Get("format") + + const formatZip = "zip" + const formatJSON = "json" + switch format { + case "": + break + case formatZip: return h.authExportPublicZip(w, r, authorities) + case formatJSON: + return h.authExportPublicJSON(w, r, authorities) + default: + return trace.BadParameter("unsupported format %q", format) } + if l := len(authorities); l > 1 { - return trace.BadParameter("found %d authorities to export, use format=%s to export all", l, formatZip) + return trace.BadParameter("found %d authorities to export, use format=%s or format=%s to export all", l, formatZip, formatJSON) } // ServeContent sets the correct headers: Content-Type, Content-Length and Accept-Ranges. @@ -119,3 +126,19 @@ func (h *Handler) authExportPublicZip( http.ServeContent(w, r, zipName, now, bytes.NewReader(out.Bytes())) return nil } + +func (h *Handler) authExportPublicJSON( + w http.ResponseWriter, + r *http.Request, + authorities []*client.ExportedAuthority, +) error { + marshalledAuthorities, err := json.Marshal(authorities) + if err != nil { + return trace.Wrap(err, "failed to JSON marshal authorities") + } + + // File name is not critical here. It is only used by `ServeContent` to determine the value of the + // `Content-Type` header. + http.ServeContent(w, r, "export.json", time.Now(), bytes.NewReader(marshalledAuthorities)) + return nil +} diff --git a/lib/web/ca_export_test.go b/lib/web/ca_export_test.go index 258b9f44f4910..8963d750cbf0e 100644 --- a/lib/web/ca_export_test.go +++ b/lib/web/ca_export_test.go @@ -22,6 +22,7 @@ import ( "context" "crypto/tls" "crypto/x509" + "encoding/json" "encoding/pem" "fmt" "io" @@ -33,6 +34,8 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + + "github.com/gravitational/teleport/lib/client" ) func TestAuthExport(t *testing.T) { @@ -90,6 +93,22 @@ func TestAuthExport(t *testing.T) { validateFormatZip(t, body, wantCAFiles, validateTLSCertificatePEMFunc) } + validateFormatJSON := func( + t *testing.T, + body []byte, + wantCAFiles int, + validateCAFile func(t *testing.T, contents []byte), + ) { + var authorities []client.ExportedAuthority + err := json.Unmarshal(body, &authorities) + require.NoError(t, err) + assert.Len(t, authorities, wantCAFiles) + + for _, authority := range authorities { + validateCAFile(t, authority.Data) + } + } + ctx := context.Background() for _, tt := range []struct { @@ -215,6 +234,17 @@ func TestAuthExport(t *testing.T) { validateFormatZipPEM(t, b, 1 /* wantCAFiles */) }, }, + { + name: "format=json", + params: url.Values{ + "type": []string{"db-client"}, + "format": []string{"json"}, + }, + expectedStatus: http.StatusOK, + assertBody: func(t *testing.T, b []byte) { + validateFormatJSON(t, b, 1, validateTLSCertificatePEMFunc) + }, + }, } { t.Run(tt.name, func(t *testing.T) { t.Parallel()