Skip to content

Commit

Permalink
[v17] Add support for JSON format to '/webapi/auth/export' endpoint (#…
Browse files Browse the repository at this point in the history
…52325)

* Add support for HTTP 'Accept' header to '/webapi/auth/export' endpoint

Signed-off-by: Fred Heinecke <[email protected]>

* Switch PR to use new `format` parameter

---------

Signed-off-by: Fred Heinecke <[email protected]>
  • Loading branch information
fheinecke authored Feb 21, 2025
1 parent 308fc6d commit e2c1977
Show file tree
Hide file tree
Showing 3 changed files with 62 additions and 9 deletions.
2 changes: 1 addition & 1 deletion lib/client/ca_export.go
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,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
Expand Down
39 changes: 31 additions & 8 deletions lib/web/ca_export.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ package web
import (
"archive/zip"
"bytes"
"encoding/json"
"fmt"
"net/http"
"time"
Expand Down Expand Up @@ -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(
Expand All @@ -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.
Expand Down Expand Up @@ -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
}
30 changes: 30 additions & 0 deletions lib/web/ca_export_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import (
"context"
"crypto/tls"
"crypto/x509"
"encoding/json"
"encoding/pem"
"fmt"
"io"
Expand All @@ -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) {
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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()
Expand Down

0 comments on commit e2c1977

Please sign in to comment.