2021-12-08 14:19:38 +00:00
|
|
|
package api
|
|
|
|
|
|
|
|
import (
|
|
|
|
"bytes"
|
|
|
|
"context"
|
|
|
|
"encoding/json"
|
|
|
|
"io"
|
|
|
|
"net/http"
|
|
|
|
"net/http/httptest"
|
2021-12-09 08:36:03 +00:00
|
|
|
"strings"
|
2021-12-08 14:19:38 +00:00
|
|
|
"testing"
|
|
|
|
|
|
|
|
"github.com/go-chi/chi"
|
2022-03-30 12:21:39 +00:00
|
|
|
|
|
|
|
"google.golang.org/protobuf/encoding/protojson"
|
|
|
|
"google.golang.org/protobuf/proto"
|
|
|
|
|
|
|
|
"go.step.sm/linkedca"
|
|
|
|
|
2021-12-08 14:19:38 +00:00
|
|
|
"github.com/smallstep/assert"
|
|
|
|
"github.com/smallstep/certificates/authority/admin"
|
|
|
|
)
|
|
|
|
|
2021-12-09 08:36:03 +00:00
|
|
|
func readProtoJSON(r io.ReadCloser, m proto.Message) error {
|
|
|
|
defer r.Close()
|
|
|
|
data, err := io.ReadAll(r)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
return protojson.Unmarshal(data, m)
|
|
|
|
}
|
|
|
|
|
2021-12-08 14:19:38 +00:00
|
|
|
func TestHandler_requireEABEnabled(t *testing.T) {
|
|
|
|
type test struct {
|
|
|
|
ctx context.Context
|
2022-03-30 12:21:39 +00:00
|
|
|
next http.HandlerFunc
|
2021-12-08 14:19:38 +00:00
|
|
|
err *admin.Error
|
|
|
|
statusCode int
|
|
|
|
}
|
|
|
|
var tests = map[string]func(t *testing.T) test{
|
2022-03-30 16:21:25 +00:00
|
|
|
"fail/prov.GetDetails": func(t *testing.T) test {
|
|
|
|
prov := &linkedca.Provisioner{
|
|
|
|
Id: "provID",
|
|
|
|
Name: "provName",
|
2021-12-08 14:19:38 +00:00
|
|
|
}
|
2022-03-30 16:21:25 +00:00
|
|
|
ctx := linkedca.NewContextWithProvisioner(context.Background(), prov)
|
|
|
|
err := admin.NewErrorISE("error getting details for provisioner 'provName'")
|
|
|
|
err.Message = "error getting details for provisioner 'provName'"
|
2021-12-08 14:19:38 +00:00
|
|
|
return test{
|
|
|
|
ctx: ctx,
|
|
|
|
err: err,
|
|
|
|
statusCode: 500,
|
|
|
|
}
|
|
|
|
},
|
2022-03-30 16:21:25 +00:00
|
|
|
"fail/details.GetACME": func(t *testing.T) test {
|
|
|
|
prov := &linkedca.Provisioner{
|
|
|
|
Id: "provID",
|
|
|
|
Name: "provName",
|
|
|
|
Details: &linkedca.ProvisionerDetails{},
|
|
|
|
}
|
|
|
|
ctx := linkedca.NewContextWithProvisioner(context.Background(), prov)
|
|
|
|
err := admin.NewErrorISE("error getting ACME details for provisioner 'provName'")
|
|
|
|
err.Message = "error getting ACME details for provisioner 'provName'"
|
|
|
|
return test{
|
|
|
|
ctx: ctx,
|
|
|
|
err: err,
|
|
|
|
statusCode: 500,
|
2021-12-08 14:19:38 +00:00
|
|
|
}
|
2022-03-30 16:21:25 +00:00
|
|
|
},
|
|
|
|
"ok/eab-disabled": func(t *testing.T) test {
|
|
|
|
prov := &linkedca.Provisioner{
|
|
|
|
Id: "provID",
|
|
|
|
Name: "provName",
|
|
|
|
Details: &linkedca.ProvisionerDetails{
|
|
|
|
Data: &linkedca.ProvisionerDetails_ACME{
|
|
|
|
ACME: &linkedca.ACMEProvisioner{
|
|
|
|
RequireEab: false,
|
2021-12-08 14:19:38 +00:00
|
|
|
},
|
2022-03-30 16:21:25 +00:00
|
|
|
},
|
2021-12-08 14:19:38 +00:00
|
|
|
},
|
|
|
|
}
|
2022-03-30 16:21:25 +00:00
|
|
|
ctx := linkedca.NewContextWithProvisioner(context.Background(), prov)
|
2021-12-08 14:19:38 +00:00
|
|
|
err := admin.NewError(admin.ErrorBadRequestType, "ACME EAB not enabled for provisioner provName")
|
2022-03-30 16:21:25 +00:00
|
|
|
err.Message = "ACME EAB not enabled for provisioner 'provName'"
|
2021-12-08 14:19:38 +00:00
|
|
|
return test{
|
|
|
|
ctx: ctx,
|
|
|
|
err: err,
|
|
|
|
statusCode: 400,
|
|
|
|
}
|
|
|
|
},
|
|
|
|
"ok/eab-enabled": func(t *testing.T) test {
|
2022-03-30 16:21:25 +00:00
|
|
|
prov := &linkedca.Provisioner{
|
|
|
|
Id: "provID",
|
|
|
|
Name: "provName",
|
|
|
|
Details: &linkedca.ProvisionerDetails{
|
|
|
|
Data: &linkedca.ProvisionerDetails_ACME{
|
|
|
|
ACME: &linkedca.ACMEProvisioner{
|
|
|
|
RequireEab: true,
|
2021-12-08 14:19:38 +00:00
|
|
|
},
|
2022-03-30 16:21:25 +00:00
|
|
|
},
|
2021-12-08 14:19:38 +00:00
|
|
|
},
|
|
|
|
}
|
2022-03-30 16:21:25 +00:00
|
|
|
ctx := linkedca.NewContextWithProvisioner(context.Background(), prov)
|
2021-12-08 14:19:38 +00:00
|
|
|
return test{
|
2022-03-30 16:21:25 +00:00
|
|
|
ctx: ctx,
|
2021-12-08 14:19:38 +00:00
|
|
|
next: func(w http.ResponseWriter, r *http.Request) {
|
|
|
|
w.Write(nil) // mock response with status 200
|
|
|
|
},
|
|
|
|
statusCode: 200,
|
|
|
|
}
|
|
|
|
},
|
|
|
|
}
|
|
|
|
|
|
|
|
for name, prep := range tests {
|
|
|
|
tc := prep(t)
|
|
|
|
t.Run(name, func(t *testing.T) {
|
2022-03-30 16:21:25 +00:00
|
|
|
h := &Handler{}
|
2021-12-08 14:19:38 +00:00
|
|
|
|
2022-03-30 16:21:25 +00:00
|
|
|
req := httptest.NewRequest("GET", "/foo", nil)
|
2021-12-08 14:19:38 +00:00
|
|
|
req = req.WithContext(tc.ctx)
|
|
|
|
w := httptest.NewRecorder()
|
|
|
|
h.requireEABEnabled(tc.next)(w, req)
|
|
|
|
res := w.Result()
|
|
|
|
|
2021-12-09 14:24:14 +00:00
|
|
|
assert.Equals(t, tc.statusCode, res.StatusCode)
|
2021-12-08 14:19:38 +00:00
|
|
|
|
|
|
|
body, err := io.ReadAll(res.Body)
|
|
|
|
res.Body.Close()
|
|
|
|
assert.FatalError(t, err)
|
|
|
|
|
|
|
|
if res.StatusCode >= 400 {
|
|
|
|
err := admin.Error{}
|
|
|
|
assert.FatalError(t, json.Unmarshal(bytes.TrimSpace(body), &err))
|
|
|
|
|
|
|
|
assert.Equals(t, tc.err.Type, err.Type)
|
|
|
|
assert.Equals(t, tc.err.Message, err.Message)
|
|
|
|
assert.Equals(t, tc.err.StatusCode(), res.StatusCode)
|
|
|
|
assert.Equals(t, tc.err.Detail, err.Detail)
|
|
|
|
assert.Equals(t, []string{"application/json"}, res.Header["Content-Type"])
|
|
|
|
return
|
|
|
|
}
|
|
|
|
})
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func TestCreateExternalAccountKeyRequest_Validate(t *testing.T) {
|
|
|
|
type fields struct {
|
|
|
|
Reference string
|
|
|
|
}
|
|
|
|
tests := []struct {
|
|
|
|
name string
|
|
|
|
fields fields
|
|
|
|
wantErr bool
|
|
|
|
}{
|
2021-12-09 14:24:14 +00:00
|
|
|
{
|
|
|
|
name: "fail/reference-too-long",
|
|
|
|
fields: fields{
|
|
|
|
Reference: strings.Repeat("A", 257),
|
|
|
|
},
|
|
|
|
wantErr: true,
|
|
|
|
},
|
2021-12-08 14:19:38 +00:00
|
|
|
{
|
|
|
|
name: "ok/empty-reference",
|
|
|
|
fields: fields{
|
|
|
|
Reference: "",
|
|
|
|
},
|
|
|
|
wantErr: false,
|
|
|
|
},
|
|
|
|
{
|
|
|
|
name: "ok",
|
|
|
|
fields: fields{
|
|
|
|
Reference: "my-eab-reference",
|
|
|
|
},
|
|
|
|
wantErr: false,
|
|
|
|
},
|
|
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
|
|
r := &CreateExternalAccountKeyRequest{
|
|
|
|
Reference: tt.fields.Reference,
|
|
|
|
}
|
|
|
|
if err := r.Validate(); (err != nil) != tt.wantErr {
|
|
|
|
t.Errorf("CreateExternalAccountKeyRequest.Validate() error = %v, wantErr %v", err, tt.wantErr)
|
|
|
|
}
|
|
|
|
})
|
|
|
|
}
|
|
|
|
}
|
2021-12-09 08:36:03 +00:00
|
|
|
|
|
|
|
func TestHandler_CreateExternalAccountKey(t *testing.T) {
|
|
|
|
type test struct {
|
|
|
|
ctx context.Context
|
|
|
|
statusCode int
|
|
|
|
err *admin.Error
|
|
|
|
}
|
|
|
|
var tests = map[string]func(t *testing.T) test{
|
2022-02-08 12:26:30 +00:00
|
|
|
"ok": func(t *testing.T) test {
|
2021-12-09 08:36:03 +00:00
|
|
|
chiCtx := chi.NewRouteContext()
|
|
|
|
ctx := context.WithValue(context.Background(), chi.RouteCtxKey, chiCtx)
|
|
|
|
return test{
|
|
|
|
ctx: ctx,
|
2022-02-08 12:26:30 +00:00
|
|
|
statusCode: 501,
|
2021-12-09 08:36:03 +00:00
|
|
|
err: &admin.Error{
|
2022-02-08 12:26:30 +00:00
|
|
|
Type: admin.ErrorNotImplementedType.String(),
|
|
|
|
Status: http.StatusNotImplemented,
|
2022-02-10 11:55:47 +00:00
|
|
|
Message: "this functionality is currently only available in Certificate Manager: https://u.step.sm/cm",
|
2022-02-08 12:26:30 +00:00
|
|
|
Detail: "not implemented",
|
2021-12-09 08:36:03 +00:00
|
|
|
},
|
|
|
|
}
|
|
|
|
},
|
|
|
|
}
|
|
|
|
for name, prep := range tests {
|
|
|
|
tc := prep(t)
|
|
|
|
t.Run(name, func(t *testing.T) {
|
2022-02-08 12:26:30 +00:00
|
|
|
|
|
|
|
req := httptest.NewRequest("POST", "/foo", nil) // chi routing is prepared in test setup
|
2021-12-09 08:36:03 +00:00
|
|
|
req = req.WithContext(tc.ctx)
|
|
|
|
w := httptest.NewRecorder()
|
2022-02-08 12:26:30 +00:00
|
|
|
acmeResponder := NewACMEAdminResponder()
|
|
|
|
acmeResponder.CreateExternalAccountKey(w, req)
|
2021-12-09 08:36:03 +00:00
|
|
|
res := w.Result()
|
2021-12-09 14:24:14 +00:00
|
|
|
assert.Equals(t, tc.statusCode, res.StatusCode)
|
2021-12-09 08:36:03 +00:00
|
|
|
|
2022-02-08 12:26:30 +00:00
|
|
|
body, err := io.ReadAll(res.Body)
|
|
|
|
res.Body.Close()
|
|
|
|
assert.FatalError(t, err)
|
2021-12-09 08:36:03 +00:00
|
|
|
|
2022-02-08 12:26:30 +00:00
|
|
|
adminErr := admin.Error{}
|
|
|
|
assert.FatalError(t, json.Unmarshal(bytes.TrimSpace(body), &adminErr))
|
2021-12-09 08:36:03 +00:00
|
|
|
|
2022-02-08 12:26:30 +00:00
|
|
|
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)
|
2021-12-09 16:29:23 +00:00
|
|
|
assert.Equals(t, []string{"application/json"}, res.Header["Content-Type"])
|
2021-12-09 08:36:03 +00:00
|
|
|
|
|
|
|
})
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func TestHandler_DeleteExternalAccountKey(t *testing.T) {
|
|
|
|
type test struct {
|
|
|
|
ctx context.Context
|
|
|
|
statusCode int
|
|
|
|
err *admin.Error
|
|
|
|
}
|
|
|
|
var tests = map[string]func(t *testing.T) test{
|
2022-02-08 12:26:30 +00:00
|
|
|
"ok": func(t *testing.T) test {
|
2021-12-09 08:36:03 +00:00
|
|
|
chiCtx := chi.NewRouteContext()
|
2022-01-24 13:03:56 +00:00
|
|
|
chiCtx.URLParams.Add("provisionerName", "provName")
|
2021-12-09 08:36:03 +00:00
|
|
|
chiCtx.URLParams.Add("id", "keyID")
|
|
|
|
ctx := context.WithValue(context.Background(), chi.RouteCtxKey, chiCtx)
|
|
|
|
return test{
|
|
|
|
ctx: ctx,
|
2022-02-08 12:26:30 +00:00
|
|
|
statusCode: 501,
|
2021-12-09 08:36:03 +00:00
|
|
|
err: &admin.Error{
|
2022-02-08 12:26:30 +00:00
|
|
|
Type: admin.ErrorNotImplementedType.String(),
|
|
|
|
Status: http.StatusNotImplemented,
|
2022-02-10 11:55:47 +00:00
|
|
|
Message: "this functionality is currently only available in Certificate Manager: https://u.step.sm/cm",
|
2022-02-08 12:26:30 +00:00
|
|
|
Detail: "not implemented",
|
2021-12-09 08:36:03 +00:00
|
|
|
},
|
|
|
|
}
|
|
|
|
},
|
|
|
|
}
|
|
|
|
for name, prep := range tests {
|
|
|
|
tc := prep(t)
|
|
|
|
t.Run(name, func(t *testing.T) {
|
2022-02-08 12:26:30 +00:00
|
|
|
|
2021-12-09 08:36:03 +00:00
|
|
|
req := httptest.NewRequest("DELETE", "/foo", nil) // chi routing is prepared in test setup
|
|
|
|
req = req.WithContext(tc.ctx)
|
|
|
|
w := httptest.NewRecorder()
|
2022-02-08 12:26:30 +00:00
|
|
|
acmeResponder := NewACMEAdminResponder()
|
|
|
|
acmeResponder.DeleteExternalAccountKey(w, req)
|
2021-12-09 08:36:03 +00:00
|
|
|
res := w.Result()
|
2021-12-09 14:24:14 +00:00
|
|
|
assert.Equals(t, tc.statusCode, res.StatusCode)
|
2021-12-09 08:36:03 +00:00
|
|
|
|
2021-12-09 16:29:23 +00:00
|
|
|
body, err := io.ReadAll(res.Body)
|
|
|
|
res.Body.Close()
|
|
|
|
assert.FatalError(t, err)
|
|
|
|
|
2022-02-08 12:26:30 +00:00
|
|
|
adminErr := admin.Error{}
|
|
|
|
assert.FatalError(t, json.Unmarshal(bytes.TrimSpace(body), &adminErr))
|
2021-12-09 16:29:23 +00:00
|
|
|
|
2022-02-08 12:26:30 +00:00
|
|
|
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"])
|
2021-12-09 08:36:03 +00:00
|
|
|
})
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func TestHandler_GetExternalAccountKeys(t *testing.T) {
|
|
|
|
type test struct {
|
|
|
|
ctx context.Context
|
|
|
|
statusCode int
|
|
|
|
req *http.Request
|
|
|
|
err *admin.Error
|
|
|
|
}
|
|
|
|
var tests = map[string]func(t *testing.T) test{
|
2022-02-08 12:26:30 +00:00
|
|
|
"ok": func(t *testing.T) test {
|
2021-12-09 08:36:03 +00:00
|
|
|
chiCtx := chi.NewRouteContext()
|
2022-01-24 13:03:56 +00:00
|
|
|
chiCtx.URLParams.Add("provisionerName", "provName")
|
2021-12-09 08:36:03 +00:00
|
|
|
req := httptest.NewRequest("GET", "/foo", nil)
|
|
|
|
ctx := context.WithValue(context.Background(), chi.RouteCtxKey, chiCtx)
|
|
|
|
return test{
|
|
|
|
ctx: ctx,
|
2022-02-08 12:26:30 +00:00
|
|
|
statusCode: 501,
|
2021-12-09 08:36:03 +00:00
|
|
|
req: req,
|
|
|
|
err: &admin.Error{
|
2022-02-08 12:26:30 +00:00
|
|
|
Type: admin.ErrorNotImplementedType.String(),
|
|
|
|
Status: http.StatusNotImplemented,
|
2022-02-10 11:55:47 +00:00
|
|
|
Message: "this functionality is currently only available in Certificate Manager: https://u.step.sm/cm",
|
2022-02-08 12:26:30 +00:00
|
|
|
Detail: "not implemented",
|
2021-12-09 08:36:03 +00:00
|
|
|
},
|
|
|
|
}
|
|
|
|
},
|
|
|
|
}
|
|
|
|
for name, prep := range tests {
|
|
|
|
tc := prep(t)
|
|
|
|
t.Run(name, func(t *testing.T) {
|
2022-02-08 12:26:30 +00:00
|
|
|
|
2021-12-09 08:36:03 +00:00
|
|
|
req := tc.req.WithContext(tc.ctx)
|
|
|
|
w := httptest.NewRecorder()
|
2022-02-08 12:26:30 +00:00
|
|
|
acmeResponder := NewACMEAdminResponder()
|
|
|
|
acmeResponder.GetExternalAccountKeys(w, req)
|
|
|
|
|
2021-12-09 08:36:03 +00:00
|
|
|
res := w.Result()
|
2021-12-09 14:24:14 +00:00
|
|
|
assert.Equals(t, tc.statusCode, res.StatusCode)
|
2021-12-09 08:36:03 +00:00
|
|
|
|
2021-12-09 22:15:38 +00:00
|
|
|
body, err := io.ReadAll(res.Body)
|
|
|
|
res.Body.Close()
|
|
|
|
assert.FatalError(t, err)
|
2021-12-09 08:36:03 +00:00
|
|
|
|
2022-02-08 12:26:30 +00:00
|
|
|
adminErr := admin.Error{}
|
|
|
|
assert.FatalError(t, json.Unmarshal(bytes.TrimSpace(body), &adminErr))
|
2021-12-09 08:36:03 +00:00
|
|
|
|
2022-02-08 12:26:30 +00:00
|
|
|
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)
|
2021-12-09 22:15:38 +00:00
|
|
|
assert.Equals(t, []string{"application/json"}, res.Header["Content-Type"])
|
2021-12-09 08:36:03 +00:00
|
|
|
})
|
|
|
|
}
|
|
|
|
}
|