smallstep-certificates/authority/admin/api/provisioner_test.go

1272 lines
36 KiB
Go

package api
import (
"bytes"
"context"
"encoding/json"
"errors"
"io"
"net/http"
"net/http/httptest"
"strings"
"testing"
"time"
"github.com/go-chi/chi"
"github.com/google/go-cmp/cmp"
"github.com/google/go-cmp/cmp/cmpopts"
"google.golang.org/protobuf/encoding/protojson"
"google.golang.org/protobuf/types/known/timestamppb"
"github.com/smallstep/assert"
"go.step.sm/linkedca"
"github.com/smallstep/certificates/authority/admin"
"github.com/smallstep/certificates/authority/provisioner"
)
func TestHandler_GetProvisioner(t *testing.T) {
type test struct {
ctx context.Context
auth adminAuthority
adminDB admin.DB
req *http.Request
statusCode int
err *admin.Error
prov *linkedca.Provisioner
}
var tests = map[string]func(t *testing.T) test{
"fail/auth.LoadProvisionerByID": func(t *testing.T) test {
req := httptest.NewRequest("GET", "/foo?id=provID", nil)
chiCtx := chi.NewRouteContext()
ctx := context.WithValue(context.Background(), chi.RouteCtxKey, chiCtx)
auth := &mockAdminAuthority{
MockLoadProvisionerByID: func(id string) (provisioner.Interface, error) {
assert.Equals(t, "provID", id)
return nil, errors.New("force")
},
}
return test{
ctx: ctx,
req: req,
auth: auth,
adminDB: &admin.MockDB{},
statusCode: 500,
err: &admin.Error{
Type: admin.ErrorServerInternalType.String(),
Status: 500,
Detail: "the server experienced an internal error",
Message: "error loading provisioner provID: force",
},
}
},
"fail/auth.LoadProvisionerByName": func(t *testing.T) test {
req := httptest.NewRequest("GET", "/foo", nil)
chiCtx := chi.NewRouteContext()
chiCtx.URLParams.Add("name", "provName")
ctx := context.WithValue(context.Background(), chi.RouteCtxKey, chiCtx)
auth := &mockAdminAuthority{
MockLoadProvisionerByName: func(name string) (provisioner.Interface, error) {
assert.Equals(t, "provName", name)
return nil, errors.New("force")
},
}
return test{
ctx: ctx,
req: req,
auth: auth,
adminDB: &admin.MockDB{},
statusCode: 500,
err: &admin.Error{
Type: admin.ErrorServerInternalType.String(),
Status: 500,
Detail: "the server experienced an internal error",
Message: "error loading provisioner provName: force",
},
}
},
"fail/db.GetProvisioner": func(t *testing.T) test {
req := httptest.NewRequest("GET", "/foo", nil)
chiCtx := chi.NewRouteContext()
chiCtx.URLParams.Add("name", "provName")
ctx := context.WithValue(context.Background(), chi.RouteCtxKey, chiCtx)
auth := &mockAdminAuthority{
MockLoadProvisionerByName: func(name string) (provisioner.Interface, error) {
assert.Equals(t, "provName", name)
return &provisioner.ACME{
ID: "acmeID",
Name: "provName",
}, nil
},
}
db := &admin.MockDB{
MockGetProvisioner: func(ctx context.Context, id string) (*linkedca.Provisioner, error) {
assert.Equals(t, "acmeID", id)
return nil, admin.NewErrorISE("error loading provisioner provName: force")
},
}
return test{
ctx: ctx,
req: req,
auth: auth,
adminDB: db,
statusCode: 500,
err: &admin.Error{
Type: admin.ErrorServerInternalType.String(),
Status: 500,
Detail: "the server experienced an internal error",
Message: "error loading provisioner provName: force",
},
}
},
"ok": func(t *testing.T) test {
req := httptest.NewRequest("GET", "/foo", nil)
chiCtx := chi.NewRouteContext()
chiCtx.URLParams.Add("name", "provName")
ctx := context.WithValue(context.Background(), chi.RouteCtxKey, chiCtx)
auth := &mockAdminAuthority{
MockLoadProvisionerByName: func(name string) (provisioner.Interface, error) {
assert.Equals(t, "provName", name)
return &provisioner.ACME{
ID: "acmeID",
Name: "provName",
}, nil
},
}
prov := &linkedca.Provisioner{
Id: "acmeID",
Type: linkedca.Provisioner_ACME,
Name: "provName", // TODO(hs): other fields too?
}
db := &admin.MockDB{
MockGetProvisioner: func(ctx context.Context, id string) (*linkedca.Provisioner, error) {
assert.Equals(t, "acmeID", id)
return prov, nil
},
}
return test{
ctx: ctx,
req: req,
auth: auth,
adminDB: db,
statusCode: 200,
err: nil,
prov: prov,
}
},
}
for name, prep := range tests {
tc := prep(t)
t.Run(name, func(t *testing.T) {
mockMustAuthority(t, tc.auth)
ctx := admin.NewContext(tc.ctx, tc.adminDB)
req := tc.req.WithContext(ctx)
w := httptest.NewRecorder()
GetProvisioner(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.Detail, adminErr.Detail)
assert.Equals(t, []string{"application/json"}, res.Header["Content-Type"])
return
}
prov := &linkedca.Provisioner{}
err := readProtoJSON(res.Body, prov)
assert.FatalError(t, err)
assert.Equals(t, []string{"application/json"}, res.Header["Content-Type"])
opts := []cmp.Option{cmpopts.IgnoreUnexported(linkedca.Provisioner{}, timestamppb.Timestamp{})}
if !cmp.Equal(tc.prov, prov, opts...) {
t.Errorf("h.GetProvisioner diff =\n%s", cmp.Diff(tc.prov, prov, opts...))
}
})
}
}
func TestHandler_GetProvisioners(t *testing.T) {
type test struct {
ctx context.Context
auth adminAuthority
req *http.Request
statusCode int
err *admin.Error
resp GetProvisionersResponse
}
var tests = map[string]func(t *testing.T) test{
"fail/parse-cursor": func(t *testing.T) test {
req := httptest.NewRequest("GET", "/foo?limit=X", nil)
return test{
ctx: context.Background(),
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 'X' is not an integer: strconv.Atoi: parsing \"X\": invalid syntax",
},
}
},
"fail/auth.GetProvisioners": func(t *testing.T) test {
req := httptest.NewRequest("GET", "/foo", nil)
auth := &mockAdminAuthority{
MockGetProvisioners: func(cursor string, limit int) (provisioner.List, string, error) {
assert.Equals(t, "", cursor)
assert.Equals(t, 0, limit)
return nil, "", errors.New("force")
},
}
return test{
ctx: context.Background(),
req: req,
auth: auth,
statusCode: 500,
err: &admin.Error{
Type: "",
Status: 500,
Detail: "",
Message: "The certificate authority encountered an Internal Server Error. Please see the certificate authority logs for more info.",
},
}
},
"ok": func(t *testing.T) test {
req := httptest.NewRequest("GET", "/foo", nil)
provisioners := provisioner.List{
&provisioner.OIDC{
Type: "OIDC",
Name: "oidcProv",
},
&provisioner.ACME{
Type: "ACME",
Name: "provName",
ForceCN: false,
RequireEAB: false,
},
}
auth := &mockAdminAuthority{
MockGetProvisioners: func(cursor string, limit int) (provisioner.List, string, error) {
assert.Equals(t, "", cursor)
assert.Equals(t, 0, limit)
return provisioners, "nextCursorValue", nil
},
}
return test{
ctx: context.Background(),
req: req,
auth: auth,
statusCode: 200,
err: nil,
resp: GetProvisionersResponse{
Provisioners: provisioners,
NextCursor: "nextCursorValue",
},
}
},
}
for name, prep := range tests {
tc := prep(t)
t.Run(name, func(t *testing.T) {
mockMustAuthority(t, tc.auth)
req := tc.req.WithContext(tc.ctx)
w := httptest.NewRecorder()
GetProvisioners(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.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 := GetProvisionersResponse{}
assert.FatalError(t, json.Unmarshal(bytes.TrimSpace(body), &response))
assert.Equals(t, []string{"application/json"}, res.Header["Content-Type"])
opts := []cmp.Option{cmpopts.IgnoreUnexported(provisioner.ACME{}, provisioner.OIDC{})}
if !cmp.Equal(tc.resp, response, opts...) {
t.Errorf("h.GetProvisioners diff =\n%s", cmp.Diff(tc.resp, response, opts...))
}
})
}
}
func TestHandler_CreateProvisioner(t *testing.T) {
type test struct {
ctx context.Context
auth adminAuthority
body []byte
statusCode int
err *admin.Error
prov *linkedca.Provisioner
}
var tests = map[string]func(t *testing.T) test{
"fail/readProtoJSON": func(t *testing.T) test {
body := []byte("{!?}")
return test{
ctx: context.Background(),
body: body,
statusCode: 400,
err: &admin.Error{
Type: "badRequest",
Status: 400,
Detail: "bad request",
Message: "proto: syntax error (line 1:2): invalid value !",
},
}
},
// TODO(hs): ValidateClaims can't be mocked atm
// "fail/authority.ValidateClaims": func(t *testing.T) test {
// return test{}
// },
"fail/validateTemplates": func(t *testing.T) test {
prov := &linkedca.Provisioner{
Id: "provID",
Type: linkedca.Provisioner_OIDC,
Name: "provName",
X509Template: &linkedca.Template{
Template: []byte(`{ {{missingFunction "foo"}} }`),
},
}
body, err := protojson.Marshal(prov)
assert.FatalError(t, err)
return test{
ctx: context.Background(),
body: body,
statusCode: 400,
err: &admin.Error{
Type: "badRequest",
Status: 400,
Detail: "bad request",
Message: "invalid template: invalid X.509 template: error parsing template: template: template:1: function \"missingFunction\" not defined",
},
}
},
"fail/auth.StoreProvisioner": func(t *testing.T) test {
prov := &linkedca.Provisioner{
Id: "provID",
Type: linkedca.Provisioner_OIDC,
Name: "provName",
}
body, err := protojson.Marshal(prov)
assert.FatalError(t, err)
auth := &mockAdminAuthority{
MockStoreProvisioner: func(ctx context.Context, prov *linkedca.Provisioner) error {
assert.Equals(t, "provID", prov.Id)
return errors.New("force")
},
}
return test{
ctx: context.Background(),
body: body,
auth: auth,
statusCode: 500,
err: &admin.Error{
Type: admin.ErrorServerInternalType.String(),
Status: 500,
Detail: "the server experienced an internal error",
Message: "error storing provisioner provName: force",
},
}
},
"ok": func(t *testing.T) test {
prov := &linkedca.Provisioner{
Id: "provID",
Type: linkedca.Provisioner_OIDC,
Name: "provName",
}
body, err := protojson.Marshal(prov)
assert.FatalError(t, err)
auth := &mockAdminAuthority{
MockStoreProvisioner: func(ctx context.Context, prov *linkedca.Provisioner) error {
assert.Equals(t, "provID", prov.Id)
return nil
},
}
return test{
ctx: context.Background(),
body: body,
auth: auth,
statusCode: 201,
err: nil,
prov: prov,
}
},
}
for name, prep := range tests {
tc := prep(t)
t.Run(name, func(t *testing.T) {
mockMustAuthority(t, tc.auth)
req := httptest.NewRequest("POST", "/foo", io.NopCloser(bytes.NewBuffer(tc.body)))
req = req.WithContext(tc.ctx)
w := httptest.NewRecorder()
CreateProvisioner(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.Detail, adminErr.Detail)
assert.Equals(t, []string{"application/json"}, res.Header["Content-Type"])
if strings.HasPrefix(tc.err.Message, "proto:") {
assert.True(t, strings.Contains(adminErr.Message, "syntax error"))
} else {
assert.Equals(t, tc.err.Message, adminErr.Message)
}
return
}
prov := &linkedca.Provisioner{}
err := readProtoJSON(res.Body, prov)
assert.FatalError(t, err)
assert.Equals(t, []string{"application/json"}, res.Header["Content-Type"])
opts := []cmp.Option{cmpopts.IgnoreUnexported(linkedca.Provisioner{}, timestamppb.Timestamp{})}
if !cmp.Equal(tc.prov, prov, opts...) {
t.Errorf("linkedca.Admin diff =\n%s", cmp.Diff(tc.prov, prov, opts...))
}
})
}
}
func TestHandler_DeleteProvisioner(t *testing.T) {
type test struct {
ctx context.Context
auth adminAuthority
req *http.Request
statusCode int
err *admin.Error
}
var tests = map[string]func(t *testing.T) test{
"fail/auth.LoadProvisionerByID": func(t *testing.T) test {
req := httptest.NewRequest("DELETE", "/foo?id=provID", nil)
chiCtx := chi.NewRouteContext()
ctx := context.WithValue(context.Background(), chi.RouteCtxKey, chiCtx)
auth := &mockAdminAuthority{
MockLoadProvisionerByID: func(id string) (provisioner.Interface, error) {
assert.Equals(t, "provID", id)
return nil, errors.New("force")
},
}
return test{
ctx: ctx,
req: req,
auth: auth,
statusCode: 500,
err: &admin.Error{
Type: admin.ErrorServerInternalType.String(),
Status: 500,
Detail: "the server experienced an internal error",
Message: "error loading provisioner provID: force",
},
}
},
"fail/auth.LoadProvisionerByName": func(t *testing.T) test {
req := httptest.NewRequest("DELETE", "/foo", nil)
chiCtx := chi.NewRouteContext()
chiCtx.URLParams.Add("name", "provName")
ctx := context.WithValue(context.Background(), chi.RouteCtxKey, chiCtx)
auth := &mockAdminAuthority{
MockLoadProvisionerByName: func(name string) (provisioner.Interface, error) {
assert.Equals(t, "provName", name)
return nil, errors.New("force")
},
}
return test{
ctx: ctx,
req: req,
auth: auth,
statusCode: 500,
err: &admin.Error{
Type: admin.ErrorServerInternalType.String(),
Status: 500,
Detail: "the server experienced an internal error",
Message: "error loading provisioner provName: force",
},
}
},
"fail/auth.RemoveProvisioner": func(t *testing.T) test {
req := httptest.NewRequest("DELETE", "/foo", nil)
chiCtx := chi.NewRouteContext()
chiCtx.URLParams.Add("name", "provName")
ctx := context.WithValue(context.Background(), chi.RouteCtxKey, chiCtx)
auth := &mockAdminAuthority{
MockLoadProvisionerByName: func(name string) (provisioner.Interface, error) {
assert.Equals(t, "provName", name)
return &provisioner.OIDC{
ID: "provID",
Name: "provName",
Type: "OIDC",
}, nil
},
MockRemoveProvisioner: func(ctx context.Context, id string) error {
assert.Equals(t, "provID", id)
return errors.New("force")
},
}
return test{
ctx: ctx,
req: req,
auth: auth,
statusCode: 500,
err: &admin.Error{
Type: admin.ErrorServerInternalType.String(),
Status: 500,
Detail: "the server experienced an internal error",
Message: "error removing provisioner provName: force",
},
}
},
"ok": func(t *testing.T) test {
req := httptest.NewRequest("DELETE", "/foo", nil)
chiCtx := chi.NewRouteContext()
chiCtx.URLParams.Add("name", "provName")
ctx := context.WithValue(context.Background(), chi.RouteCtxKey, chiCtx)
auth := &mockAdminAuthority{
MockLoadProvisionerByName: func(name string) (provisioner.Interface, error) {
assert.Equals(t, "provName", name)
return &provisioner.OIDC{
ID: "provID",
Name: "provName",
Type: "OIDC",
}, nil
},
MockRemoveProvisioner: func(ctx context.Context, id string) error {
assert.Equals(t, "provID", id)
return nil
},
}
return test{
ctx: ctx,
req: req,
auth: auth,
statusCode: 200,
err: nil,
}
},
}
for name, prep := range tests {
tc := prep(t)
t.Run(name, func(t *testing.T) {
mockMustAuthority(t, tc.auth)
req := tc.req.WithContext(tc.ctx)
w := httptest.NewRecorder()
DeleteProvisioner(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.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"])
})
}
}
func TestHandler_UpdateProvisioner(t *testing.T) {
type test struct {
ctx context.Context
auth adminAuthority
body []byte
adminDB admin.DB
statusCode int
err *admin.Error
prov *linkedca.Provisioner
}
var tests = map[string]func(t *testing.T) test{
"fail/readProtoJSON": func(t *testing.T) test {
body := []byte("{!?}")
return test{
ctx: context.Background(),
body: body,
adminDB: &admin.MockDB{},
statusCode: 400,
err: &admin.Error{
Type: "badRequest",
Status: 400,
Detail: "bad request",
Message: "proto: syntax error (line 1:2): invalid value !",
},
}
},
"fail/auth.LoadProvisionerByName": func(t *testing.T) test {
chiCtx := chi.NewRouteContext()
chiCtx.URLParams.Add("name", "provName")
ctx := context.WithValue(context.Background(), chi.RouteCtxKey, chiCtx)
prov := &linkedca.Provisioner{
Id: "provID",
Type: linkedca.Provisioner_OIDC,
Name: "provName",
}
body, err := protojson.Marshal(prov)
assert.FatalError(t, err)
auth := &mockAdminAuthority{
MockLoadProvisionerByName: func(name string) (provisioner.Interface, error) {
assert.Equals(t, "provName", name)
return nil, errors.New("force")
},
}
return test{
ctx: ctx,
body: body,
adminDB: &admin.MockDB{},
auth: auth,
statusCode: 500,
err: &admin.Error{
Type: admin.ErrorServerInternalType.String(),
Status: 500,
Detail: "the server experienced an internal error",
Message: "error loading provisioner from cached configuration 'provName': force",
},
}
},
"fail/db.GetProvisioner": func(t *testing.T) test {
chiCtx := chi.NewRouteContext()
chiCtx.URLParams.Add("name", "provName")
ctx := context.WithValue(context.Background(), chi.RouteCtxKey, chiCtx)
prov := &linkedca.Provisioner{
Id: "provID",
Type: linkedca.Provisioner_OIDC,
Name: "provName",
}
body, err := protojson.Marshal(prov)
assert.FatalError(t, err)
auth := &mockAdminAuthority{
MockLoadProvisionerByName: func(name string) (provisioner.Interface, error) {
assert.Equals(t, "provName", name)
return &provisioner.OIDC{
ID: "provID",
Name: "provName",
}, nil
},
}
db := &admin.MockDB{
MockGetProvisioner: func(ctx context.Context, id string) (*linkedca.Provisioner, error) {
assert.Equals(t, "provID", id)
return nil, errors.New("force")
},
}
return test{
ctx: ctx,
body: body,
auth: auth,
adminDB: db,
statusCode: 500,
err: &admin.Error{
Type: admin.ErrorServerInternalType.String(),
Status: 500,
Detail: "the server experienced an internal error",
Message: "error loading provisioner from db 'provID': force",
},
}
},
"fail/change-id-error": func(t *testing.T) test {
chiCtx := chi.NewRouteContext()
chiCtx.URLParams.Add("name", "provName")
ctx := context.WithValue(context.Background(), chi.RouteCtxKey, chiCtx)
prov := &linkedca.Provisioner{
Id: "differentProvID",
Type: linkedca.Provisioner_OIDC,
Name: "provName",
}
body, err := protojson.Marshal(prov)
assert.FatalError(t, err)
auth := &mockAdminAuthority{
MockLoadProvisionerByName: func(name string) (provisioner.Interface, error) {
assert.Equals(t, "provName", name)
return &provisioner.OIDC{
ID: "provID",
Name: "provName",
}, nil
},
}
db := &admin.MockDB{
MockGetProvisioner: func(ctx context.Context, id string) (*linkedca.Provisioner, error) {
assert.Equals(t, "provID", id)
return &linkedca.Provisioner{
Id: "provID",
Name: "provName",
}, nil
},
}
return test{
ctx: ctx,
body: body,
auth: auth,
adminDB: db,
statusCode: 500,
err: &admin.Error{
Type: admin.ErrorServerInternalType.String(),
Status: 500,
Detail: "the server experienced an internal error",
Message: "cannot change provisioner ID",
},
}
},
"fail/change-type-error": func(t *testing.T) test {
chiCtx := chi.NewRouteContext()
chiCtx.URLParams.Add("name", "provName")
ctx := context.WithValue(context.Background(), chi.RouteCtxKey, chiCtx)
prov := &linkedca.Provisioner{
Id: "provID",
Type: linkedca.Provisioner_JWK,
Name: "provName",
}
body, err := protojson.Marshal(prov)
assert.FatalError(t, err)
auth := &mockAdminAuthority{
MockLoadProvisionerByName: func(name string) (provisioner.Interface, error) {
assert.Equals(t, "provName", name)
return &provisioner.OIDC{
ID: "provID",
Name: "provName",
}, nil
},
}
db := &admin.MockDB{
MockGetProvisioner: func(ctx context.Context, id string) (*linkedca.Provisioner, error) {
assert.Equals(t, "provID", id)
return &linkedca.Provisioner{
Id: "provID",
Name: "provName",
Type: linkedca.Provisioner_OIDC,
}, nil
},
}
return test{
ctx: ctx,
body: body,
auth: auth,
adminDB: db,
statusCode: 500,
err: &admin.Error{
Type: admin.ErrorServerInternalType.String(),
Status: 500,
Detail: "the server experienced an internal error",
Message: "cannot change provisioner type",
},
}
},
"fail/change-authority-id-error": func(t *testing.T) test {
chiCtx := chi.NewRouteContext()
chiCtx.URLParams.Add("name", "provName")
ctx := context.WithValue(context.Background(), chi.RouteCtxKey, chiCtx)
prov := &linkedca.Provisioner{
Id: "provID",
Type: linkedca.Provisioner_OIDC,
Name: "provName",
AuthorityId: "differentAuthorityID",
}
body, err := protojson.Marshal(prov)
assert.FatalError(t, err)
auth := &mockAdminAuthority{
MockLoadProvisionerByName: func(name string) (provisioner.Interface, error) {
assert.Equals(t, "provName", name)
return &provisioner.OIDC{
ID: "provID",
Name: "provName",
}, nil
},
}
db := &admin.MockDB{
MockGetProvisioner: func(ctx context.Context, id string) (*linkedca.Provisioner, error) {
assert.Equals(t, "provID", id)
return &linkedca.Provisioner{
Id: "provID",
Name: "provName",
Type: linkedca.Provisioner_OIDC,
AuthorityId: "authorityID",
}, nil
},
}
return test{
ctx: ctx,
body: body,
auth: auth,
adminDB: db,
statusCode: 500,
err: &admin.Error{
Type: admin.ErrorServerInternalType.String(),
Status: 500,
Detail: "the server experienced an internal error",
Message: "cannot change provisioner authorityID",
},
}
},
"fail/change-createdAt-error": func(t *testing.T) test {
chiCtx := chi.NewRouteContext()
chiCtx.URLParams.Add("name", "provName")
ctx := context.WithValue(context.Background(), chi.RouteCtxKey, chiCtx)
createdAt := time.Now()
prov := &linkedca.Provisioner{
Id: "provID",
Type: linkedca.Provisioner_OIDC,
Name: "provName",
AuthorityId: "authorityID",
CreatedAt: timestamppb.New(time.Now().Add(-1 * time.Hour)),
}
body, err := protojson.Marshal(prov)
assert.FatalError(t, err)
auth := &mockAdminAuthority{
MockLoadProvisionerByName: func(name string) (provisioner.Interface, error) {
assert.Equals(t, "provName", name)
return &provisioner.OIDC{
ID: "provID",
Name: "provName",
}, nil
},
}
db := &admin.MockDB{
MockGetProvisioner: func(ctx context.Context, id string) (*linkedca.Provisioner, error) {
assert.Equals(t, "provID", id)
return &linkedca.Provisioner{
Id: "provID",
Name: "provName",
Type: linkedca.Provisioner_OIDC,
AuthorityId: "authorityID",
CreatedAt: timestamppb.New(createdAt),
}, nil
},
}
return test{
ctx: ctx,
body: body,
auth: auth,
adminDB: db,
statusCode: 500,
err: &admin.Error{
Type: admin.ErrorServerInternalType.String(),
Status: 500,
Detail: "the server experienced an internal error",
Message: "cannot change provisioner createdAt",
},
}
},
"fail/change-deletedAt-error": func(t *testing.T) test {
chiCtx := chi.NewRouteContext()
chiCtx.URLParams.Add("name", "provName")
ctx := context.WithValue(context.Background(), chi.RouteCtxKey, chiCtx)
createdAt := time.Now()
var deletedAt time.Time
prov := &linkedca.Provisioner{
Id: "provID",
Type: linkedca.Provisioner_OIDC,
Name: "provName",
AuthorityId: "authorityID",
CreatedAt: timestamppb.New(createdAt),
DeletedAt: timestamppb.New(time.Now()),
}
body, err := protojson.Marshal(prov)
assert.FatalError(t, err)
auth := &mockAdminAuthority{
MockLoadProvisionerByName: func(name string) (provisioner.Interface, error) {
assert.Equals(t, "provName", name)
return &provisioner.OIDC{
ID: "provID",
Name: "provName",
}, nil
},
}
db := &admin.MockDB{
MockGetProvisioner: func(ctx context.Context, id string) (*linkedca.Provisioner, error) {
assert.Equals(t, "provID", id)
return &linkedca.Provisioner{
Id: "provID",
Name: "provName",
Type: linkedca.Provisioner_OIDC,
AuthorityId: "authorityID",
CreatedAt: timestamppb.New(createdAt),
DeletedAt: timestamppb.New(deletedAt),
}, nil
},
}
return test{
ctx: ctx,
body: body,
auth: auth,
adminDB: db,
statusCode: 500,
err: &admin.Error{
Type: admin.ErrorServerInternalType.String(),
Status: 500,
Detail: "the server experienced an internal error",
Message: "cannot change provisioner deletedAt",
},
}
},
// TODO(hs): ValidateClaims can't be mocked atm
//"fail/ValidateClaims": func(t *testing.T) test { return test{} },
"fail/validateTemplates": func(t *testing.T) test {
chiCtx := chi.NewRouteContext()
chiCtx.URLParams.Add("name", "provName")
ctx := context.WithValue(context.Background(), chi.RouteCtxKey, chiCtx)
createdAt := time.Now()
var deletedAt time.Time
prov := &linkedca.Provisioner{
Id: "provID",
Type: linkedca.Provisioner_OIDC,
Name: "provName",
AuthorityId: "authorityID",
CreatedAt: timestamppb.New(createdAt),
DeletedAt: timestamppb.New(deletedAt),
X509Template: &linkedca.Template{
Template: []byte("{ {{ missingFunction }} }"),
},
}
body, err := protojson.Marshal(prov)
assert.FatalError(t, err)
auth := &mockAdminAuthority{
MockLoadProvisionerByName: func(name string) (provisioner.Interface, error) {
assert.Equals(t, "provName", name)
return &provisioner.OIDC{
ID: "provID",
Name: "provName",
}, nil
},
}
db := &admin.MockDB{
MockGetProvisioner: func(ctx context.Context, id string) (*linkedca.Provisioner, error) {
assert.Equals(t, "provID", id)
return &linkedca.Provisioner{
Id: "provID",
Name: "provName",
Type: linkedca.Provisioner_OIDC,
AuthorityId: "authorityID",
CreatedAt: timestamppb.New(createdAt),
DeletedAt: timestamppb.New(deletedAt),
}, nil
},
}
return test{
ctx: ctx,
body: body,
auth: auth,
adminDB: db,
statusCode: 400,
err: &admin.Error{
Type: "badRequest",
Status: 400,
Detail: "bad request",
Message: "invalid template: invalid X.509 template: error parsing template: template: template:1: function \"missingFunction\" not defined",
},
}
},
"fail/auth.UpdateProvisioner": func(t *testing.T) test {
chiCtx := chi.NewRouteContext()
chiCtx.URLParams.Add("name", "provName")
ctx := context.WithValue(context.Background(), chi.RouteCtxKey, chiCtx)
createdAt := time.Now()
var deletedAt time.Time
prov := &linkedca.Provisioner{
Id: "provID",
Type: linkedca.Provisioner_OIDC,
Name: "provName",
AuthorityId: "authorityID",
CreatedAt: timestamppb.New(createdAt),
DeletedAt: timestamppb.New(deletedAt),
}
body, err := protojson.Marshal(prov)
assert.FatalError(t, err)
auth := &mockAdminAuthority{
MockLoadProvisionerByName: func(name string) (provisioner.Interface, error) {
assert.Equals(t, "provName", name)
return &provisioner.OIDC{
ID: "provID",
Name: "provName",
}, nil
},
MockUpdateProvisioner: func(ctx context.Context, nu *linkedca.Provisioner) error {
assert.Equals(t, "provID", nu.Id)
assert.Equals(t, "provName", nu.Name)
return errors.New("force")
},
}
db := &admin.MockDB{
MockGetProvisioner: func(ctx context.Context, id string) (*linkedca.Provisioner, error) {
assert.Equals(t, "provID", id)
return &linkedca.Provisioner{
Id: "provID",
Name: "provName",
Type: linkedca.Provisioner_OIDC,
AuthorityId: "authorityID",
CreatedAt: timestamppb.New(createdAt),
DeletedAt: timestamppb.New(deletedAt),
}, nil
},
}
return test{
ctx: ctx,
body: body,
auth: auth,
adminDB: db,
statusCode: 500,
err: &admin.Error{
Type: "", // TODO(hs): this error can be improved
Status: 500,
Detail: "",
Message: "",
},
}
},
"ok": func(t *testing.T) test {
chiCtx := chi.NewRouteContext()
chiCtx.URLParams.Add("name", "provName")
ctx := context.WithValue(context.Background(), chi.RouteCtxKey, chiCtx)
createdAt := time.Now()
var deletedAt time.Time
prov := &linkedca.Provisioner{
Id: "provID",
Type: linkedca.Provisioner_OIDC,
Name: "provName",
AuthorityId: "authorityID",
CreatedAt: timestamppb.New(createdAt),
DeletedAt: timestamppb.New(deletedAt),
Details: &linkedca.ProvisionerDetails{
Data: &linkedca.ProvisionerDetails_OIDC{
OIDC: &linkedca.OIDCProvisioner{
ClientId: "new-client-id",
ClientSecret: "new-client-secret",
},
},
},
}
body, err := protojson.Marshal(prov)
assert.FatalError(t, err)
auth := &mockAdminAuthority{
MockLoadProvisionerByName: func(name string) (provisioner.Interface, error) {
assert.Equals(t, "provName", name)
return &provisioner.OIDC{
ID: "provID",
Name: "provName",
}, nil
},
MockUpdateProvisioner: func(ctx context.Context, nu *linkedca.Provisioner) error {
assert.Equals(t, "provID", nu.Id)
assert.Equals(t, "provName", nu.Name)
return nil
},
}
db := &admin.MockDB{
MockGetProvisioner: func(ctx context.Context, id string) (*linkedca.Provisioner, error) {
assert.Equals(t, "provID", id)
return &linkedca.Provisioner{
Id: "provID",
Name: "provName",
Type: linkedca.Provisioner_OIDC,
AuthorityId: "authorityID",
CreatedAt: timestamppb.New(createdAt),
DeletedAt: timestamppb.New(deletedAt),
}, nil
},
}
return test{
ctx: ctx,
body: body,
auth: auth,
adminDB: db,
statusCode: 200,
prov: prov,
}
},
}
for name, prep := range tests {
tc := prep(t)
t.Run(name, func(t *testing.T) {
mockMustAuthority(t, tc.auth)
ctx := admin.NewContext(tc.ctx, tc.adminDB)
req := httptest.NewRequest("POST", "/foo", io.NopCloser(bytes.NewBuffer(tc.body)))
req = req.WithContext(ctx)
w := httptest.NewRecorder()
UpdateProvisioner(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.Detail, adminErr.Detail)
assert.Equals(t, []string{"application/json"}, res.Header["Content-Type"])
if strings.HasPrefix(tc.err.Message, "proto:") {
assert.True(t, strings.Contains(adminErr.Message, "syntax error"))
} else {
assert.Equals(t, tc.err.Message, adminErr.Message)
}
return
}
prov := &linkedca.Provisioner{}
err := readProtoJSON(res.Body, prov)
assert.FatalError(t, err)
assert.Equals(t, []string{"application/json"}, res.Header["Content-Type"])
opts := []cmp.Option{
cmpopts.IgnoreUnexported(
linkedca.Provisioner{}, linkedca.ProvisionerDetails{}, linkedca.ProvisionerDetails_OIDC{},
linkedca.OIDCProvisioner{}, timestamppb.Timestamp{},
),
}
if !cmp.Equal(tc.prov, prov, opts...) {
t.Errorf("linkedca.Admin diff =\n%s", cmp.Diff(tc.prov, prov, opts...))
}
})
}
}
func Test_validateTemplates(t *testing.T) {
type args struct {
x509 *linkedca.Template
ssh *linkedca.Template
}
tests := []struct {
name string
args args
err error
}{
{
name: "ok",
args: args{},
err: nil,
},
{
name: "ok/x509",
args: args{
x509: &linkedca.Template{
Template: []byte(`{"x": 1}`),
},
},
err: nil,
},
{
name: "ok/ssh",
args: args{
ssh: &linkedca.Template{
Template: []byte(`{"x": 1}`),
},
},
err: nil,
},
{
name: "fail/x509-template-missing-quote",
args: args{
x509: &linkedca.Template{
Template: []byte(`{ {{printf "%q" "quoted}} }`),
},
},
err: errors.New("invalid X.509 template: error parsing template: template: template:1: unterminated quoted string"),
},
{
name: "fail/x509-template-data",
args: args{
x509: &linkedca.Template{
Data: []byte(`{!?}`),
},
},
err: errors.New("invalid X.509 template data: error validating json template data"),
},
{
name: "fail/ssh-template-unknown-function",
args: args{
ssh: &linkedca.Template{
Template: []byte(`{ {{unknownFunction "foo"}} }`),
},
},
err: errors.New("invalid SSH template: error parsing template: template: template:1: function \"unknownFunction\" not defined"),
},
{
name: "fail/ssh-template-data",
args: args{
ssh: &linkedca.Template{
Data: []byte(`{!?}`),
},
},
err: errors.New("invalid SSH template data: error validating json template data"),
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := validateTemplates(tt.args.x509, tt.args.ssh)
if tt.err != nil {
assert.Error(t, err)
assert.Equals(t, tt.err.Error(), err.Error())
return
}
assert.Nil(t, err)
})
}
}