forked from TrueCloudLab/certificates
Refactor ACME Admin API
This commit is contained in:
parent
039d2455d9
commit
d00729df0b
7 changed files with 140 additions and 891 deletions
|
@ -2,17 +2,14 @@ package api
|
|||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/go-chi/chi"
|
||||
"github.com/smallstep/certificates/acme"
|
||||
"github.com/smallstep/certificates/api"
|
||||
"github.com/smallstep/certificates/authority/admin"
|
||||
"github.com/smallstep/certificates/authority/provisioner"
|
||||
"go.step.sm/linkedca"
|
||||
"google.golang.org/protobuf/types/known/timestamppb"
|
||||
)
|
||||
|
||||
const (
|
||||
|
@ -70,7 +67,7 @@ func (h *Handler) provisionerHasEABEnabled(ctx context.Context, provisionerName
|
|||
return false, nil, admin.WrapErrorISE(err, "error loading provisioner %s", provisionerName)
|
||||
}
|
||||
|
||||
prov, err := h.db.GetProvisioner(ctx, p.GetID())
|
||||
prov, err := h.adminDB.GetProvisioner(ctx, p.GetID())
|
||||
if err != nil {
|
||||
return false, nil, admin.WrapErrorISE(err, "error getting provisioner with ID: %s", p.GetID())
|
||||
}
|
||||
|
@ -88,159 +85,31 @@ func (h *Handler) provisionerHasEABEnabled(ctx context.Context, provisionerName
|
|||
return acmeProvisioner.GetRequireEab(), prov, nil
|
||||
}
|
||||
|
||||
// provisionerFromContext searches the context for a provisioner. Returns the
|
||||
// provisioner or an error.
|
||||
func provisionerFromContext(ctx context.Context) (*linkedca.Provisioner, error) {
|
||||
val := ctx.Value(provisionerContextKey)
|
||||
if val == nil {
|
||||
return nil, admin.NewErrorISE("provisioner expected in request context")
|
||||
}
|
||||
pval, ok := val.(*linkedca.Provisioner)
|
||||
if !ok || pval == nil {
|
||||
return nil, admin.NewErrorISE("provisioner in context is not a linkedca.Provisioner")
|
||||
}
|
||||
return pval, nil
|
||||
type acmeAdminResponderInterface interface {
|
||||
GetExternalAccountKeys(w http.ResponseWriter, r *http.Request)
|
||||
CreateExternalAccountKey(w http.ResponseWriter, r *http.Request)
|
||||
DeleteExternalAccountKey(w http.ResponseWriter, r *http.Request)
|
||||
}
|
||||
|
||||
// CreateExternalAccountKey creates a new External Account Binding key
|
||||
func (h *Handler) CreateExternalAccountKey(w http.ResponseWriter, r *http.Request) {
|
||||
var body CreateExternalAccountKeyRequest
|
||||
if err := api.ReadJSON(r.Body, &body); err != nil {
|
||||
api.WriteError(w, admin.WrapError(admin.ErrorBadRequestType, err, "error reading request body"))
|
||||
return
|
||||
}
|
||||
// ACMEAdminResponder is responsible for writing ACME admin responses
|
||||
type ACMEAdminResponder struct{}
|
||||
|
||||
if err := body.Validate(); err != nil {
|
||||
api.WriteError(w, admin.WrapError(admin.ErrorBadRequestType, err, "error validating request body"))
|
||||
return
|
||||
}
|
||||
|
||||
ctx := r.Context()
|
||||
prov, err := provisionerFromContext(ctx)
|
||||
if err != nil {
|
||||
api.WriteError(w, admin.WrapErrorISE(err, "error getting provisioner from context"))
|
||||
return
|
||||
}
|
||||
|
||||
// check if a key with the reference does not exist (only when a reference was in the request)
|
||||
reference := body.Reference
|
||||
if reference != "" {
|
||||
k, err := h.acmeDB.GetExternalAccountKeyByReference(ctx, prov.GetId(), reference)
|
||||
// retrieving an EAB key from DB results in an error if it doesn't exist, which is what we're looking for,
|
||||
// but other errors can also happen. Return early if that happens; continuing if it was acme.ErrNotFound.
|
||||
if shouldWriteError := err != nil && !errors.Is(err, acme.ErrNotFound); shouldWriteError {
|
||||
api.WriteError(w, admin.WrapErrorISE(err, "could not lookup external account key by reference"))
|
||||
return
|
||||
}
|
||||
// if a key was found, return HTTP 409 conflict
|
||||
if k != nil {
|
||||
err := admin.NewError(admin.ErrorBadRequestType, "an ACME EAB key for provisioner '%s' with reference '%s' already exists", prov.GetName(), reference)
|
||||
err.Status = 409
|
||||
api.WriteError(w, err)
|
||||
return
|
||||
}
|
||||
// continue execution if no key was found for the reference
|
||||
}
|
||||
|
||||
eak, err := h.acmeDB.CreateExternalAccountKey(ctx, prov.GetId(), reference)
|
||||
if err != nil {
|
||||
msg := fmt.Sprintf("error creating ACME EAB key for provisioner '%s'", prov.GetName())
|
||||
if reference != "" {
|
||||
msg += fmt.Sprintf(" and reference '%s'", reference)
|
||||
}
|
||||
api.WriteError(w, admin.WrapErrorISE(err, msg))
|
||||
return
|
||||
}
|
||||
|
||||
response := &linkedca.EABKey{
|
||||
Id: eak.ID,
|
||||
HmacKey: eak.KeyBytes,
|
||||
Provisioner: prov.GetName(),
|
||||
Reference: eak.Reference,
|
||||
}
|
||||
|
||||
api.ProtoJSONStatus(w, response, http.StatusCreated)
|
||||
// NewACMEAdminResponder returns a new ACMEAdminResponder
|
||||
func NewACMEAdminResponder() *ACMEAdminResponder {
|
||||
return &ACMEAdminResponder{}
|
||||
}
|
||||
|
||||
// DeleteExternalAccountKey deletes an ACME External Account Key.
|
||||
func (h *Handler) DeleteExternalAccountKey(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
keyID := chi.URLParam(r, "id")
|
||||
|
||||
ctx := r.Context()
|
||||
prov, err := provisionerFromContext(ctx)
|
||||
if err != nil {
|
||||
api.WriteError(w, admin.WrapErrorISE(err, "error getting provisioner from context"))
|
||||
return
|
||||
}
|
||||
|
||||
if err := h.acmeDB.DeleteExternalAccountKey(ctx, prov.GetId(), keyID); err != nil {
|
||||
api.WriteError(w, admin.WrapErrorISE(err, "error deleting ACME EAB Key '%s'", keyID))
|
||||
return
|
||||
}
|
||||
|
||||
api.JSON(w, &DeleteResponse{Status: "ok"})
|
||||
// GetExternalAccountKeys writes the response for the EAB keys GET endpoint
|
||||
func (h *ACMEAdminResponder) GetExternalAccountKeys(w http.ResponseWriter, r *http.Request) {
|
||||
api.WriteError(w, admin.NewError(admin.ErrorNotImplementedType, "this functionality is currently only available in Certificate Manager: https://smallstep.com/signup?product=cm"))
|
||||
}
|
||||
|
||||
// GetExternalAccountKeys returns ACME EAB Keys. If a reference is specified,
|
||||
// only the ExternalAccountKey with that reference is returned. Otherwise all
|
||||
// ExternalAccountKeys in the system for a specific provisioner are returned.
|
||||
func (h *Handler) GetExternalAccountKeys(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
var (
|
||||
key *acme.ExternalAccountKey
|
||||
keys []*acme.ExternalAccountKey
|
||||
err error
|
||||
cursor string
|
||||
nextCursor string
|
||||
limit int
|
||||
)
|
||||
|
||||
ctx := r.Context()
|
||||
prov, err := provisionerFromContext(ctx)
|
||||
if err != nil {
|
||||
api.WriteError(w, admin.WrapErrorISE(err, "error getting provisioner from context"))
|
||||
return
|
||||
}
|
||||
|
||||
if cursor, limit, err = api.ParseCursor(r); err != nil {
|
||||
api.WriteError(w, admin.WrapError(admin.ErrorBadRequestType, err,
|
||||
"error parsing cursor and limit from query params"))
|
||||
return
|
||||
}
|
||||
|
||||
reference := chi.URLParam(r, "reference")
|
||||
if reference != "" {
|
||||
if key, err = h.acmeDB.GetExternalAccountKeyByReference(ctx, prov.GetId(), reference); err != nil {
|
||||
api.WriteError(w, admin.WrapErrorISE(err, "error retrieving external account key with reference '%s'", reference))
|
||||
return
|
||||
}
|
||||
if key != nil {
|
||||
keys = []*acme.ExternalAccountKey{key}
|
||||
}
|
||||
} else {
|
||||
if keys, nextCursor, err = h.acmeDB.GetExternalAccountKeys(ctx, prov.GetId(), cursor, limit); err != nil {
|
||||
api.WriteError(w, admin.WrapErrorISE(err, "error retrieving external account keys"))
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
provisionerName := prov.GetName()
|
||||
eaks := make([]*linkedca.EABKey, len(keys))
|
||||
for i, k := range keys {
|
||||
eaks[i] = &linkedca.EABKey{
|
||||
Id: k.ID,
|
||||
HmacKey: []byte{},
|
||||
Provisioner: provisionerName,
|
||||
Reference: k.Reference,
|
||||
Account: k.AccountID,
|
||||
CreatedAt: timestamppb.New(k.CreatedAt),
|
||||
BoundAt: timestamppb.New(k.BoundAt),
|
||||
}
|
||||
}
|
||||
|
||||
api.JSON(w, &GetExternalAccountKeysResponse{
|
||||
EAKs: eaks,
|
||||
NextCursor: nextCursor,
|
||||
})
|
||||
// CreateExternalAccountKey writes the response for the EAB key POST endpoint
|
||||
func (h *ACMEAdminResponder) CreateExternalAccountKey(w http.ResponseWriter, r *http.Request) {
|
||||
api.WriteError(w, admin.NewError(admin.ErrorNotImplementedType, "this functionality is currently only available in Certificate Manager: https://smallstep.com/signup?product=cm"))
|
||||
}
|
||||
|
||||
// DeleteExternalAccountKey writes the response for the EAB key DELETE endpoint
|
||||
func (h *ACMEAdminResponder) DeleteExternalAccountKey(w http.ResponseWriter, r *http.Request) {
|
||||
api.WriteError(w, admin.NewError(admin.ErrorNotImplementedType, "this functionality is currently only available in Certificate Manager: https://smallstep.com/signup?product=cm"))
|
||||
}
|
||||
|
|
|
@ -10,19 +10,14 @@ import (
|
|||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/go-chi/chi"
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"github.com/google/go-cmp/cmp/cmpopts"
|
||||
"github.com/smallstep/assert"
|
||||
"github.com/smallstep/certificates/acme"
|
||||
"github.com/smallstep/certificates/authority/admin"
|
||||
"github.com/smallstep/certificates/authority/provisioner"
|
||||
"go.step.sm/linkedca"
|
||||
"google.golang.org/protobuf/encoding/protojson"
|
||||
"google.golang.org/protobuf/proto"
|
||||
"google.golang.org/protobuf/types/known/timestamppb"
|
||||
)
|
||||
|
||||
func readProtoJSON(r io.ReadCloser, m proto.Message) error {
|
||||
|
@ -37,7 +32,7 @@ func readProtoJSON(r io.ReadCloser, m proto.Message) error {
|
|||
func TestHandler_requireEABEnabled(t *testing.T) {
|
||||
type test struct {
|
||||
ctx context.Context
|
||||
db admin.DB
|
||||
adminDB admin.DB
|
||||
auth adminAuthority
|
||||
next nextHTTP
|
||||
err *admin.Error
|
||||
|
@ -98,7 +93,7 @@ func TestHandler_requireEABEnabled(t *testing.T) {
|
|||
return test{
|
||||
ctx: ctx,
|
||||
auth: auth,
|
||||
db: db,
|
||||
adminDB: db,
|
||||
err: err,
|
||||
statusCode: 400,
|
||||
}
|
||||
|
@ -134,9 +129,9 @@ func TestHandler_requireEABEnabled(t *testing.T) {
|
|||
},
|
||||
}
|
||||
return test{
|
||||
ctx: ctx,
|
||||
auth: auth,
|
||||
db: db,
|
||||
ctx: ctx,
|
||||
auth: auth,
|
||||
adminDB: db,
|
||||
next: func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Write(nil) // mock response with status 200
|
||||
},
|
||||
|
@ -149,9 +144,9 @@ func TestHandler_requireEABEnabled(t *testing.T) {
|
|||
tc := prep(t)
|
||||
t.Run(name, func(t *testing.T) {
|
||||
h := &Handler{
|
||||
db: tc.db,
|
||||
auth: tc.auth,
|
||||
acmeDB: nil,
|
||||
auth: tc.auth,
|
||||
adminDB: tc.adminDB,
|
||||
acmeDB: nil,
|
||||
}
|
||||
|
||||
req := httptest.NewRequest("GET", "/foo", nil) // chi routing is prepared in test setup
|
||||
|
@ -183,7 +178,7 @@ func TestHandler_requireEABEnabled(t *testing.T) {
|
|||
|
||||
func TestHandler_provisionerHasEABEnabled(t *testing.T) {
|
||||
type test struct {
|
||||
db admin.DB
|
||||
adminDB admin.DB
|
||||
auth adminAuthority
|
||||
provisionerName string
|
||||
want bool
|
||||
|
@ -223,7 +218,7 @@ func TestHandler_provisionerHasEABEnabled(t *testing.T) {
|
|||
}
|
||||
return test{
|
||||
auth: auth,
|
||||
db: db,
|
||||
adminDB: db,
|
||||
provisionerName: "provName",
|
||||
want: false,
|
||||
err: admin.WrapErrorISE(errors.New("force"), "error loading provisioner provName"),
|
||||
|
@ -252,7 +247,7 @@ func TestHandler_provisionerHasEABEnabled(t *testing.T) {
|
|||
}
|
||||
return test{
|
||||
auth: auth,
|
||||
db: db,
|
||||
adminDB: db,
|
||||
provisionerName: "provName",
|
||||
want: false,
|
||||
err: admin.WrapErrorISE(errors.New("force"), "error loading provisioner provName"),
|
||||
|
@ -285,7 +280,7 @@ func TestHandler_provisionerHasEABEnabled(t *testing.T) {
|
|||
}
|
||||
return test{
|
||||
auth: auth,
|
||||
db: db,
|
||||
adminDB: db,
|
||||
provisionerName: "provName",
|
||||
want: false,
|
||||
err: admin.WrapErrorISE(errors.New("force"), "error loading provisioner provName"),
|
||||
|
@ -319,7 +314,7 @@ func TestHandler_provisionerHasEABEnabled(t *testing.T) {
|
|||
},
|
||||
}
|
||||
return test{
|
||||
db: db,
|
||||
adminDB: db,
|
||||
auth: auth,
|
||||
provisionerName: "eab-disabled",
|
||||
want: false,
|
||||
|
@ -353,7 +348,7 @@ func TestHandler_provisionerHasEABEnabled(t *testing.T) {
|
|||
},
|
||||
}
|
||||
return test{
|
||||
db: db,
|
||||
adminDB: db,
|
||||
auth: auth,
|
||||
provisionerName: "eab-enabled",
|
||||
want: true,
|
||||
|
@ -364,9 +359,9 @@ func TestHandler_provisionerHasEABEnabled(t *testing.T) {
|
|||
tc := prep(t)
|
||||
t.Run(name, func(t *testing.T) {
|
||||
h := &Handler{
|
||||
db: tc.db,
|
||||
auth: tc.auth,
|
||||
acmeDB: nil,
|
||||
auth: tc.auth,
|
||||
adminDB: tc.adminDB,
|
||||
acmeDB: nil,
|
||||
}
|
||||
got, prov, err := h.provisionerHasEABEnabled(context.TODO(), tc.provisionerName)
|
||||
if (err != nil) != (tc.err != nil) {
|
||||
|
@ -391,54 +386,6 @@ func TestHandler_provisionerHasEABEnabled(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func Test_provisionerFromContext(t *testing.T) {
|
||||
prov := &linkedca.Provisioner{
|
||||
Id: "provID",
|
||||
Name: "acmeProv",
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
ctx context.Context
|
||||
want *linkedca.Provisioner
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "fail/no-provisioner",
|
||||
ctx: context.Background(),
|
||||
want: nil,
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "fail/wrong-type",
|
||||
ctx: context.WithValue(context.Background(), provisionerContextKey, "prov"),
|
||||
want: nil,
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "ok",
|
||||
ctx: context.WithValue(context.Background(), provisionerContextKey, prov),
|
||||
want: &linkedca.Provisioner{
|
||||
Id: "provID",
|
||||
Name: "acmeProv",
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got, err := provisionerFromContext(tt.ctx)
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("provisionerFromContext() error = %v, wantErr %v", err, tt.wantErr)
|
||||
return
|
||||
}
|
||||
opts := []cmp.Option{cmpopts.IgnoreUnexported(linkedca.Provisioner{})}
|
||||
if !cmp.Equal(tt.want, got, opts...) {
|
||||
t.Errorf("provisionerFromContext() diff =\n %s", cmp.Diff(tt.want, got, opts...))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCreateExternalAccountKeyRequest_Validate(t *testing.T) {
|
||||
type fields struct {
|
||||
Reference string
|
||||
|
@ -483,740 +430,158 @@ func TestCreateExternalAccountKeyRequest_Validate(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestHandler_CreateExternalAccountKey(t *testing.T) {
|
||||
prov := &linkedca.Provisioner{
|
||||
Id: "provID",
|
||||
Name: "provName",
|
||||
}
|
||||
type test struct {
|
||||
ctx context.Context
|
||||
db acme.DB
|
||||
body []byte
|
||||
statusCode int
|
||||
eak *linkedca.EABKey
|
||||
err *admin.Error
|
||||
}
|
||||
var tests = map[string]func(t *testing.T) test{
|
||||
"fail/ReadJSON": func(t *testing.T) test {
|
||||
"ok": func(t *testing.T) test {
|
||||
chiCtx := chi.NewRouteContext()
|
||||
chiCtx.URLParams.Add("provisionerName", "provName")
|
||||
ctx := context.WithValue(context.Background(), chi.RouteCtxKey, chiCtx)
|
||||
body := []byte("{!?}")
|
||||
return test{
|
||||
ctx: ctx,
|
||||
body: body,
|
||||
statusCode: 400,
|
||||
eak: nil,
|
||||
statusCode: 501,
|
||||
err: &admin.Error{
|
||||
Type: admin.ErrorBadRequestType.String(),
|
||||
Status: 400,
|
||||
Detail: "bad request",
|
||||
Message: "error reading request body: error decoding json: invalid character '!' looking for beginning of object key string",
|
||||
},
|
||||
}
|
||||
},
|
||||
"fail/validate": func(t *testing.T) test {
|
||||
chiCtx := chi.NewRouteContext()
|
||||
chiCtx.URLParams.Add("provisionerName", "provName")
|
||||
ctx := context.WithValue(context.Background(), chi.RouteCtxKey, chiCtx)
|
||||
req := CreateExternalAccountKeyRequest{
|
||||
Reference: strings.Repeat("A", 257),
|
||||
}
|
||||
body, err := json.Marshal(req)
|
||||
assert.FatalError(t, err)
|
||||
return test{
|
||||
ctx: ctx,
|
||||
body: body,
|
||||
statusCode: 400,
|
||||
eak: nil,
|
||||
err: &admin.Error{
|
||||
Type: admin.ErrorBadRequestType.String(),
|
||||
Status: 400,
|
||||
Detail: "bad request",
|
||||
Message: "error validating request body: reference length 257 exceeds the maximum (256)",
|
||||
},
|
||||
}
|
||||
},
|
||||
"fail/no-provisioner-in-context": func(t *testing.T) test {
|
||||
chiCtx := chi.NewRouteContext()
|
||||
chiCtx.URLParams.Add("provisionerName", "provName")
|
||||
ctx := context.WithValue(context.Background(), chi.RouteCtxKey, chiCtx)
|
||||
req := CreateExternalAccountKeyRequest{
|
||||
Reference: "aRef",
|
||||
}
|
||||
body, err := json.Marshal(req)
|
||||
assert.FatalError(t, err)
|
||||
return test{
|
||||
ctx: ctx,
|
||||
body: body,
|
||||
statusCode: 500,
|
||||
eak: nil,
|
||||
err: &admin.Error{
|
||||
Type: admin.ErrorServerInternalType.String(),
|
||||
Status: 500,
|
||||
Detail: "the server experienced an internal error",
|
||||
Message: "error getting provisioner from context: provisioner expected in request context",
|
||||
},
|
||||
}
|
||||
},
|
||||
"fail/acmeDB.GetExternalAccountKeyByReference": func(t *testing.T) test {
|
||||
chiCtx := chi.NewRouteContext()
|
||||
chiCtx.URLParams.Add("provisionerName", "provName")
|
||||
ctx := context.WithValue(context.Background(), chi.RouteCtxKey, chiCtx)
|
||||
ctx = context.WithValue(ctx, provisionerContextKey, prov)
|
||||
req := CreateExternalAccountKeyRequest{
|
||||
Reference: "an-external-key-reference",
|
||||
}
|
||||
body, err := json.Marshal(req)
|
||||
assert.FatalError(t, err)
|
||||
db := &acme.MockDB{
|
||||
MockGetExternalAccountKeyByReference: func(ctx context.Context, provisionerID, reference string) (*acme.ExternalAccountKey, error) {
|
||||
assert.Equals(t, "provID", provisionerID)
|
||||
assert.Equals(t, "an-external-key-reference", reference)
|
||||
return nil, errors.New("force")
|
||||
},
|
||||
}
|
||||
return test{
|
||||
ctx: ctx,
|
||||
db: db,
|
||||
body: body,
|
||||
statusCode: 500,
|
||||
eak: nil,
|
||||
err: &admin.Error{
|
||||
Type: admin.ErrorServerInternalType.String(),
|
||||
Status: 500,
|
||||
Detail: "the server experienced an internal error",
|
||||
Message: "could not lookup external account key by reference: force",
|
||||
},
|
||||
}
|
||||
},
|
||||
"fail/reference-conflict-409": func(t *testing.T) test {
|
||||
chiCtx := chi.NewRouteContext()
|
||||
chiCtx.URLParams.Add("provisionerName", "provName")
|
||||
ctx := context.WithValue(context.Background(), chi.RouteCtxKey, chiCtx)
|
||||
ctx = context.WithValue(ctx, provisionerContextKey, prov)
|
||||
req := CreateExternalAccountKeyRequest{
|
||||
Reference: "an-external-key-reference",
|
||||
}
|
||||
body, err := json.Marshal(req)
|
||||
assert.FatalError(t, err)
|
||||
db := &acme.MockDB{
|
||||
MockGetExternalAccountKeyByReference: func(ctx context.Context, provisionerID, reference string) (*acme.ExternalAccountKey, error) {
|
||||
assert.Equals(t, "provID", provisionerID)
|
||||
assert.Equals(t, "an-external-key-reference", reference)
|
||||
past := time.Now().Add(-24 * time.Hour)
|
||||
return &acme.ExternalAccountKey{
|
||||
ID: "eakID",
|
||||
ProvisionerID: "provID",
|
||||
Reference: "an-external-key-reference",
|
||||
KeyBytes: []byte{1, 3, 3, 7},
|
||||
CreatedAt: past,
|
||||
}, nil
|
||||
},
|
||||
}
|
||||
return test{
|
||||
ctx: ctx,
|
||||
db: db,
|
||||
body: body,
|
||||
statusCode: 409,
|
||||
eak: nil,
|
||||
err: &admin.Error{
|
||||
Type: admin.ErrorBadRequestType.String(),
|
||||
Status: 409,
|
||||
Detail: "bad request",
|
||||
Message: "an ACME EAB key for provisioner 'provName' with reference 'an-external-key-reference' already exists",
|
||||
},
|
||||
}
|
||||
},
|
||||
"fail/acmeDB.CreateExternalAccountKey-no-reference": func(t *testing.T) test {
|
||||
chiCtx := chi.NewRouteContext()
|
||||
chiCtx.URLParams.Add("provisionerName", "provName")
|
||||
ctx := context.WithValue(context.Background(), chi.RouteCtxKey, chiCtx)
|
||||
ctx = context.WithValue(ctx, provisionerContextKey, prov)
|
||||
req := CreateExternalAccountKeyRequest{
|
||||
Reference: "",
|
||||
}
|
||||
body, err := json.Marshal(req)
|
||||
assert.FatalError(t, err)
|
||||
db := &acme.MockDB{
|
||||
MockCreateExternalAccountKey: func(ctx context.Context, provisionerID, reference string) (*acme.ExternalAccountKey, error) {
|
||||
assert.Equals(t, "provID", provisionerID)
|
||||
assert.Equals(t, "", reference)
|
||||
return nil, errors.New("force")
|
||||
},
|
||||
}
|
||||
return test{
|
||||
ctx: ctx,
|
||||
db: db,
|
||||
body: body,
|
||||
statusCode: 500,
|
||||
err: &admin.Error{
|
||||
Type: admin.ErrorServerInternalType.String(),
|
||||
Status: 500,
|
||||
Detail: "the server experienced an internal error",
|
||||
Message: "error creating ACME EAB key for provisioner 'provName': force",
|
||||
},
|
||||
}
|
||||
},
|
||||
"fail/acmeDB.CreateExternalAccountKey-with-reference": func(t *testing.T) test {
|
||||
chiCtx := chi.NewRouteContext()
|
||||
chiCtx.URLParams.Add("provisionerName", "provName")
|
||||
ctx := context.WithValue(context.Background(), chi.RouteCtxKey, chiCtx)
|
||||
ctx = context.WithValue(ctx, provisionerContextKey, prov)
|
||||
req := CreateExternalAccountKeyRequest{
|
||||
Reference: "an-external-key-reference",
|
||||
}
|
||||
body, err := json.Marshal(req)
|
||||
assert.FatalError(t, err)
|
||||
db := &acme.MockDB{
|
||||
MockGetExternalAccountKeyByReference: func(ctx context.Context, provisionerID, reference string) (*acme.ExternalAccountKey, error) {
|
||||
assert.Equals(t, "provID", provisionerID)
|
||||
assert.Equals(t, "an-external-key-reference", reference)
|
||||
return nil, acme.ErrNotFound // simulating not found; skipping 409 conflict
|
||||
},
|
||||
MockCreateExternalAccountKey: func(ctx context.Context, provisionerID, reference string) (*acme.ExternalAccountKey, error) {
|
||||
assert.Equals(t, "provID", provisionerID)
|
||||
assert.Equals(t, "an-external-key-reference", reference)
|
||||
return nil, errors.New("force")
|
||||
},
|
||||
}
|
||||
return test{
|
||||
ctx: ctx,
|
||||
db: db,
|
||||
body: body,
|
||||
statusCode: 500,
|
||||
err: &admin.Error{
|
||||
Type: admin.ErrorServerInternalType.String(),
|
||||
Status: 500,
|
||||
Detail: "the server experienced an internal error",
|
||||
Message: "error creating ACME EAB key for provisioner 'provName' and reference 'an-external-key-reference': force",
|
||||
},
|
||||
}
|
||||
},
|
||||
"ok/no-reference": func(t *testing.T) test {
|
||||
chiCtx := chi.NewRouteContext()
|
||||
chiCtx.URLParams.Add("provisionerName", "provName")
|
||||
ctx := context.WithValue(context.Background(), chi.RouteCtxKey, chiCtx)
|
||||
ctx = context.WithValue(ctx, provisionerContextKey, prov)
|
||||
req := CreateExternalAccountKeyRequest{
|
||||
Reference: "",
|
||||
}
|
||||
body, err := json.Marshal(req)
|
||||
assert.FatalError(t, err)
|
||||
now := time.Now()
|
||||
db := &acme.MockDB{
|
||||
MockCreateExternalAccountKey: func(ctx context.Context, provisionerID, reference string) (*acme.ExternalAccountKey, error) {
|
||||
assert.Equals(t, "provID", provisionerID)
|
||||
assert.Equals(t, "", reference)
|
||||
return &acme.ExternalAccountKey{
|
||||
ID: "eakID",
|
||||
ProvisionerID: "provID",
|
||||
Reference: "",
|
||||
KeyBytes: []byte{1, 3, 3, 7},
|
||||
CreatedAt: now,
|
||||
}, nil
|
||||
},
|
||||
}
|
||||
return test{
|
||||
ctx: ctx,
|
||||
db: db,
|
||||
body: body,
|
||||
statusCode: 201,
|
||||
eak: &linkedca.EABKey{
|
||||
Id: "eakID",
|
||||
Provisioner: "provName",
|
||||
Reference: "",
|
||||
HmacKey: []byte{1, 3, 3, 7},
|
||||
},
|
||||
}
|
||||
},
|
||||
"ok/with-reference": func(t *testing.T) test {
|
||||
chiCtx := chi.NewRouteContext()
|
||||
chiCtx.URLParams.Add("provisionerName", "provName")
|
||||
ctx := context.WithValue(context.Background(), chi.RouteCtxKey, chiCtx)
|
||||
ctx = context.WithValue(ctx, provisionerContextKey, prov)
|
||||
req := CreateExternalAccountKeyRequest{
|
||||
Reference: "an-external-key-reference",
|
||||
}
|
||||
body, err := json.Marshal(req)
|
||||
assert.FatalError(t, err)
|
||||
now := time.Now()
|
||||
db := &acme.MockDB{
|
||||
MockGetExternalAccountKeyByReference: func(ctx context.Context, provisionerID, reference string) (*acme.ExternalAccountKey, error) {
|
||||
assert.Equals(t, "provID", provisionerID)
|
||||
assert.Equals(t, "an-external-key-reference", reference)
|
||||
return nil, acme.ErrNotFound // simulating not found; skipping 409 conflict
|
||||
},
|
||||
MockCreateExternalAccountKey: func(ctx context.Context, provisionerID, reference string) (*acme.ExternalAccountKey, error) {
|
||||
assert.Equals(t, "provID", provisionerID)
|
||||
assert.Equals(t, "an-external-key-reference", reference)
|
||||
return &acme.ExternalAccountKey{
|
||||
ID: "eakID",
|
||||
ProvisionerID: "provID",
|
||||
Reference: "an-external-key-reference",
|
||||
KeyBytes: []byte{1, 3, 3, 7},
|
||||
CreatedAt: now,
|
||||
}, nil
|
||||
},
|
||||
}
|
||||
return test{
|
||||
ctx: ctx,
|
||||
db: db,
|
||||
body: body,
|
||||
statusCode: 201,
|
||||
eak: &linkedca.EABKey{
|
||||
Id: "eakID",
|
||||
Provisioner: "provName",
|
||||
Reference: "an-external-key-reference",
|
||||
HmacKey: []byte{1, 3, 3, 7},
|
||||
Type: admin.ErrorNotImplementedType.String(),
|
||||
Status: http.StatusNotImplemented,
|
||||
Message: "this functionality is currently only available in Certificate Manager: https://smallstep.com/signup?product=cm",
|
||||
Detail: "not implemented",
|
||||
},
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
for name, prep := range tests {
|
||||
tc := prep(t)
|
||||
t.Run(name, func(t *testing.T) {
|
||||
h := &Handler{
|
||||
acmeDB: tc.db,
|
||||
}
|
||||
req := httptest.NewRequest("POST", "/foo", io.NopCloser(bytes.NewBuffer(tc.body))) // chi routing is prepared in test setup
|
||||
|
||||
req := httptest.NewRequest("POST", "/foo", nil) // chi routing is prepared in test setup
|
||||
req = req.WithContext(tc.ctx)
|
||||
w := httptest.NewRecorder()
|
||||
h.CreateExternalAccountKey(w, req)
|
||||
acmeResponder := NewACMEAdminResponder()
|
||||
acmeResponder.CreateExternalAccountKey(w, req)
|
||||
res := w.Result()
|
||||
assert.Equals(t, tc.statusCode, res.StatusCode)
|
||||
|
||||
if res.StatusCode >= 400 {
|
||||
|
||||
body, err := io.ReadAll(res.Body)
|
||||
res.Body.Close()
|
||||
assert.FatalError(t, err)
|
||||
|
||||
adminErr := admin.Error{}
|
||||
assert.FatalError(t, json.Unmarshal(bytes.TrimSpace(body), &adminErr))
|
||||
|
||||
assert.Equals(t, tc.err.Type, adminErr.Type)
|
||||
assert.Equals(t, tc.err.Message, adminErr.Message)
|
||||
assert.Equals(t, tc.err.StatusCode(), res.StatusCode)
|
||||
assert.Equals(t, tc.err.Detail, adminErr.Detail)
|
||||
assert.Equals(t, []string{"application/json"}, res.Header["Content-Type"])
|
||||
return
|
||||
}
|
||||
|
||||
eabKey := &linkedca.EABKey{}
|
||||
err := readProtoJSON(res.Body, eabKey)
|
||||
body, err := io.ReadAll(res.Body)
|
||||
res.Body.Close()
|
||||
assert.FatalError(t, err)
|
||||
assert.Equals(t, []string{"application/json"}, res.Header["Content-Type"])
|
||||
|
||||
opts := []cmp.Option{cmpopts.IgnoreUnexported(linkedca.EABKey{})}
|
||||
if !cmp.Equal(tc.eak, eabKey, opts...) {
|
||||
t.Errorf("h.CreateExternalAccountKey diff =\n%s", cmp.Diff(tc.eak, eabKey, opts...))
|
||||
}
|
||||
adminErr := admin.Error{}
|
||||
assert.FatalError(t, json.Unmarshal(bytes.TrimSpace(body), &adminErr))
|
||||
|
||||
assert.Equals(t, tc.err.Type, adminErr.Type)
|
||||
assert.Equals(t, tc.err.Message, adminErr.Message)
|
||||
assert.Equals(t, tc.err.StatusCode(), res.StatusCode)
|
||||
assert.Equals(t, tc.err.Detail, adminErr.Detail)
|
||||
assert.Equals(t, []string{"application/json"}, res.Header["Content-Type"])
|
||||
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestHandler_DeleteExternalAccountKey(t *testing.T) {
|
||||
prov := &linkedca.Provisioner{
|
||||
Id: "provID",
|
||||
Name: "provName",
|
||||
}
|
||||
type test struct {
|
||||
ctx context.Context
|
||||
db acme.DB
|
||||
statusCode int
|
||||
err *admin.Error
|
||||
}
|
||||
var tests = map[string]func(t *testing.T) test{
|
||||
"fail/no-provisioner-in-context": func(t *testing.T) test {
|
||||
chiCtx := chi.NewRouteContext()
|
||||
chiCtx.URLParams.Add("provisionerName", "provName")
|
||||
ctx := context.WithValue(context.Background(), chi.RouteCtxKey, chiCtx)
|
||||
return test{
|
||||
ctx: ctx,
|
||||
statusCode: 500,
|
||||
err: &admin.Error{
|
||||
Type: admin.ErrorServerInternalType.String(),
|
||||
Status: 500,
|
||||
Detail: "the server experienced an internal error",
|
||||
Message: "error getting provisioner from context: provisioner expected in request context",
|
||||
},
|
||||
}
|
||||
},
|
||||
"fail/acmeDB.DeleteExternalAccountKey": func(t *testing.T) test {
|
||||
chiCtx := chi.NewRouteContext()
|
||||
chiCtx.URLParams.Add("provisionerName", "provName")
|
||||
chiCtx.URLParams.Add("id", "keyID")
|
||||
ctx := context.WithValue(context.Background(), chi.RouteCtxKey, chiCtx)
|
||||
ctx = context.WithValue(ctx, provisionerContextKey, prov)
|
||||
db := &acme.MockDB{
|
||||
MockDeleteExternalAccountKey: func(ctx context.Context, provisionerID, keyID string) error {
|
||||
assert.Equals(t, "provID", provisionerID)
|
||||
assert.Equals(t, "keyID", keyID)
|
||||
return errors.New("force")
|
||||
},
|
||||
}
|
||||
return test{
|
||||
ctx: ctx,
|
||||
db: db,
|
||||
statusCode: 500,
|
||||
err: &admin.Error{
|
||||
Type: admin.ErrorServerInternalType.String(),
|
||||
Status: 500,
|
||||
Detail: "the server experienced an internal error",
|
||||
Message: "error deleting ACME EAB Key 'keyID': force",
|
||||
},
|
||||
}
|
||||
},
|
||||
"ok": func(t *testing.T) test {
|
||||
chiCtx := chi.NewRouteContext()
|
||||
chiCtx.URLParams.Add("provisionerName", "provName")
|
||||
chiCtx.URLParams.Add("id", "keyID")
|
||||
ctx := context.WithValue(context.Background(), chi.RouteCtxKey, chiCtx)
|
||||
ctx = context.WithValue(ctx, provisionerContextKey, prov)
|
||||
db := &acme.MockDB{
|
||||
MockDeleteExternalAccountKey: func(ctx context.Context, provisionerID, keyID string) error {
|
||||
assert.Equals(t, "provID", provisionerID)
|
||||
assert.Equals(t, "keyID", keyID)
|
||||
return nil
|
||||
},
|
||||
}
|
||||
return test{
|
||||
ctx: ctx,
|
||||
db: db,
|
||||
statusCode: 200,
|
||||
err: nil,
|
||||
statusCode: 501,
|
||||
err: &admin.Error{
|
||||
Type: admin.ErrorNotImplementedType.String(),
|
||||
Status: http.StatusNotImplemented,
|
||||
Message: "this functionality is currently only available in Certificate Manager: https://smallstep.com/signup?product=cm",
|
||||
Detail: "not implemented",
|
||||
},
|
||||
}
|
||||
},
|
||||
}
|
||||
for name, prep := range tests {
|
||||
tc := prep(t)
|
||||
t.Run(name, func(t *testing.T) {
|
||||
h := &Handler{
|
||||
acmeDB: tc.db,
|
||||
}
|
||||
|
||||
req := httptest.NewRequest("DELETE", "/foo", nil) // chi routing is prepared in test setup
|
||||
req = req.WithContext(tc.ctx)
|
||||
w := httptest.NewRecorder()
|
||||
h.DeleteExternalAccountKey(w, req)
|
||||
acmeResponder := NewACMEAdminResponder()
|
||||
acmeResponder.DeleteExternalAccountKey(w, req)
|
||||
res := w.Result()
|
||||
assert.Equals(t, tc.statusCode, res.StatusCode)
|
||||
|
||||
if res.StatusCode >= 400 {
|
||||
body, err := io.ReadAll(res.Body)
|
||||
res.Body.Close()
|
||||
assert.FatalError(t, err)
|
||||
|
||||
adminErr := admin.Error{}
|
||||
assert.FatalError(t, json.Unmarshal(bytes.TrimSpace(body), &adminErr))
|
||||
|
||||
assert.Equals(t, tc.err.Type, adminErr.Type)
|
||||
assert.Equals(t, tc.err.Message, adminErr.Message)
|
||||
assert.Equals(t, tc.err.StatusCode(), res.StatusCode)
|
||||
assert.Equals(t, tc.err.Detail, adminErr.Detail)
|
||||
assert.Equals(t, []string{"application/json"}, res.Header["Content-Type"])
|
||||
return
|
||||
}
|
||||
|
||||
body, err := io.ReadAll(res.Body)
|
||||
res.Body.Close()
|
||||
assert.FatalError(t, err)
|
||||
|
||||
response := DeleteResponse{}
|
||||
assert.FatalError(t, json.Unmarshal(bytes.TrimSpace(body), &response))
|
||||
assert.Equals(t, "ok", response.Status)
|
||||
assert.Equals(t, []string{"application/json"}, res.Header["Content-Type"])
|
||||
adminErr := admin.Error{}
|
||||
assert.FatalError(t, json.Unmarshal(bytes.TrimSpace(body), &adminErr))
|
||||
|
||||
assert.Equals(t, tc.err.Type, adminErr.Type)
|
||||
assert.Equals(t, tc.err.Message, adminErr.Message)
|
||||
assert.Equals(t, tc.err.StatusCode(), res.StatusCode)
|
||||
assert.Equals(t, tc.err.Detail, adminErr.Detail)
|
||||
assert.Equals(t, []string{"application/json"}, res.Header["Content-Type"])
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestHandler_GetExternalAccountKeys(t *testing.T) {
|
||||
prov := &linkedca.Provisioner{
|
||||
Id: "provID",
|
||||
Name: "provName",
|
||||
}
|
||||
type test struct {
|
||||
ctx context.Context
|
||||
db acme.DB
|
||||
statusCode int
|
||||
req *http.Request
|
||||
resp GetExternalAccountKeysResponse
|
||||
err *admin.Error
|
||||
}
|
||||
var tests = map[string]func(t *testing.T) test{
|
||||
"fail/no-provisioner-in-context": func(t *testing.T) test {
|
||||
"ok": func(t *testing.T) test {
|
||||
chiCtx := chi.NewRouteContext()
|
||||
chiCtx.URLParams.Add("provisionerName", "provName")
|
||||
ctx := context.WithValue(context.Background(), chi.RouteCtxKey, chiCtx)
|
||||
req := httptest.NewRequest("GET", "/foo", nil)
|
||||
ctx := context.WithValue(context.Background(), chi.RouteCtxKey, chiCtx)
|
||||
return test{
|
||||
ctx: ctx,
|
||||
statusCode: 500,
|
||||
statusCode: 501,
|
||||
req: req,
|
||||
err: &admin.Error{
|
||||
Type: admin.ErrorServerInternalType.String(),
|
||||
Status: 500,
|
||||
Detail: "the server experienced an internal error",
|
||||
Message: "error getting provisioner from context: provisioner expected in request context",
|
||||
Type: admin.ErrorNotImplementedType.String(),
|
||||
Status: http.StatusNotImplemented,
|
||||
Message: "this functionality is currently only available in Certificate Manager: https://smallstep.com/signup?product=cm",
|
||||
Detail: "not implemented",
|
||||
},
|
||||
}
|
||||
},
|
||||
"fail/parse-cursor": func(t *testing.T) test {
|
||||
chiCtx := chi.NewRouteContext()
|
||||
chiCtx.URLParams.Add("provisionerName", "provName")
|
||||
req := httptest.NewRequest("GET", "/foo?limit=A", nil)
|
||||
ctx := context.WithValue(context.Background(), chi.RouteCtxKey, chiCtx)
|
||||
ctx = context.WithValue(ctx, provisionerContextKey, prov)
|
||||
return test{
|
||||
ctx: ctx,
|
||||
statusCode: 400,
|
||||
req: req,
|
||||
err: &admin.Error{
|
||||
Status: 400,
|
||||
Type: admin.ErrorBadRequestType.String(),
|
||||
Detail: "bad request",
|
||||
Message: "error parsing cursor and limit from query params: limit 'A' is not an integer: strconv.Atoi: parsing \"A\": invalid syntax",
|
||||
},
|
||||
}
|
||||
},
|
||||
"fail/acmeDB.GetExternalAccountKeyByReference": func(t *testing.T) test {
|
||||
chiCtx := chi.NewRouteContext()
|
||||
chiCtx.URLParams.Add("provisionerName", "provName")
|
||||
chiCtx.URLParams.Add("reference", "an-external-key-reference")
|
||||
req := httptest.NewRequest("GET", "/foo", nil)
|
||||
ctx := context.WithValue(context.Background(), chi.RouteCtxKey, chiCtx)
|
||||
ctx = context.WithValue(ctx, provisionerContextKey, prov)
|
||||
db := &acme.MockDB{
|
||||
MockGetExternalAccountKeyByReference: func(ctx context.Context, provisionerID, reference string) (*acme.ExternalAccountKey, error) {
|
||||
assert.Equals(t, "provID", provisionerID)
|
||||
assert.Equals(t, "an-external-key-reference", reference)
|
||||
return nil, errors.New("force")
|
||||
},
|
||||
}
|
||||
return test{
|
||||
ctx: ctx,
|
||||
statusCode: 500,
|
||||
req: req,
|
||||
db: db,
|
||||
err: &admin.Error{
|
||||
Status: 500,
|
||||
Type: admin.ErrorServerInternalType.String(),
|
||||
Detail: "the server experienced an internal error",
|
||||
Message: "error retrieving external account key with reference 'an-external-key-reference': force",
|
||||
},
|
||||
}
|
||||
},
|
||||
"fail/acmeDB.GetExternalAccountKeys": func(t *testing.T) test {
|
||||
chiCtx := chi.NewRouteContext()
|
||||
chiCtx.URLParams.Add("provisionerName", "provName")
|
||||
req := httptest.NewRequest("GET", "/foo", nil)
|
||||
ctx := context.WithValue(context.Background(), chi.RouteCtxKey, chiCtx)
|
||||
ctx = context.WithValue(ctx, provisionerContextKey, prov)
|
||||
db := &acme.MockDB{
|
||||
MockGetExternalAccountKeys: func(ctx context.Context, provisionerID, cursor string, limit int) ([]*acme.ExternalAccountKey, string, error) {
|
||||
assert.Equals(t, "provID", provisionerID)
|
||||
assert.Equals(t, "", cursor)
|
||||
assert.Equals(t, 0, limit)
|
||||
return nil, "", errors.New("force")
|
||||
},
|
||||
}
|
||||
return test{
|
||||
ctx: ctx,
|
||||
statusCode: 500,
|
||||
req: req,
|
||||
db: db,
|
||||
err: &admin.Error{
|
||||
Status: 500,
|
||||
Type: admin.ErrorServerInternalType.String(),
|
||||
Detail: "the server experienced an internal error",
|
||||
Message: "error retrieving external account keys: force",
|
||||
},
|
||||
}
|
||||
},
|
||||
"ok/reference-not-found": func(t *testing.T) test {
|
||||
chiCtx := chi.NewRouteContext()
|
||||
chiCtx.URLParams.Add("provisionerName", "provName")
|
||||
chiCtx.URLParams.Add("reference", "an-external-key-reference")
|
||||
req := httptest.NewRequest("GET", "/foo", nil)
|
||||
ctx := context.WithValue(context.Background(), chi.RouteCtxKey, chiCtx)
|
||||
ctx = context.WithValue(ctx, provisionerContextKey, prov)
|
||||
db := &acme.MockDB{
|
||||
MockGetExternalAccountKeyByReference: func(ctx context.Context, provisionerID, reference string) (*acme.ExternalAccountKey, error) {
|
||||
assert.Equals(t, "provID", provisionerID)
|
||||
assert.Equals(t, "an-external-key-reference", reference)
|
||||
return nil, nil // returning nil; no key found
|
||||
},
|
||||
}
|
||||
return test{
|
||||
ctx: ctx,
|
||||
statusCode: 200,
|
||||
req: req,
|
||||
resp: GetExternalAccountKeysResponse{
|
||||
EAKs: []*linkedca.EABKey{},
|
||||
},
|
||||
db: db,
|
||||
err: nil,
|
||||
}
|
||||
},
|
||||
"ok/reference-found": func(t *testing.T) test {
|
||||
chiCtx := chi.NewRouteContext()
|
||||
chiCtx.URLParams.Add("provisionerName", "provName")
|
||||
chiCtx.URLParams.Add("reference", "an-external-key-reference")
|
||||
req := httptest.NewRequest("GET", "/foo", nil)
|
||||
ctx := context.WithValue(context.Background(), chi.RouteCtxKey, chiCtx)
|
||||
ctx = context.WithValue(ctx, provisionerContextKey, prov)
|
||||
createdAt := time.Now().Add(-24 * time.Hour)
|
||||
var boundAt time.Time
|
||||
db := &acme.MockDB{
|
||||
MockGetExternalAccountKeyByReference: func(ctx context.Context, provisionerID, reference string) (*acme.ExternalAccountKey, error) {
|
||||
assert.Equals(t, "provID", provisionerID)
|
||||
assert.Equals(t, "an-external-key-reference", reference)
|
||||
return &acme.ExternalAccountKey{
|
||||
ID: "eakID",
|
||||
ProvisionerID: "provID",
|
||||
Reference: "an-external-key-reference",
|
||||
CreatedAt: createdAt,
|
||||
}, nil
|
||||
},
|
||||
}
|
||||
return test{
|
||||
ctx: ctx,
|
||||
statusCode: 200,
|
||||
req: req,
|
||||
resp: GetExternalAccountKeysResponse{
|
||||
EAKs: []*linkedca.EABKey{
|
||||
{
|
||||
Id: "eakID",
|
||||
Provisioner: "provName",
|
||||
Reference: "an-external-key-reference",
|
||||
CreatedAt: timestamppb.New(createdAt),
|
||||
BoundAt: timestamppb.New(boundAt),
|
||||
},
|
||||
},
|
||||
},
|
||||
db: db,
|
||||
err: nil,
|
||||
}
|
||||
},
|
||||
"ok/multiple-keys": func(t *testing.T) test {
|
||||
chiCtx := chi.NewRouteContext()
|
||||
chiCtx.URLParams.Add("provisionerName", "provName")
|
||||
req := httptest.NewRequest("GET", "/foo", nil)
|
||||
ctx := context.WithValue(context.Background(), chi.RouteCtxKey, chiCtx)
|
||||
ctx = context.WithValue(ctx, provisionerContextKey, prov)
|
||||
createdAt := time.Now().Add(-24 * time.Hour)
|
||||
var boundAt time.Time
|
||||
boundAtSet := time.Now().Add(-12 * time.Hour)
|
||||
db := &acme.MockDB{
|
||||
MockGetExternalAccountKeys: func(ctx context.Context, provisionerID, cursor string, limit int) ([]*acme.ExternalAccountKey, string, error) {
|
||||
assert.Equals(t, "provID", provisionerID)
|
||||
assert.Equals(t, "", cursor)
|
||||
assert.Equals(t, 0, limit)
|
||||
return []*acme.ExternalAccountKey{
|
||||
{
|
||||
ID: "eakID1",
|
||||
ProvisionerID: "provID",
|
||||
Reference: "some-external-key-reference",
|
||||
KeyBytes: []byte{1, 3, 3, 7},
|
||||
CreatedAt: createdAt,
|
||||
},
|
||||
{
|
||||
ID: "eakID2",
|
||||
ProvisionerID: "provID",
|
||||
Reference: "some-other-external-key-reference",
|
||||
KeyBytes: []byte{1, 3, 3, 7},
|
||||
CreatedAt: createdAt.Add(1 * time.Hour),
|
||||
},
|
||||
{
|
||||
ID: "eakID3",
|
||||
ProvisionerID: "provID",
|
||||
Reference: "another-external-key-reference",
|
||||
KeyBytes: []byte{1, 3, 3, 7},
|
||||
CreatedAt: createdAt,
|
||||
BoundAt: boundAtSet,
|
||||
AccountID: "accountID",
|
||||
},
|
||||
}, "", nil
|
||||
},
|
||||
}
|
||||
return test{
|
||||
ctx: ctx,
|
||||
statusCode: 200,
|
||||
req: req,
|
||||
resp: GetExternalAccountKeysResponse{
|
||||
EAKs: []*linkedca.EABKey{
|
||||
{
|
||||
Id: "eakID1",
|
||||
Provisioner: "provName",
|
||||
Reference: "some-external-key-reference",
|
||||
CreatedAt: timestamppb.New(createdAt),
|
||||
BoundAt: timestamppb.New(boundAt),
|
||||
},
|
||||
{
|
||||
Id: "eakID2",
|
||||
Provisioner: "provName",
|
||||
Reference: "some-other-external-key-reference",
|
||||
CreatedAt: timestamppb.New(createdAt.Add(1 * time.Hour)),
|
||||
BoundAt: timestamppb.New(boundAt),
|
||||
},
|
||||
{
|
||||
Id: "eakID3",
|
||||
Provisioner: "provName",
|
||||
Reference: "another-external-key-reference",
|
||||
CreatedAt: timestamppb.New(createdAt),
|
||||
BoundAt: timestamppb.New(boundAtSet),
|
||||
Account: "accountID",
|
||||
},
|
||||
},
|
||||
},
|
||||
db: db,
|
||||
err: nil,
|
||||
}
|
||||
},
|
||||
}
|
||||
for name, prep := range tests {
|
||||
tc := prep(t)
|
||||
t.Run(name, func(t *testing.T) {
|
||||
h := &Handler{
|
||||
acmeDB: tc.db,
|
||||
}
|
||||
|
||||
req := tc.req.WithContext(tc.ctx)
|
||||
w := httptest.NewRecorder()
|
||||
h.GetExternalAccountKeys(w, req)
|
||||
acmeResponder := NewACMEAdminResponder()
|
||||
acmeResponder.GetExternalAccountKeys(w, req)
|
||||
|
||||
res := w.Result()
|
||||
assert.Equals(t, tc.statusCode, res.StatusCode)
|
||||
|
||||
if res.StatusCode >= 400 {
|
||||
body, err := io.ReadAll(res.Body)
|
||||
res.Body.Close()
|
||||
assert.FatalError(t, err)
|
||||
|
||||
adminErr := admin.Error{}
|
||||
assert.FatalError(t, json.Unmarshal(bytes.TrimSpace(body), &adminErr))
|
||||
|
||||
assert.Equals(t, tc.err.Type, adminErr.Type)
|
||||
assert.Equals(t, tc.err.Message, adminErr.Message)
|
||||
assert.Equals(t, tc.err.StatusCode(), res.StatusCode)
|
||||
assert.Equals(t, tc.err.Detail, adminErr.Detail)
|
||||
assert.Equals(t, []string{"application/json"}, res.Header["Content-Type"])
|
||||
return
|
||||
}
|
||||
|
||||
body, err := io.ReadAll(res.Body)
|
||||
res.Body.Close()
|
||||
assert.FatalError(t, err)
|
||||
|
||||
response := GetExternalAccountKeysResponse{}
|
||||
assert.FatalError(t, json.Unmarshal(bytes.TrimSpace(body), &response))
|
||||
adminErr := admin.Error{}
|
||||
assert.FatalError(t, json.Unmarshal(bytes.TrimSpace(body), &adminErr))
|
||||
|
||||
assert.Equals(t, tc.err.Type, adminErr.Type)
|
||||
assert.Equals(t, tc.err.Message, adminErr.Message)
|
||||
assert.Equals(t, tc.err.StatusCode(), res.StatusCode)
|
||||
assert.Equals(t, tc.err.Detail, adminErr.Detail)
|
||||
assert.Equals(t, []string{"application/json"}, res.Header["Content-Type"])
|
||||
|
||||
opts := []cmp.Option{cmpopts.IgnoreUnexported(linkedca.EABKey{}, timestamppb.Timestamp{})}
|
||||
if !cmp.Equal(tc.resp, response, opts...) {
|
||||
t.Errorf("h.GetExternalAccountKeys diff =\n%s", cmp.Diff(tc.resp, response, opts...))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -8,17 +8,19 @@ import (
|
|||
|
||||
// Handler is the Admin API request handler.
|
||||
type Handler struct {
|
||||
db admin.DB
|
||||
auth adminAuthority
|
||||
acmeDB acme.DB
|
||||
adminDB admin.DB
|
||||
auth adminAuthority
|
||||
acmeDB acme.DB
|
||||
acmeResponder acmeAdminResponderInterface
|
||||
}
|
||||
|
||||
// NewHandler returns a new Authority Config Handler.
|
||||
func NewHandler(auth adminAuthority, adminDB admin.DB, acmeDB acme.DB) api.RouterHandler {
|
||||
func NewHandler(auth adminAuthority, adminDB admin.DB, acmeDB acme.DB, acmeResponder acmeAdminResponderInterface) api.RouterHandler {
|
||||
return &Handler{
|
||||
db: adminDB,
|
||||
auth: auth,
|
||||
acmeDB: acmeDB,
|
||||
auth: auth,
|
||||
adminDB: adminDB,
|
||||
acmeDB: acmeDB,
|
||||
acmeResponder: acmeResponder,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -47,8 +49,8 @@ func (h *Handler) Route(r api.Router) {
|
|||
r.MethodFunc("DELETE", "/admins/{id}", authnz(h.DeleteAdmin))
|
||||
|
||||
// ACME External Account Binding Keys
|
||||
r.MethodFunc("GET", "/acme/eab/{provisionerName}/{reference}", authnz(requireEABEnabled(h.GetExternalAccountKeys)))
|
||||
r.MethodFunc("GET", "/acme/eab/{provisionerName}", authnz(requireEABEnabled(h.GetExternalAccountKeys)))
|
||||
r.MethodFunc("POST", "/acme/eab/{provisionerName}", authnz(requireEABEnabled(h.CreateExternalAccountKey)))
|
||||
r.MethodFunc("DELETE", "/acme/eab/{provisionerName}/{id}", authnz(requireEABEnabled(h.DeleteExternalAccountKey)))
|
||||
r.MethodFunc("GET", "/acme/eab/{provisionerName}/{reference}", authnz(requireEABEnabled(h.acmeResponder.GetExternalAccountKeys)))
|
||||
r.MethodFunc("GET", "/acme/eab/{provisionerName}", authnz(requireEABEnabled(h.acmeResponder.GetExternalAccountKeys)))
|
||||
r.MethodFunc("POST", "/acme/eab/{provisionerName}", authnz(requireEABEnabled(h.acmeResponder.CreateExternalAccountKey)))
|
||||
r.MethodFunc("DELETE", "/acme/eab/{provisionerName}/{id}", authnz(requireEABEnabled(h.acmeResponder.DeleteExternalAccountKey)))
|
||||
}
|
||||
|
|
|
@ -41,7 +41,7 @@ func (h *Handler) GetProvisioner(w http.ResponseWriter, r *http.Request) {
|
|||
}
|
||||
}
|
||||
|
||||
prov, err := h.db.GetProvisioner(ctx, p.GetID())
|
||||
prov, err := h.adminDB.GetProvisioner(ctx, p.GetID())
|
||||
if err != nil {
|
||||
api.WriteError(w, err)
|
||||
return
|
||||
|
@ -134,7 +134,7 @@ func (h *Handler) UpdateProvisioner(w http.ResponseWriter, r *http.Request) {
|
|||
return
|
||||
}
|
||||
|
||||
old, err := h.db.GetProvisioner(r.Context(), _old.GetID())
|
||||
old, err := h.adminDB.GetProvisioner(r.Context(), _old.GetID())
|
||||
if err != nil {
|
||||
api.WriteError(w, admin.WrapErrorISE(err, "error loading provisioner from db '%s'", _old.GetID()))
|
||||
return
|
||||
|
|
|
@ -26,7 +26,7 @@ func TestHandler_GetProvisioner(t *testing.T) {
|
|||
type test struct {
|
||||
ctx context.Context
|
||||
auth adminAuthority
|
||||
db admin.DB
|
||||
adminDB admin.DB
|
||||
req *http.Request
|
||||
statusCode int
|
||||
err *admin.Error
|
||||
|
@ -104,7 +104,7 @@ func TestHandler_GetProvisioner(t *testing.T) {
|
|||
ctx: ctx,
|
||||
req: req,
|
||||
auth: auth,
|
||||
db: db,
|
||||
adminDB: db,
|
||||
statusCode: 500,
|
||||
err: &admin.Error{
|
||||
Type: admin.ErrorServerInternalType.String(),
|
||||
|
@ -143,7 +143,7 @@ func TestHandler_GetProvisioner(t *testing.T) {
|
|||
ctx: ctx,
|
||||
req: req,
|
||||
auth: auth,
|
||||
db: db,
|
||||
adminDB: db,
|
||||
statusCode: 200,
|
||||
err: nil,
|
||||
prov: prov,
|
||||
|
@ -154,8 +154,8 @@ func TestHandler_GetProvisioner(t *testing.T) {
|
|||
tc := prep(t)
|
||||
t.Run(name, func(t *testing.T) {
|
||||
h := &Handler{
|
||||
auth: tc.auth,
|
||||
db: tc.db,
|
||||
auth: tc.auth,
|
||||
adminDB: tc.adminDB,
|
||||
}
|
||||
req := tc.req.WithContext(tc.ctx)
|
||||
w := httptest.NewRecorder()
|
||||
|
@ -605,7 +605,7 @@ func TestHandler_UpdateProvisioner(t *testing.T) {
|
|||
ctx context.Context
|
||||
auth adminAuthority
|
||||
body []byte
|
||||
db admin.DB
|
||||
adminDB admin.DB
|
||||
statusCode int
|
||||
err *admin.Error
|
||||
prov *linkedca.Provisioner
|
||||
|
@ -685,7 +685,7 @@ func TestHandler_UpdateProvisioner(t *testing.T) {
|
|||
ctx: ctx,
|
||||
body: body,
|
||||
auth: auth,
|
||||
db: db,
|
||||
adminDB: db,
|
||||
statusCode: 500,
|
||||
err: &admin.Error{
|
||||
Type: admin.ErrorServerInternalType.String(),
|
||||
|
@ -728,7 +728,7 @@ func TestHandler_UpdateProvisioner(t *testing.T) {
|
|||
ctx: ctx,
|
||||
body: body,
|
||||
auth: auth,
|
||||
db: db,
|
||||
adminDB: db,
|
||||
statusCode: 500,
|
||||
err: &admin.Error{
|
||||
Type: admin.ErrorServerInternalType.String(),
|
||||
|
@ -772,7 +772,7 @@ func TestHandler_UpdateProvisioner(t *testing.T) {
|
|||
ctx: ctx,
|
||||
body: body,
|
||||
auth: auth,
|
||||
db: db,
|
||||
adminDB: db,
|
||||
statusCode: 500,
|
||||
err: &admin.Error{
|
||||
Type: admin.ErrorServerInternalType.String(),
|
||||
|
@ -818,7 +818,7 @@ func TestHandler_UpdateProvisioner(t *testing.T) {
|
|||
ctx: ctx,
|
||||
body: body,
|
||||
auth: auth,
|
||||
db: db,
|
||||
adminDB: db,
|
||||
statusCode: 500,
|
||||
err: &admin.Error{
|
||||
Type: admin.ErrorServerInternalType.String(),
|
||||
|
@ -867,7 +867,7 @@ func TestHandler_UpdateProvisioner(t *testing.T) {
|
|||
ctx: ctx,
|
||||
body: body,
|
||||
auth: auth,
|
||||
db: db,
|
||||
adminDB: db,
|
||||
statusCode: 500,
|
||||
err: &admin.Error{
|
||||
Type: admin.ErrorServerInternalType.String(),
|
||||
|
@ -919,7 +919,7 @@ func TestHandler_UpdateProvisioner(t *testing.T) {
|
|||
ctx: ctx,
|
||||
body: body,
|
||||
auth: auth,
|
||||
db: db,
|
||||
adminDB: db,
|
||||
statusCode: 500,
|
||||
err: &admin.Error{
|
||||
Type: admin.ErrorServerInternalType.String(),
|
||||
|
@ -978,7 +978,7 @@ func TestHandler_UpdateProvisioner(t *testing.T) {
|
|||
ctx: ctx,
|
||||
body: body,
|
||||
auth: auth,
|
||||
db: db,
|
||||
adminDB: db,
|
||||
statusCode: 500,
|
||||
err: &admin.Error{
|
||||
Type: "", // TODO(hs): this error can be improved
|
||||
|
@ -1043,7 +1043,7 @@ func TestHandler_UpdateProvisioner(t *testing.T) {
|
|||
ctx: ctx,
|
||||
body: body,
|
||||
auth: auth,
|
||||
db: db,
|
||||
adminDB: db,
|
||||
statusCode: 200,
|
||||
prov: prov,
|
||||
}
|
||||
|
@ -1053,8 +1053,8 @@ func TestHandler_UpdateProvisioner(t *testing.T) {
|
|||
tc := prep(t)
|
||||
t.Run(name, func(t *testing.T) {
|
||||
h := &Handler{
|
||||
auth: tc.auth,
|
||||
db: tc.db,
|
||||
auth: tc.auth,
|
||||
adminDB: tc.adminDB,
|
||||
}
|
||||
req := httptest.NewRequest("POST", "/foo", io.NopCloser(bytes.NewBuffer(tc.body)))
|
||||
req = req.WithContext(tc.ctx)
|
||||
|
|
|
@ -12,7 +12,6 @@ import (
|
|||
"time"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/smallstep/certificates/authority/admin"
|
||||
adminAPI "github.com/smallstep/certificates/authority/admin/api"
|
||||
"github.com/smallstep/certificates/authority/provisioner"
|
||||
"github.com/smallstep/certificates/errs"
|
||||
|
@ -40,6 +39,19 @@ type AdminClient struct {
|
|||
x5cSubject string
|
||||
}
|
||||
|
||||
// AdminClientError is the client side representation of an
|
||||
// AdminError returned by the CA.
|
||||
type AdminClientError struct {
|
||||
Type string `json:"type"`
|
||||
Detail string `json:"detail"`
|
||||
Message string `json:"message"`
|
||||
}
|
||||
|
||||
// Error returns the AdminClientError message as the error message
|
||||
func (e *AdminClientError) Error() string {
|
||||
return e.Message
|
||||
}
|
||||
|
||||
// NewAdminClient creates a new AdminClient with the given endpoint and options.
|
||||
func NewAdminClient(endpoint string, opts ...ClientOption) (*AdminClient, error) {
|
||||
u, err := parseEndpoint(endpoint)
|
||||
|
@ -670,9 +682,9 @@ retry:
|
|||
func readAdminError(r io.ReadCloser) error {
|
||||
// TODO: not all errors can be read (i.e. 404); seems to be a bigger issue
|
||||
defer r.Close()
|
||||
adminErr := new(admin.Error)
|
||||
adminErr := new(AdminClientError)
|
||||
if err := json.NewDecoder(r).Decode(adminErr); err != nil {
|
||||
return err
|
||||
}
|
||||
return errors.New(adminErr.Message)
|
||||
return adminErr
|
||||
}
|
||||
|
|
3
ca/ca.go
3
ca/ca.go
|
@ -207,7 +207,8 @@ func (ca *CA) Init(cfg *config.Config) (*CA, error) {
|
|||
if cfg.AuthorityConfig.EnableAdmin {
|
||||
adminDB := auth.GetAdminDatabase()
|
||||
if adminDB != nil {
|
||||
adminHandler := adminAPI.NewHandler(auth, adminDB, acmeDB)
|
||||
acmeAdminResponder := adminAPI.NewACMEAdminResponder()
|
||||
adminHandler := adminAPI.NewHandler(auth, adminDB, acmeDB, acmeAdminResponder)
|
||||
mux.Route("/admin", func(r chi.Router) {
|
||||
adminHandler.Route(r)
|
||||
})
|
||||
|
|
Loading…
Reference in a new issue