Fix PR comments

This commit is contained in:
Herman Slatman 2021-07-22 23:48:41 +02:00
parent b65a588d5b
commit c6bfc6eac2
No known key found for this signature in database
GPG key ID: F4D8A44EA0A75A4F
12 changed files with 62 additions and 121 deletions

View file

@ -52,8 +52,12 @@ type ExternalAccountKey struct {
BoundAt time.Time `json:"boundAt,omitempty"` BoundAt time.Time `json:"boundAt,omitempty"`
} }
func (eak *ExternalAccountKey) AlreadyBound() bool {
return !eak.BoundAt.IsZero()
}
func (eak *ExternalAccountKey) BindTo(account *Account) { func (eak *ExternalAccountKey) BindTo(account *Account) {
eak.AccountID = account.ID eak.AccountID = account.ID
eak.BoundAt = time.Now() eak.BoundAt = time.Now()
eak.KeyBytes = []byte{} // TODO: ensure that single use keys are OK eak.KeyBytes = []byte{} // clearing the key bytes; can only be used once
} }

View file

@ -8,7 +8,6 @@ import (
"github.com/go-chi/chi" "github.com/go-chi/chi"
"github.com/smallstep/certificates/acme" "github.com/smallstep/certificates/acme"
"github.com/smallstep/certificates/api" "github.com/smallstep/certificates/api"
"github.com/smallstep/certificates/authority/provisioner"
"github.com/smallstep/certificates/logging" "github.com/smallstep/certificates/logging"
squarejose "gopkg.in/square/go-jose.v2" squarejose "gopkg.in/square/go-jose.v2"
@ -125,7 +124,7 @@ func (h *Handler) NewAccount(w http.ResponseWriter, r *http.Request) {
api.WriteError(w, acme.WrapErrorISE(err, "error creating account")) api.WriteError(w, acme.WrapErrorISE(err, "error creating account"))
return return
} }
if eak != nil { // means that we have a (valid) External Account Binding key that should be used if eak != nil { // means that we have a (valid) External Account Binding key that should be bound, updated and sent in the response
eak.BindTo(acc) eak.BindTo(acc)
if err := h.db.UpdateExternalAccountKey(ctx, eak); err != nil { if err := h.db.UpdateExternalAccountKey(ctx, eak); err != nil {
api.WriteError(w, acme.WrapErrorISE(err, "error updating external account binding key")) api.WriteError(w, acme.WrapErrorISE(err, "error updating external account binding key"))
@ -134,7 +133,7 @@ func (h *Handler) NewAccount(w http.ResponseWriter, r *http.Request) {
acc.ExternalAccountBinding = nar.ExternalAccountBinding acc.ExternalAccountBinding = nar.ExternalAccountBinding
} }
} else { } else {
// Account exists // // Account exists
httpStatus = http.StatusOK httpStatus = http.StatusOK
} }
@ -227,19 +226,12 @@ func (h *Handler) GetOrdersByAccountID(w http.ResponseWriter, r *http.Request) {
// validateExternalAccountBinding validates the externalAccountBinding property in a call to new-account // validateExternalAccountBinding validates the externalAccountBinding property in a call to new-account
func (h *Handler) validateExternalAccountBinding(ctx context.Context, nar *NewAccountRequest) (*acme.ExternalAccountKey, error) { func (h *Handler) validateExternalAccountBinding(ctx context.Context, nar *NewAccountRequest) (*acme.ExternalAccountKey, error) {
acmeProv, err := acmeProvisionerFromContext(ctx)
prov, err := provisionerFromContext(ctx)
if err != nil { if err != nil {
return nil, err return nil, acme.WrapErrorISE(err, "could not load ACME provisioner from context")
} }
acmeProv, ok := prov.(*provisioner.ACME) // TODO: rewrite into providing configuration via function on acme.Provisioner if !acmeProv.RequireEAB {
if !ok || acmeProv == nil {
return nil, acme.NewErrorISE("provisioner in context is not an ACME provisioner")
}
shouldSkipAccountBindingValidation := !acmeProv.RequireEAB
if shouldSkipAccountBindingValidation {
return nil, nil return nil, nil
} }
@ -249,7 +241,7 @@ func (h *Handler) validateExternalAccountBinding(ctx context.Context, nar *NewAc
eabJSONBytes, err := json.Marshal(nar.ExternalAccountBinding) eabJSONBytes, err := json.Marshal(nar.ExternalAccountBinding)
if err != nil { if err != nil {
return nil, acme.WrapErrorISE(err, "error marshaling externalAccountBinding") return nil, acme.WrapErrorISE(err, "error marshaling externalAccountBinding into JSON")
} }
eabJWS, err := squarejose.ParseSigned(string(eabJSONBytes)) eabJWS, err := squarejose.ParseSigned(string(eabJSONBytes))
@ -266,8 +258,8 @@ func (h *Handler) validateExternalAccountBinding(ctx context.Context, nar *NewAc
return nil, acme.WrapErrorISE(err, "error retrieving external account key") return nil, acme.WrapErrorISE(err, "error retrieving external account key")
} }
if !externalAccountKey.BoundAt.IsZero() { // TODO: ensure that single use keys are OK if externalAccountKey.AlreadyBound() {
return nil, acme.NewError(acme.ErrorUnauthorizedType, "external account binding key with id '%s' was already used", keyID) return nil, acme.NewError(acme.ErrorUnauthorizedType, "external account binding key with id '%s' was already bound to account '%s' on %s", keyID, externalAccountKey.AccountID, externalAccountKey.BoundAt)
} }
payload, err := eabJWS.Verify(externalAccountKey.KeyBytes) payload, err := eabJWS.Verify(externalAccountKey.KeyBytes)

View file

@ -153,18 +153,12 @@ func (d *Directory) ToLog() (interface{}, error) {
// for client configuration. // for client configuration.
func (h *Handler) GetDirectory(w http.ResponseWriter, r *http.Request) { func (h *Handler) GetDirectory(w http.ResponseWriter, r *http.Request) {
ctx := r.Context() ctx := r.Context()
prov, err := provisionerFromContext(ctx) acmeProv, err := acmeProvisionerFromContext(ctx)
if err != nil { if err != nil {
api.WriteError(w, err) api.WriteError(w, err)
return return
} }
acmeProv, ok := prov.(*provisioner.ACME) // TODO: rewrite into providing configuration via function on acme.Provisioner
if !ok || acmeProv == nil {
api.WriteError(w, acme.NewErrorISE("provisioner in context is not an ACME provisioner"))
return
}
api.JSON(w, &Directory{ api.JSON(w, &Directory{
NewNonce: h.linker.GetLink(ctx, NewNonceLinkType), NewNonce: h.linker.GetLink(ctx, NewNonceLinkType),
NewAccount: h.linker.GetLink(ctx, NewAccountLinkType), NewAccount: h.linker.GetLink(ctx, NewAccountLinkType),

View file

@ -471,6 +471,20 @@ func provisionerFromContext(ctx context.Context) (acme.Provisioner, error) {
return pval, nil return pval, nil
} }
// acmeProvisionerFromContext searches the context for an ACME provisioner. Returns
// pointer to an ACME provisioner or an error.
func acmeProvisionerFromContext(ctx context.Context) (*provisioner.ACME, error) {
prov, err := provisionerFromContext(ctx)
if err != nil {
return nil, err
}
acmeProv, ok := prov.(*provisioner.ACME)
if !ok || acmeProv == nil {
return nil, acme.NewErrorISE("provisioner in context is not an ACME provisioner")
}
return acmeProv, nil
}
// payloadFromContext searches the context for a payload. Returns the payload // payloadFromContext searches the context for a payload. Returns the payload
// or an error. // or an error.
func payloadFromContext(ctx context.Context) (*payloadInfo, error) { func payloadFromContext(ctx context.Context) (*payloadInfo, error) {

View file

@ -29,7 +29,7 @@ func (h *Handler) CreateExternalAccountKey(w http.ResponseWriter, r *http.Reques
// TODO: Validate input // TODO: Validate input
eak, err := h.db.CreateExternalAccountKey(r.Context(), body.Name) eak, err := h.acmeDB.CreateExternalAccountKey(r.Context(), body.Name)
if err != nil { if err != nil {
api.WriteError(w, admin.WrapErrorISE(err, "error creating external account key %s", body.Name)) api.WriteError(w, admin.WrapErrorISE(err, "error creating external account key %s", body.Name))
return return

View file

@ -1,6 +1,7 @@
package api package api
import ( import (
"github.com/smallstep/certificates/acme"
"github.com/smallstep/certificates/api" "github.com/smallstep/certificates/api"
"github.com/smallstep/certificates/authority" "github.com/smallstep/certificates/authority"
"github.com/smallstep/certificates/authority/admin" "github.com/smallstep/certificates/authority/admin"
@ -8,15 +9,18 @@ import (
// Handler is the ACME API request handler. // Handler is the ACME API request handler.
type Handler struct { type Handler struct {
db admin.DB db admin.DB
auth *authority.Authority auth *authority.Authority
acmeDB acme.DB
} }
// NewHandler returns a new Authority Config Handler. // NewHandler returns a new Authority Config Handler.
func NewHandler(auth *authority.Authority) api.RouterHandler { func NewHandler(auth *authority.Authority, adminDB admin.DB, acmeDB acme.DB) api.RouterHandler {
h := &Handler{db: auth.GetAdminDatabase(), auth: auth} return &Handler{
db: adminDB,
return h auth: auth,
acmeDB: acmeDB,
}
} }
// Route traffic and implement the Router interface. // Route traffic and implement the Router interface.

View file

@ -7,8 +7,6 @@ import (
"github.com/pkg/errors" "github.com/pkg/errors"
"go.step.sm/linkedca" "go.step.sm/linkedca"
"github.com/smallstep/certificates/authority/admin/eak"
) )
const ( const (
@ -56,7 +54,7 @@ func UnmarshalProvisionerDetails(typ linkedca.Provisioner_Type, data []byte) (*l
return &linkedca.ProvisionerDetails{Data: v.Data}, nil return &linkedca.ProvisionerDetails{Data: v.Data}, nil
} }
// DB is the DB interface expected by the step-ca ACME API. // DB is the DB interface expected by the step-ca Admin API.
type DB interface { type DB interface {
CreateProvisioner(ctx context.Context, prov *linkedca.Provisioner) error CreateProvisioner(ctx context.Context, prov *linkedca.Provisioner) error
GetProvisioner(ctx context.Context, id string) (*linkedca.Provisioner, error) GetProvisioner(ctx context.Context, id string) (*linkedca.Provisioner, error)
@ -69,8 +67,6 @@ type DB interface {
GetAdmins(ctx context.Context) ([]*linkedca.Admin, error) GetAdmins(ctx context.Context) ([]*linkedca.Admin, error)
UpdateAdmin(ctx context.Context, admin *linkedca.Admin) error UpdateAdmin(ctx context.Context, admin *linkedca.Admin) error
DeleteAdmin(ctx context.Context, id string) error DeleteAdmin(ctx context.Context, id string) error
CreateExternalAccountKey(ctx context.Context, name string) (*eak.ExternalAccountKey, error)
} }
// MockDB is an implementation of the DB interface that should only be used as // MockDB is an implementation of the DB interface that should only be used as
@ -88,8 +84,6 @@ type MockDB struct {
MockUpdateAdmin func(ctx context.Context, adm *linkedca.Admin) error MockUpdateAdmin func(ctx context.Context, adm *linkedca.Admin) error
MockDeleteAdmin func(ctx context.Context, id string) error MockDeleteAdmin func(ctx context.Context, id string) error
MockCreateExternalAccountKey func(ctx context.Context, name string) (*eak.ExternalAccountKey, error)
MockError error MockError error
MockRet1 interface{} MockRet1 interface{}
} }
@ -183,10 +177,3 @@ func (m *MockDB) DeleteAdmin(ctx context.Context, id string) error {
} }
return m.MockError return m.MockError
} }
func (m *MockDB) CreateExternalAccountKey(ctx context.Context, name string) (*eak.ExternalAccountKey, error) {
if m.MockCreateExternalAccountKey != nil {
return m.MockCreateExternalAccountKey(ctx, name)
}
return m.MockRet1.(*eak.ExternalAccountKey), m.MockError
}

View file

@ -11,7 +11,6 @@ import (
"github.com/smallstep/certificates/authority/admin" "github.com/smallstep/certificates/authority/admin"
"github.com/smallstep/certificates/db" "github.com/smallstep/certificates/db"
"github.com/smallstep/nosql" "github.com/smallstep/nosql"
"github.com/smallstep/nosql/database"
nosqldb "github.com/smallstep/nosql/database" nosqldb "github.com/smallstep/nosql/database"
"go.step.sm/linkedca" "go.step.sm/linkedca"
"google.golang.org/protobuf/types/known/timestamppb" "google.golang.org/protobuf/types/known/timestamppb"
@ -996,7 +995,7 @@ func TestDB_GetAdmins(t *testing.T) {
"fail/db.List-error": func(t *testing.T) test { "fail/db.List-error": func(t *testing.T) test {
return test{ return test{
db: &db.MockNoSQLDB{ db: &db.MockNoSQLDB{
MList: func(bucket []byte) ([]*database.Entry, error) { MList: func(bucket []byte) ([]*nosqldb.Entry, error) {
assert.Equals(t, bucket, adminsTable) assert.Equals(t, bucket, adminsTable)
return nil, errors.New("force") return nil, errors.New("force")
@ -1006,14 +1005,14 @@ func TestDB_GetAdmins(t *testing.T) {
} }
}, },
"fail/unmarshal-error": func(t *testing.T) test { "fail/unmarshal-error": func(t *testing.T) test {
ret := []*database.Entry{ ret := []*nosqldb.Entry{
{Bucket: adminsTable, Key: []byte("foo"), Value: foob}, {Bucket: adminsTable, Key: []byte("foo"), Value: foob},
{Bucket: adminsTable, Key: []byte("bar"), Value: barb}, {Bucket: adminsTable, Key: []byte("bar"), Value: barb},
{Bucket: adminsTable, Key: []byte("zap"), Value: []byte("zap")}, {Bucket: adminsTable, Key: []byte("zap"), Value: []byte("zap")},
} }
return test{ return test{
db: &db.MockNoSQLDB{ db: &db.MockNoSQLDB{
MList: func(bucket []byte) ([]*database.Entry, error) { MList: func(bucket []byte) ([]*nosqldb.Entry, error) {
assert.Equals(t, bucket, adminsTable) assert.Equals(t, bucket, adminsTable)
return ret, nil return ret, nil
@ -1023,10 +1022,10 @@ func TestDB_GetAdmins(t *testing.T) {
} }
}, },
"ok/none": func(t *testing.T) test { "ok/none": func(t *testing.T) test {
ret := []*database.Entry{} ret := []*nosqldb.Entry{}
return test{ return test{
db: &db.MockNoSQLDB{ db: &db.MockNoSQLDB{
MList: func(bucket []byte) ([]*database.Entry, error) { MList: func(bucket []byte) ([]*nosqldb.Entry, error) {
assert.Equals(t, bucket, adminsTable) assert.Equals(t, bucket, adminsTable)
return ret, nil return ret, nil
@ -1038,13 +1037,13 @@ func TestDB_GetAdmins(t *testing.T) {
} }
}, },
"ok/only-invalid": func(t *testing.T) test { "ok/only-invalid": func(t *testing.T) test {
ret := []*database.Entry{ ret := []*nosqldb.Entry{
{Bucket: adminsTable, Key: []byte("bar"), Value: barb}, {Bucket: adminsTable, Key: []byte("bar"), Value: barb},
{Bucket: adminsTable, Key: []byte("baz"), Value: bazb}, {Bucket: adminsTable, Key: []byte("baz"), Value: bazb},
} }
return test{ return test{
db: &db.MockNoSQLDB{ db: &db.MockNoSQLDB{
MList: func(bucket []byte) ([]*database.Entry, error) { MList: func(bucket []byte) ([]*nosqldb.Entry, error) {
assert.Equals(t, bucket, adminsTable) assert.Equals(t, bucket, adminsTable)
return ret, nil return ret, nil
@ -1056,7 +1055,7 @@ func TestDB_GetAdmins(t *testing.T) {
} }
}, },
"ok": func(t *testing.T) test { "ok": func(t *testing.T) test {
ret := []*database.Entry{ ret := []*nosqldb.Entry{
{Bucket: adminsTable, Key: []byte("foo"), Value: foob}, {Bucket: adminsTable, Key: []byte("foo"), Value: foob},
{Bucket: adminsTable, Key: []byte("bar"), Value: barb}, {Bucket: adminsTable, Key: []byte("bar"), Value: barb},
{Bucket: adminsTable, Key: []byte("baz"), Value: bazb}, {Bucket: adminsTable, Key: []byte("baz"), Value: bazb},
@ -1064,7 +1063,7 @@ func TestDB_GetAdmins(t *testing.T) {
} }
return test{ return test{
db: &db.MockNoSQLDB{ db: &db.MockNoSQLDB{
MList: func(bucket []byte) ([]*database.Entry, error) { MList: func(bucket []byte) ([]*nosqldb.Entry, error) {
assert.Equals(t, bucket, adminsTable) assert.Equals(t, bucket, adminsTable)
return ret, nil return ret, nil

View file

@ -1,51 +0,0 @@
package nosql
import (
"context"
"crypto/rand"
"time"
"github.com/smallstep/certificates/authority/admin/eak"
)
type dbExternalAccountKey struct {
ID string `json:"id"`
Name string `json:"name"`
AccountID string `json:"accountID,omitempty"`
KeyBytes []byte `json:"key,omitempty"`
CreatedAt time.Time `json:"createdAt"`
BoundAt time.Time `json:"boundAt"`
}
// CreateExternalAccountKey creates a new External Account Binding key
func (db *DB) CreateExternalAccountKey(ctx context.Context, name string) (*eak.ExternalAccountKey, error) {
keyID, err := randID()
if err != nil {
return nil, err
}
random := make([]byte, 32)
_, err = rand.Read(random)
if err != nil {
return nil, err
}
dbeak := &dbExternalAccountKey{
ID: keyID,
Name: name,
KeyBytes: random,
CreatedAt: clock.Now(),
}
if err = db.save(ctx, keyID, dbeak, nil, "external_account_key", externalAccountKeyTable); err != nil {
return nil, err
}
return &eak.ExternalAccountKey{
ID: dbeak.ID,
Name: dbeak.Name,
AccountID: dbeak.AccountID,
KeyBytes: dbeak.KeyBytes,
CreatedAt: dbeak.CreatedAt,
BoundAt: dbeak.BoundAt,
}, nil
}

View file

@ -11,9 +11,8 @@ import (
) )
var ( var (
adminsTable = []byte("admins") adminsTable = []byte("admins")
provisionersTable = []byte("provisioners") provisionersTable = []byte("provisioners")
externalAccountKeyTable = []byte("acme_external_account_keys")
) )
// DB is a struct that implements the AdminDB interface. // DB is a struct that implements the AdminDB interface.
@ -24,7 +23,7 @@ type DB struct {
// New configures and returns a new Authority DB backend implemented using a nosql DB. // New configures and returns a new Authority DB backend implemented using a nosql DB.
func New(db nosqlDB.DB, authorityID string) (*DB, error) { func New(db nosqlDB.DB, authorityID string) (*DB, error) {
tables := [][]byte{adminsTable, provisionersTable, externalAccountKeyTable} tables := [][]byte{adminsTable, provisionersTable}
for _, b := range tables { for _, b := range tables {
if err := db.CreateTable(b); err != nil { if err := db.CreateTable(b); err != nil {
return nil, errors.Wrapf(err, "error creating table %s", return nil, errors.Wrapf(err, "error creating table %s",

View file

@ -11,7 +11,6 @@ import (
"github.com/smallstep/certificates/authority/admin" "github.com/smallstep/certificates/authority/admin"
"github.com/smallstep/certificates/db" "github.com/smallstep/certificates/db"
"github.com/smallstep/nosql" "github.com/smallstep/nosql"
"github.com/smallstep/nosql/database"
nosqldb "github.com/smallstep/nosql/database" nosqldb "github.com/smallstep/nosql/database"
"go.step.sm/linkedca" "go.step.sm/linkedca"
) )
@ -746,7 +745,7 @@ func TestDB_GetProvisioners(t *testing.T) {
"fail/db.List-error": func(t *testing.T) test { "fail/db.List-error": func(t *testing.T) test {
return test{ return test{
db: &db.MockNoSQLDB{ db: &db.MockNoSQLDB{
MList: func(bucket []byte) ([]*database.Entry, error) { MList: func(bucket []byte) ([]*nosqldb.Entry, error) {
assert.Equals(t, bucket, provisionersTable) assert.Equals(t, bucket, provisionersTable)
return nil, errors.New("force") return nil, errors.New("force")
@ -756,14 +755,14 @@ func TestDB_GetProvisioners(t *testing.T) {
} }
}, },
"fail/unmarshal-error": func(t *testing.T) test { "fail/unmarshal-error": func(t *testing.T) test {
ret := []*database.Entry{ ret := []*nosqldb.Entry{
{Bucket: provisionersTable, Key: []byte("foo"), Value: foob}, {Bucket: provisionersTable, Key: []byte("foo"), Value: foob},
{Bucket: provisionersTable, Key: []byte("bar"), Value: barb}, {Bucket: provisionersTable, Key: []byte("bar"), Value: barb},
{Bucket: provisionersTable, Key: []byte("zap"), Value: []byte("zap")}, {Bucket: provisionersTable, Key: []byte("zap"), Value: []byte("zap")},
} }
return test{ return test{
db: &db.MockNoSQLDB{ db: &db.MockNoSQLDB{
MList: func(bucket []byte) ([]*database.Entry, error) { MList: func(bucket []byte) ([]*nosqldb.Entry, error) {
assert.Equals(t, bucket, provisionersTable) assert.Equals(t, bucket, provisionersTable)
return ret, nil return ret, nil
@ -773,10 +772,10 @@ func TestDB_GetProvisioners(t *testing.T) {
} }
}, },
"ok/none": func(t *testing.T) test { "ok/none": func(t *testing.T) test {
ret := []*database.Entry{} ret := []*nosqldb.Entry{}
return test{ return test{
db: &db.MockNoSQLDB{ db: &db.MockNoSQLDB{
MList: func(bucket []byte) ([]*database.Entry, error) { MList: func(bucket []byte) ([]*nosqldb.Entry, error) {
assert.Equals(t, bucket, provisionersTable) assert.Equals(t, bucket, provisionersTable)
return ret, nil return ret, nil
@ -788,13 +787,13 @@ func TestDB_GetProvisioners(t *testing.T) {
} }
}, },
"ok/only-invalid": func(t *testing.T) test { "ok/only-invalid": func(t *testing.T) test {
ret := []*database.Entry{ ret := []*nosqldb.Entry{
{Bucket: provisionersTable, Key: []byte("bar"), Value: barb}, {Bucket: provisionersTable, Key: []byte("bar"), Value: barb},
{Bucket: provisionersTable, Key: []byte("baz"), Value: bazb}, {Bucket: provisionersTable, Key: []byte("baz"), Value: bazb},
} }
return test{ return test{
db: &db.MockNoSQLDB{ db: &db.MockNoSQLDB{
MList: func(bucket []byte) ([]*database.Entry, error) { MList: func(bucket []byte) ([]*nosqldb.Entry, error) {
assert.Equals(t, bucket, provisionersTable) assert.Equals(t, bucket, provisionersTable)
return ret, nil return ret, nil
@ -806,7 +805,7 @@ func TestDB_GetProvisioners(t *testing.T) {
} }
}, },
"ok": func(t *testing.T) test { "ok": func(t *testing.T) test {
ret := []*database.Entry{ ret := []*nosqldb.Entry{
{Bucket: provisionersTable, Key: []byte("foo"), Value: foob}, {Bucket: provisionersTable, Key: []byte("foo"), Value: foob},
{Bucket: provisionersTable, Key: []byte("bar"), Value: barb}, {Bucket: provisionersTable, Key: []byte("bar"), Value: barb},
{Bucket: provisionersTable, Key: []byte("baz"), Value: bazb}, {Bucket: provisionersTable, Key: []byte("baz"), Value: bazb},
@ -814,7 +813,7 @@ func TestDB_GetProvisioners(t *testing.T) {
} }
return test{ return test{
db: &db.MockNoSQLDB{ db: &db.MockNoSQLDB{
MList: func(bucket []byte) ([]*database.Entry, error) { MList: func(bucket []byte) ([]*nosqldb.Entry, error) {
assert.Equals(t, bucket, provisionersTable) assert.Equals(t, bucket, provisionersTable)
return ret, nil return ret, nil

View file

@ -182,7 +182,7 @@ func (ca *CA) Init(config *config.Config) (*CA, error) {
if config.AuthorityConfig.EnableAdmin { if config.AuthorityConfig.EnableAdmin {
adminDB := auth.GetAdminDatabase() adminDB := auth.GetAdminDatabase()
if adminDB != nil { if adminDB != nil {
adminHandler := adminAPI.NewHandler(auth) adminHandler := adminAPI.NewHandler(auth, adminDB, acmeDB)
mux.Route("/admin", func(r chi.Router) { mux.Route("/admin", func(r chi.Router) {
adminHandler.Route(r) adminHandler.Route(r)
}) })