Refactor ACME EAB queries

The ACME EAB keys are now also indexed by the provisioner. This
solves part of the issue in which too many EAB keys may be in
memory at a given time.
This commit is contained in:
Herman Slatman 2022-01-07 16:59:55 +01:00
parent 30859d3c83
commit ef16febf40
No known key found for this signature in database
GPG key ID: F4D8A44EA0A75A4F
10 changed files with 766 additions and 553 deletions

View file

@ -46,7 +46,7 @@ func KeyToID(jwk *jose.JSONWebKey) (string, error) {
// ExternalAccountKey is an ACME External Account Binding key. // ExternalAccountKey is an ACME External Account Binding key.
type ExternalAccountKey struct { type ExternalAccountKey struct {
ID string `json:"id"` ID string `json:"id"`
Provisioner string `json:"provisioner"` ProvisionerID string `json:"provisionerID"`
Reference string `json:"reference"` Reference string `json:"reference"`
AccountID string `json:"-"` AccountID string `json:"-"`
KeyBytes []byte `json:"-"` KeyBytes []byte `json:"-"`

View file

@ -93,7 +93,7 @@ func TestExternalAccountKey_BindTo(t *testing.T) {
name: "ok", name: "ok",
eak: &ExternalAccountKey{ eak: &ExternalAccountKey{
ID: "eakID", ID: "eakID",
Provisioner: "prov", ProvisionerID: "provID",
Reference: "ref", Reference: "ref",
KeyBytes: []byte{1, 3, 3, 7}, KeyBytes: []byte{1, 3, 3, 7},
}, },
@ -106,7 +106,7 @@ func TestExternalAccountKey_BindTo(t *testing.T) {
name: "fail/already-bound", name: "fail/already-bound",
eak: &ExternalAccountKey{ eak: &ExternalAccountKey{
ID: "eakID", ID: "eakID",
Provisioner: "prov", ProvisionerID: "provID",
Reference: "ref", Reference: "ref",
KeyBytes: []byte{1, 3, 3, 7}, KeyBytes: []byte{1, 3, 3, 7},
AccountID: "someAccountID", AccountID: "someAccountID",

View file

@ -144,7 +144,7 @@ func (h *Handler) NewAccount(w http.ResponseWriter, r *http.Request) {
api.WriteError(w, err) api.WriteError(w, err)
return return
} }
if err := h.db.UpdateExternalAccountKey(ctx, prov.Name, eak); err != nil { if err := h.db.UpdateExternalAccountKey(ctx, prov.ID, 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"))
return return
} }
@ -274,7 +274,7 @@ func (h *Handler) validateExternalAccountBinding(ctx context.Context, nar *NewAc
return nil, acmeErr return nil, acmeErr
} }
externalAccountKey, err := h.db.GetExternalAccountKey(ctx, acmeProv.Name, keyID) externalAccountKey, err := h.db.GetExternalAccountKey(ctx, acmeProv.ID, keyID)
if err != nil { if err != nil {
if _, ok := err.(*acme.Error); ok { if _, ok := err.(*acme.Error); ok {
return nil, acme.WrapError(acme.ErrorUnauthorizedType, err, "the field 'kid' references an unknown key") return nil, acme.WrapError(acme.ErrorUnauthorizedType, err, "the field 'kid' references an unknown key")

View file

@ -351,6 +351,7 @@ func TestHandler_NewAccount(t *testing.T) {
prov := newProv() prov := newProv()
escProvName := url.PathEscape(prov.GetName()) escProvName := url.PathEscape(prov.GetName())
baseURL := &url.URL{Scheme: "https", Host: "test.ca.smallstep.com"} baseURL := &url.URL{Scheme: "https", Host: "test.ca.smallstep.com"}
provID := prov.GetID()
type test struct { type test struct {
db acme.DB db acme.DB
@ -555,7 +556,7 @@ func TestHandler_NewAccount(t *testing.T) {
ctx = context.WithValue(ctx, jwsContextKey, parsedJWS) ctx = context.WithValue(ctx, jwsContextKey, parsedJWS)
eak := &acme.ExternalAccountKey{ eak := &acme.ExternalAccountKey{
ID: "eakID", ID: "eakID",
Provisioner: escProvName, ProvisionerID: provID,
Reference: "testeak", Reference: "testeak",
KeyBytes: []byte{1, 3, 3, 7}, KeyBytes: []byte{1, 3, 3, 7},
CreatedAt: time.Now(), CreatedAt: time.Now(),
@ -732,7 +733,7 @@ func TestHandler_NewAccount(t *testing.T) {
MockGetExternalAccountKey: func(ctx context.Context, provisionerName, keyID string) (*acme.ExternalAccountKey, error) { MockGetExternalAccountKey: func(ctx context.Context, provisionerName, keyID string) (*acme.ExternalAccountKey, error) {
return &acme.ExternalAccountKey{ return &acme.ExternalAccountKey{
ID: "eakID", ID: "eakID",
Provisioner: escProvName, ProvisionerID: provID,
Reference: "testeak", Reference: "testeak",
KeyBytes: []byte{1, 3, 3, 7}, KeyBytes: []byte{1, 3, 3, 7},
CreatedAt: time.Now(), CreatedAt: time.Now(),
@ -1056,6 +1057,7 @@ func TestHandler_validateExternalAccountBinding(t *testing.T) {
acmeProv := newACMEProv(t) acmeProv := newACMEProv(t)
escProvName := url.PathEscape(acmeProv.GetName()) escProvName := url.PathEscape(acmeProv.GetName())
baseURL := &url.URL{Scheme: "https", Host: "test.ca.smallstep.com"} baseURL := &url.URL{Scheme: "https", Host: "test.ca.smallstep.com"}
provID := acmeProv.GetID()
type test struct { type test struct {
db acme.DB db acme.DB
ctx context.Context ctx context.Context
@ -1129,7 +1131,7 @@ func TestHandler_validateExternalAccountBinding(t *testing.T) {
MockGetExternalAccountKey: func(ctx context.Context, provisionerName, keyID string) (*acme.ExternalAccountKey, error) { MockGetExternalAccountKey: func(ctx context.Context, provisionerName, keyID string) (*acme.ExternalAccountKey, error) {
return &acme.ExternalAccountKey{ return &acme.ExternalAccountKey{
ID: "eakID", ID: "eakID",
Provisioner: escProvName, ProvisionerID: provID,
Reference: "testeak", Reference: "testeak",
KeyBytes: []byte{1, 3, 3, 7}, KeyBytes: []byte{1, 3, 3, 7},
CreatedAt: createdAt, CreatedAt: createdAt,
@ -1143,7 +1145,7 @@ func TestHandler_validateExternalAccountBinding(t *testing.T) {
}, },
eak: &acme.ExternalAccountKey{ eak: &acme.ExternalAccountKey{
ID: "eakID", ID: "eakID",
Provisioner: escProvName, ProvisionerID: provID,
Reference: "testeak", Reference: "testeak",
KeyBytes: []byte{1, 3, 3, 7}, KeyBytes: []byte{1, 3, 3, 7},
CreatedAt: createdAt, CreatedAt: createdAt,
@ -1493,7 +1495,7 @@ func TestHandler_validateExternalAccountBinding(t *testing.T) {
MockGetExternalAccountKey: func(ctx context.Context, provisionerName, keyID string) (*acme.ExternalAccountKey, error) { MockGetExternalAccountKey: func(ctx context.Context, provisionerName, keyID string) (*acme.ExternalAccountKey, error) {
return &acme.ExternalAccountKey{ return &acme.ExternalAccountKey{
ID: "eakID", ID: "eakID",
Provisioner: escProvName, ProvisionerID: provID,
Reference: "testeak", Reference: "testeak",
CreatedAt: createdAt, CreatedAt: createdAt,
AccountID: "some-account-id", AccountID: "some-account-id",
@ -1550,7 +1552,7 @@ func TestHandler_validateExternalAccountBinding(t *testing.T) {
MockGetExternalAccountKey: func(ctx context.Context, provisionerName, keyID string) (*acme.ExternalAccountKey, error) { MockGetExternalAccountKey: func(ctx context.Context, provisionerName, keyID string) (*acme.ExternalAccountKey, error) {
return &acme.ExternalAccountKey{ return &acme.ExternalAccountKey{
ID: "eakID", ID: "eakID",
Provisioner: escProvName, ProvisionerID: provID,
Reference: "testeak", Reference: "testeak",
KeyBytes: []byte{1, 2, 3, 4}, KeyBytes: []byte{1, 2, 3, 4},
CreatedAt: time.Now(), CreatedAt: time.Now(),
@ -1608,7 +1610,7 @@ func TestHandler_validateExternalAccountBinding(t *testing.T) {
MockGetExternalAccountKey: func(ctx context.Context, provisionerName, keyID string) (*acme.ExternalAccountKey, error) { MockGetExternalAccountKey: func(ctx context.Context, provisionerName, keyID string) (*acme.ExternalAccountKey, error) {
return &acme.ExternalAccountKey{ return &acme.ExternalAccountKey{
ID: "eakID", ID: "eakID",
Provisioner: escProvName, ProvisionerID: provID,
Reference: "testeak", Reference: "testeak",
KeyBytes: []byte{1, 3, 3, 7}, KeyBytes: []byte{1, 3, 3, 7},
CreatedAt: time.Now(), CreatedAt: time.Now(),
@ -1663,7 +1665,7 @@ func TestHandler_validateExternalAccountBinding(t *testing.T) {
MockGetExternalAccountKey: func(ctx context.Context, provisionerName, keyID string) (*acme.ExternalAccountKey, error) { MockGetExternalAccountKey: func(ctx context.Context, provisionerName, keyID string) (*acme.ExternalAccountKey, error) {
return &acme.ExternalAccountKey{ return &acme.ExternalAccountKey{
ID: "eakID", ID: "eakID",
Provisioner: escProvName, ProvisionerID: provID,
Reference: "testeak", Reference: "testeak",
KeyBytes: []byte{1, 3, 3, 7}, KeyBytes: []byte{1, 3, 3, 7},
CreatedAt: time.Now(), CreatedAt: time.Now(),
@ -1719,7 +1721,7 @@ func TestHandler_validateExternalAccountBinding(t *testing.T) {
MockGetExternalAccountKey: func(ctx context.Context, provisionerName, keyID string) (*acme.ExternalAccountKey, error) { MockGetExternalAccountKey: func(ctx context.Context, provisionerName, keyID string) (*acme.ExternalAccountKey, error) {
return &acme.ExternalAccountKey{ return &acme.ExternalAccountKey{
ID: "eakID", ID: "eakID",
Provisioner: escProvName, ProvisionerID: provID,
Reference: "testeak", Reference: "testeak",
KeyBytes: []byte{1, 3, 3, 7}, KeyBytes: []byte{1, 3, 3, 7},
CreatedAt: time.Now(), CreatedAt: time.Now(),
@ -1765,7 +1767,7 @@ func TestHandler_validateExternalAccountBinding(t *testing.T) {
assert.NotNil(t, tc.eak) assert.NotNil(t, tc.eak)
assert.Equals(t, got.ID, tc.eak.ID) assert.Equals(t, got.ID, tc.eak.ID)
assert.Equals(t, got.KeyBytes, tc.eak.KeyBytes) assert.Equals(t, got.KeyBytes, tc.eak.KeyBytes)
assert.Equals(t, got.Provisioner, tc.eak.Provisioner) assert.Equals(t, got.ProvisionerID, tc.eak.ProvisionerID)
assert.Equals(t, got.Reference, tc.eak.Reference) assert.Equals(t, got.Reference, tc.eak.Reference)
assert.Equals(t, got.CreatedAt, tc.eak.CreatedAt) assert.Equals(t, got.CreatedAt, tc.eak.CreatedAt)
assert.Equals(t, got.AccountID, tc.eak.AccountID) assert.Equals(t, got.AccountID, tc.eak.AccountID)

View file

@ -19,12 +19,12 @@ type DB interface {
GetAccountByKeyID(ctx context.Context, kid string) (*Account, error) GetAccountByKeyID(ctx context.Context, kid string) (*Account, error)
UpdateAccount(ctx context.Context, acc *Account) error UpdateAccount(ctx context.Context, acc *Account) error
CreateExternalAccountKey(ctx context.Context, provisionerName, reference string) (*ExternalAccountKey, error) CreateExternalAccountKey(ctx context.Context, provisionerID, reference string) (*ExternalAccountKey, error)
GetExternalAccountKey(ctx context.Context, provisionerName, keyID string) (*ExternalAccountKey, error) GetExternalAccountKey(ctx context.Context, provisionerID, keyID string) (*ExternalAccountKey, error)
GetExternalAccountKeys(ctx context.Context, provisionerName string) ([]*ExternalAccountKey, error) GetExternalAccountKeys(ctx context.Context, provisionerID string) ([]*ExternalAccountKey, error)
GetExternalAccountKeyByReference(ctx context.Context, provisionerName, reference string) (*ExternalAccountKey, error) GetExternalAccountKeyByReference(ctx context.Context, provisionerID, reference string) (*ExternalAccountKey, error)
DeleteExternalAccountKey(ctx context.Context, provisionerName, keyID string) error DeleteExternalAccountKey(ctx context.Context, provisionerID, keyID string) error
UpdateExternalAccountKey(ctx context.Context, provisionerName string, eak *ExternalAccountKey) error UpdateExternalAccountKey(ctx context.Context, provisionerID string, eak *ExternalAccountKey) error
CreateNonce(ctx context.Context) (Nonce, error) CreateNonce(ctx context.Context) (Nonce, error)
DeleteNonce(ctx context.Context, nonce Nonce) error DeleteNonce(ctx context.Context, nonce Nonce) error
@ -56,12 +56,12 @@ type MockDB struct {
MockGetAccountByKeyID func(ctx context.Context, kid string) (*Account, error) MockGetAccountByKeyID func(ctx context.Context, kid string) (*Account, error)
MockUpdateAccount func(ctx context.Context, acc *Account) error MockUpdateAccount func(ctx context.Context, acc *Account) error
MockCreateExternalAccountKey func(ctx context.Context, provisionerName, reference string) (*ExternalAccountKey, error) MockCreateExternalAccountKey func(ctx context.Context, provisionerID, reference string) (*ExternalAccountKey, error)
MockGetExternalAccountKey func(ctx context.Context, provisionerName, keyID string) (*ExternalAccountKey, error) MockGetExternalAccountKey func(ctx context.Context, provisionerID, keyID string) (*ExternalAccountKey, error)
MockGetExternalAccountKeys func(ctx context.Context, provisionerName string) ([]*ExternalAccountKey, error) MockGetExternalAccountKeys func(ctx context.Context, provisionerID string) ([]*ExternalAccountKey, error)
MockGetExternalAccountKeyByReference func(ctx context.Context, provisionerName, reference string) (*ExternalAccountKey, error) MockGetExternalAccountKeyByReference func(ctx context.Context, provisionerID, reference string) (*ExternalAccountKey, error)
MockDeleteExternalAccountKey func(ctx context.Context, provisionerName, keyID string) error MockDeleteExternalAccountKey func(ctx context.Context, provisionerID, keyID string) error
MockUpdateExternalAccountKey func(ctx context.Context, provisionerName string, eak *ExternalAccountKey) error MockUpdateExternalAccountKey func(ctx context.Context, provisionerID string, eak *ExternalAccountKey) error
MockCreateNonce func(ctx context.Context) (Nonce, error) MockCreateNonce func(ctx context.Context) (Nonce, error)
MockDeleteNonce func(ctx context.Context, nonce Nonce) error MockDeleteNonce func(ctx context.Context, nonce Nonce) error
@ -129,9 +129,9 @@ func (m *MockDB) UpdateAccount(ctx context.Context, acc *Account) error {
} }
// CreateExternalAccountKey mock // CreateExternalAccountKey mock
func (m *MockDB) CreateExternalAccountKey(ctx context.Context, provisionerName, reference string) (*ExternalAccountKey, error) { func (m *MockDB) CreateExternalAccountKey(ctx context.Context, provisionerID, reference string) (*ExternalAccountKey, error) {
if m.MockCreateExternalAccountKey != nil { if m.MockCreateExternalAccountKey != nil {
return m.MockCreateExternalAccountKey(ctx, provisionerName, reference) return m.MockCreateExternalAccountKey(ctx, provisionerID, reference)
} else if m.MockError != nil { } else if m.MockError != nil {
return nil, m.MockError return nil, m.MockError
} }
@ -139,9 +139,9 @@ func (m *MockDB) CreateExternalAccountKey(ctx context.Context, provisionerName,
} }
// GetExternalAccountKey mock // GetExternalAccountKey mock
func (m *MockDB) GetExternalAccountKey(ctx context.Context, provisionerName, keyID string) (*ExternalAccountKey, error) { func (m *MockDB) GetExternalAccountKey(ctx context.Context, provisionerID, keyID string) (*ExternalAccountKey, error) {
if m.MockGetExternalAccountKey != nil { if m.MockGetExternalAccountKey != nil {
return m.MockGetExternalAccountKey(ctx, provisionerName, keyID) return m.MockGetExternalAccountKey(ctx, provisionerID, keyID)
} else if m.MockError != nil { } else if m.MockError != nil {
return nil, m.MockError return nil, m.MockError
} }
@ -149,9 +149,9 @@ func (m *MockDB) GetExternalAccountKey(ctx context.Context, provisionerName, key
} }
// GetExternalAccountKeys mock // GetExternalAccountKeys mock
func (m *MockDB) GetExternalAccountKeys(ctx context.Context, provisionerName string) ([]*ExternalAccountKey, error) { func (m *MockDB) GetExternalAccountKeys(ctx context.Context, provisionerID string) ([]*ExternalAccountKey, error) {
if m.MockGetExternalAccountKeys != nil { if m.MockGetExternalAccountKeys != nil {
return m.MockGetExternalAccountKeys(ctx, provisionerName) return m.MockGetExternalAccountKeys(ctx, provisionerID)
} else if m.MockError != nil { } else if m.MockError != nil {
return nil, m.MockError return nil, m.MockError
} }
@ -159,9 +159,9 @@ func (m *MockDB) GetExternalAccountKeys(ctx context.Context, provisionerName str
} }
// GetExternalAccountKeyByReference mock // GetExternalAccountKeyByReference mock
func (m *MockDB) GetExternalAccountKeyByReference(ctx context.Context, provisionerName, reference string) (*ExternalAccountKey, error) { func (m *MockDB) GetExternalAccountKeyByReference(ctx context.Context, provisionerID, reference string) (*ExternalAccountKey, error) {
if m.MockGetExternalAccountKeyByReference != nil { if m.MockGetExternalAccountKeyByReference != nil {
return m.MockGetExternalAccountKeyByReference(ctx, provisionerName, reference) return m.MockGetExternalAccountKeyByReference(ctx, provisionerID, reference)
} else if m.MockError != nil { } else if m.MockError != nil {
return nil, m.MockError return nil, m.MockError
} }
@ -169,9 +169,9 @@ func (m *MockDB) GetExternalAccountKeyByReference(ctx context.Context, provision
} }
// DeleteExternalAccountKey mock // DeleteExternalAccountKey mock
func (m *MockDB) DeleteExternalAccountKey(ctx context.Context, provisionerName, keyID string) error { func (m *MockDB) DeleteExternalAccountKey(ctx context.Context, provisionerID, keyID string) error {
if m.MockDeleteExternalAccountKey != nil { if m.MockDeleteExternalAccountKey != nil {
return m.MockDeleteExternalAccountKey(ctx, provisionerName, keyID) return m.MockDeleteExternalAccountKey(ctx, provisionerID, keyID)
} else if m.MockError != nil { } else if m.MockError != nil {
return m.MockError return m.MockError
} }
@ -179,9 +179,9 @@ func (m *MockDB) DeleteExternalAccountKey(ctx context.Context, provisionerName,
} }
// UpdateExternalAccountKey mock // UpdateExternalAccountKey mock
func (m *MockDB) UpdateExternalAccountKey(ctx context.Context, provisionerName string, eak *ExternalAccountKey) error { func (m *MockDB) UpdateExternalAccountKey(ctx context.Context, provisionerID string, eak *ExternalAccountKey) error {
if m.MockUpdateExternalAccountKey != nil { if m.MockUpdateExternalAccountKey != nil {
return m.MockUpdateExternalAccountKey(ctx, provisionerName, eak) return m.MockUpdateExternalAccountKey(ctx, provisionerID, eak)
} else if m.MockError != nil { } else if m.MockError != nil {
return m.MockError return m.MockError
} }

View file

@ -4,6 +4,7 @@ import (
"context" "context"
"crypto/rand" "crypto/rand"
"encoding/json" "encoding/json"
"sync"
"time" "time"
"github.com/pkg/errors" "github.com/pkg/errors"
@ -12,6 +13,9 @@ import (
"go.step.sm/crypto/jose" "go.step.sm/crypto/jose"
) )
// Mutex for locking referencesByProvisioner index operations.
var referencesByProvisionerIndexMux sync.Mutex
// dbAccount represents an ACME account. // dbAccount represents an ACME account.
type dbAccount struct { type dbAccount struct {
ID string `json:"id"` ID string `json:"id"`
@ -29,7 +33,7 @@ func (dba *dbAccount) clone() *dbAccount {
type dbExternalAccountKey struct { type dbExternalAccountKey struct {
ID string `json:"id"` ID string `json:"id"`
Provisioner string `json:"provisioner"` ProvisionerID string `json:"provisionerID"`
Reference string `json:"reference"` Reference string `json:"reference"`
AccountID string `json:"accountID,omitempty"` AccountID string `json:"accountID,omitempty"`
KeyBytes []byte `json:"key"` KeyBytes []byte `json:"key"`
@ -170,7 +174,7 @@ func (db *DB) UpdateAccount(ctx context.Context, acc *acme.Account) error {
} }
// CreateExternalAccountKey creates a new External Account Binding key with a name // CreateExternalAccountKey creates a new External Account Binding key with a name
func (db *DB) CreateExternalAccountKey(ctx context.Context, provisionerName, reference string) (*acme.ExternalAccountKey, error) { func (db *DB) CreateExternalAccountKey(ctx context.Context, provisionerID, reference string) (*acme.ExternalAccountKey, error) {
keyID, err := randID() keyID, err := randID()
if err != nil { if err != nil {
return nil, err return nil, err
@ -184,7 +188,7 @@ func (db *DB) CreateExternalAccountKey(ctx context.Context, provisionerName, ref
dbeak := &dbExternalAccountKey{ dbeak := &dbExternalAccountKey{
ID: keyID, ID: keyID,
Provisioner: provisionerName, ProvisionerID: provisionerID,
Reference: reference, Reference: reference,
KeyBytes: random, KeyBytes: random,
CreatedAt: clock.Now(), CreatedAt: clock.Now(),
@ -194,19 +198,23 @@ func (db *DB) CreateExternalAccountKey(ctx context.Context, provisionerName, ref
return nil, err return nil, err
} }
if err := db.addEAKID(ctx, provisionerID, dbeak.ID); err != nil {
return nil, err
}
if dbeak.Reference != "" { if dbeak.Reference != "" {
dbExternalAccountKeyReference := &dbExternalAccountKeyReference{ dbExternalAccountKeyReference := &dbExternalAccountKeyReference{
Reference: dbeak.Reference, Reference: dbeak.Reference,
ExternalAccountKeyID: dbeak.ID, ExternalAccountKeyID: dbeak.ID,
} }
if err := db.save(ctx, dbeak.Reference, dbExternalAccountKeyReference, nil, "external_account_key_reference", externalAccountKeysByReferenceTable); err != nil { if err := db.save(ctx, referenceKey(provisionerID, dbeak.Reference), dbExternalAccountKeyReference, nil, "external_account_key_reference", externalAccountKeysByReferenceTable); err != nil {
return nil, err return nil, err
} }
} }
return &acme.ExternalAccountKey{ return &acme.ExternalAccountKey{
ID: dbeak.ID, ID: dbeak.ID,
Provisioner: dbeak.Provisioner, ProvisionerID: dbeak.ProvisionerID,
Reference: dbeak.Reference, Reference: dbeak.Reference,
AccountID: dbeak.AccountID, AccountID: dbeak.AccountID,
KeyBytes: dbeak.KeyBytes, KeyBytes: dbeak.KeyBytes,
@ -216,19 +224,19 @@ func (db *DB) CreateExternalAccountKey(ctx context.Context, provisionerName, ref
} }
// GetExternalAccountKey retrieves an External Account Binding key by KeyID // GetExternalAccountKey retrieves an External Account Binding key by KeyID
func (db *DB) GetExternalAccountKey(ctx context.Context, provisionerName, keyID string) (*acme.ExternalAccountKey, error) { func (db *DB) GetExternalAccountKey(ctx context.Context, provisionerID, keyID string) (*acme.ExternalAccountKey, error) {
dbeak, err := db.getDBExternalAccountKey(ctx, keyID) dbeak, err := db.getDBExternalAccountKey(ctx, keyID)
if err != nil { if err != nil {
return nil, err return nil, err
} }
if dbeak.Provisioner != provisionerName { if dbeak.ProvisionerID != provisionerID {
return nil, acme.NewError(acme.ErrorUnauthorizedType, "name of provisioner does not match provisioner for which the EAB key was created") return nil, acme.NewError(acme.ErrorUnauthorizedType, "provisioner does not match provisioner for which the EAB key was created")
} }
return &acme.ExternalAccountKey{ return &acme.ExternalAccountKey{
ID: dbeak.ID, ID: dbeak.ID,
Provisioner: dbeak.Provisioner, ProvisionerID: dbeak.ProvisionerID,
Reference: dbeak.Reference, Reference: dbeak.Reference,
AccountID: dbeak.AccountID, AccountID: dbeak.AccountID,
KeyBytes: dbeak.KeyBytes, KeyBytes: dbeak.KeyBytes,
@ -237,52 +245,67 @@ func (db *DB) GetExternalAccountKey(ctx context.Context, provisionerName, keyID
}, nil }, nil
} }
func (db *DB) DeleteExternalAccountKey(ctx context.Context, provisionerName, keyID string) error { func (db *DB) DeleteExternalAccountKey(ctx context.Context, provisionerID, keyID string) error {
dbeak, err := db.getDBExternalAccountKey(ctx, keyID) dbeak, err := db.getDBExternalAccountKey(ctx, keyID)
if err != nil { if err != nil {
return errors.Wrapf(err, "error loading ACME EAB Key with Key ID %s", keyID) return errors.Wrapf(err, "error loading ACME EAB Key with Key ID %s", keyID)
} }
if dbeak.Provisioner != provisionerName {
return errors.New("name of provisioner does not match provisioner for which the EAB key was created") if dbeak.ProvisionerID != provisionerID {
return errors.New("provisioner does not match provisioner for which the EAB key was created")
} }
if dbeak.Reference != "" { if dbeak.Reference != "" {
err = db.db.Del(externalAccountKeysByReferenceTable, []byte(dbeak.Reference)) if err := db.db.Del(externalAccountKeysByReferenceTable, []byte(referenceKey(provisionerID, dbeak.Reference))); err != nil {
if err != nil { return errors.Wrapf(err, "error deleting ACME EAB Key reference with Key ID %s and reference %s", keyID, dbeak.Reference)
return errors.Wrapf(err, "error deleting ACME EAB Key Reference with Key ID %s and reference %s", keyID, dbeak.Reference)
} }
} }
if err = db.db.Del(externalAccountKeyTable, []byte(keyID)); err != nil { if err := db.db.Del(externalAccountKeyTable, []byte(keyID)); err != nil {
return errors.Wrapf(err, "error deleting ACME EAB Key with Key ID %s", keyID) return errors.Wrapf(err, "error deleting ACME EAB Key with Key ID %s", keyID)
} }
if err := db.deleteEAKID(ctx, provisionerID, keyID); err != nil {
return errors.Wrapf(err, "error removing ACME EAB Key ID %s", keyID)
}
return nil return nil
} }
// GetExternalAccountKeys retrieves all External Account Binding keys for a provisioner // GetExternalAccountKeys retrieves all External Account Binding keys for a provisioner
func (db *DB) GetExternalAccountKeys(ctx context.Context, provisionerName string) ([]*acme.ExternalAccountKey, error) { func (db *DB) GetExternalAccountKeys(ctx context.Context, provisionerID string) ([]*acme.ExternalAccountKey, error) {
// TODO: lookup by provisioner based on index // TODO: mutex?
entries, err := db.db.List(externalAccountKeyTable)
var eakIDs []string
r, err := db.db.Get(externalAccountKeysByProvisionerIDTable, []byte(provisionerID))
if err != nil { if err != nil {
return nil, err if !nosqlDB.IsErrNotFound(err) {
return nil, errors.Wrapf(err, "error loading ACME EAB Key IDs for provisioner %s", provisionerID)
}
} else {
if err := json.Unmarshal(r, &eakIDs); err != nil {
return nil, errors.Wrapf(err, "error unmarshaling ACME EAB Key IDs for provisioner %s", provisionerID)
}
} }
keys := []*acme.ExternalAccountKey{} keys := []*acme.ExternalAccountKey{}
for _, entry := range entries { // entries is sorted alphabetically on the key (ID) of the EAK; no need to sort this again. for _, eakID := range eakIDs {
dbeak := new(dbExternalAccountKey) if eakID == "" {
if err = json.Unmarshal(entry.Value, dbeak); err != nil { continue // shouldn't happen; just in case
return nil, errors.Wrapf(err, "error unmarshaling external account key %s into ExternalAccountKey", string(entry.Key)) }
eak, err := db.getDBExternalAccountKey(ctx, eakID)
if err != nil {
if !nosqlDB.IsErrNotFound(err) {
return nil, errors.Wrapf(err, "error retrieving ACME EAB Key for provisioner %s and keyID %s", provisionerID, eakID)
} }
if dbeak.Provisioner != provisionerName {
continue
} }
keys = append(keys, &acme.ExternalAccountKey{ keys = append(keys, &acme.ExternalAccountKey{
ID: dbeak.ID, ID: eak.ID,
KeyBytes: dbeak.KeyBytes, KeyBytes: eak.KeyBytes,
Provisioner: dbeak.Provisioner, ProvisionerID: eak.ProvisionerID,
Reference: dbeak.Reference, Reference: eak.Reference,
AccountID: dbeak.AccountID, AccountID: eak.AccountID,
CreatedAt: dbeak.CreatedAt, CreatedAt: eak.CreatedAt,
BoundAt: dbeak.BoundAt, BoundAt: eak.BoundAt,
}) })
} }
@ -290,11 +313,12 @@ func (db *DB) GetExternalAccountKeys(ctx context.Context, provisionerName string
} }
// GetExternalAccountKeyByReference retrieves an External Account Binding key with unique reference // GetExternalAccountKeyByReference retrieves an External Account Binding key with unique reference
func (db *DB) GetExternalAccountKeyByReference(ctx context.Context, provisionerName, reference string) (*acme.ExternalAccountKey, error) { func (db *DB) GetExternalAccountKeyByReference(ctx context.Context, provisionerID, reference string) (*acme.ExternalAccountKey, error) {
if reference == "" { if reference == "" {
return nil, nil return nil, nil
} }
k, err := db.db.Get(externalAccountKeysByReferenceTable, []byte(reference))
k, err := db.db.Get(externalAccountKeysByReferenceTable, []byte(referenceKey(provisionerID, reference)))
if nosqlDB.IsErrNotFound(err) { if nosqlDB.IsErrNotFound(err) {
return nil, acme.ErrNotFound return nil, acme.ErrNotFound
} else if err != nil { } else if err != nil {
@ -304,22 +328,31 @@ func (db *DB) GetExternalAccountKeyByReference(ctx context.Context, provisionerN
if err := json.Unmarshal(k, dbExternalAccountKeyReference); err != nil { if err := json.Unmarshal(k, dbExternalAccountKeyReference); err != nil {
return nil, errors.Wrapf(err, "error unmarshaling ACME EAB key for reference %s", reference) return nil, errors.Wrapf(err, "error unmarshaling ACME EAB key for reference %s", reference)
} }
return db.GetExternalAccountKey(ctx, provisionerName, dbExternalAccountKeyReference.ExternalAccountKeyID)
return db.GetExternalAccountKey(ctx, provisionerID, dbExternalAccountKeyReference.ExternalAccountKeyID)
} }
func (db *DB) UpdateExternalAccountKey(ctx context.Context, provisionerName string, eak *acme.ExternalAccountKey) error { func (db *DB) UpdateExternalAccountKey(ctx context.Context, provisionerID string, eak *acme.ExternalAccountKey) error {
old, err := db.getDBExternalAccountKey(ctx, eak.ID) old, err := db.getDBExternalAccountKey(ctx, eak.ID)
if err != nil { if err != nil {
return err return err
} }
if old.Provisioner != provisionerName { if old.ProvisionerID != provisionerID {
return errors.New("name of provisioner does not match provisioner for which the EAB key was created") return errors.New("provisioner does not match provisioner for which the EAB key was created")
}
if old.ProvisionerID != eak.ProvisionerID {
return errors.New("cannot change provisioner for an existing ACME EAB Key")
}
if old.Reference != eak.Reference {
return errors.New("cannot change reference for an existing ACME EAB Key")
} }
nu := dbExternalAccountKey{ nu := dbExternalAccountKey{
ID: eak.ID, ID: eak.ID,
Provisioner: eak.Provisioner, ProvisionerID: eak.ProvisionerID,
Reference: eak.Reference, Reference: eak.Reference,
AccountID: eak.AccountID, AccountID: eak.AccountID,
KeyBytes: eak.KeyBytes, KeyBytes: eak.KeyBytes,
@ -329,3 +362,105 @@ func (db *DB) UpdateExternalAccountKey(ctx context.Context, provisionerName stri
return db.save(ctx, nu.ID, nu, old, "external_account_key", externalAccountKeyTable) return db.save(ctx, nu.ID, nu, old, "external_account_key", externalAccountKeyTable)
} }
func (db *DB) addEAKID(ctx context.Context, provisionerID, eakID string) error {
referencesByProvisionerIndexMux.Lock()
defer referencesByProvisionerIndexMux.Unlock()
var eakIDs []string
b, err := db.db.Get(externalAccountKeysByProvisionerIDTable, []byte(provisionerID))
if err != nil {
if !nosqlDB.IsErrNotFound(err) {
return errors.Wrapf(err, "error loading eakIDs for provisioner %s", provisionerID)
}
} else {
if err := json.Unmarshal(b, &eakIDs); err != nil {
return errors.Wrapf(err, "error unmarshaling eakIDs for provisioner %s", provisionerID)
}
}
var newEAKIDs []string
newEAKIDs = append(newEAKIDs, eakIDs...)
newEAKIDs = append(newEAKIDs, eakID)
var (
_old interface{} = eakIDs
_new interface{} = newEAKIDs
)
if len(eakIDs) == 0 {
_old = nil
}
if err = db.save(ctx, provisionerID, _new, _old, "externalAccountKeysByProvisionerID", externalAccountKeysByProvisionerIDTable); err != nil {
return errors.Wrapf(err, "error saving eakIDs index for provisioner %s", provisionerID)
}
return nil
}
func (db *DB) deleteEAKID(ctx context.Context, provisionerID, eakID string) error {
referencesByProvisionerIndexMux.Lock()
defer referencesByProvisionerIndexMux.Unlock()
var eakIDs []string
b, err := db.db.Get(externalAccountKeysByProvisionerIDTable, []byte(provisionerID))
if err != nil {
if !nosqlDB.IsErrNotFound(err) {
return errors.Wrapf(err, "error loading reference IDs for provisioner %s", provisionerID)
}
} else {
if err := json.Unmarshal(b, &eakIDs); err != nil {
return errors.Wrapf(err, "error unmarshaling eakIDs for provisioner %s", provisionerID)
}
}
newEAKIDs := removeElement(eakIDs, eakID)
var (
_old interface{} = eakIDs
_new interface{} = newEAKIDs
)
switch {
case len(eakIDs) == 0:
_old = nil
case len(newEAKIDs) == 0:
_new = nil
}
if err = db.save(ctx, provisionerID, _new, _old, "externalAccountKeysByProvisionerID", externalAccountKeysByProvisionerIDTable); err != nil {
return errors.Wrapf(err, "error saving referenceIDs index for provisioner %s", provisionerID)
}
return nil
}
// referenceKey returns a unique key for a reference per provisioner
func referenceKey(provisionerID, reference string) string {
return provisionerID + "." + reference
}
// sliceIndex finds the index of item in slice
func sliceIndex(slice []string, item string) int {
for i := range slice {
if slice[i] == item {
return i
}
}
return -1
}
// removeElement deletes the item if it exists in the
// slice. It returns a new slice, keeping the old one intact.
func removeElement(slice []string, item string) []string {
newSlice := make([]string, 0)
index := sliceIndex(slice, item)
if index < 0 {
newSlice = append(newSlice, slice...)
return newSlice
}
newSlice = append(newSlice, slice[:index]...)
return append(newSlice, slice[index+1:]...)
}

View file

@ -3,6 +3,7 @@ package nosql
import ( import (
"context" "context"
"encoding/json" "encoding/json"
"fmt"
"testing" "testing"
"time" "time"
@ -307,7 +308,7 @@ func TestDB_GetAccountByKeyID(t *testing.T) {
assert.Equals(t, string(key), accID) assert.Equals(t, string(key), accID)
return nil, errors.New("force") return nil, errors.New("force")
default: default:
assert.FatalError(t, errors.Errorf("unrecognized bucket %s", string(bucket))) assert.FatalError(t, errors.Errorf("unexpected bucket %s", string(bucket)))
return nil, errors.New("force") return nil, errors.New("force")
} }
}, },
@ -340,7 +341,7 @@ func TestDB_GetAccountByKeyID(t *testing.T) {
assert.Equals(t, string(key), accID) assert.Equals(t, string(key), accID)
return b, nil return b, nil
default: default:
assert.FatalError(t, errors.Errorf("unrecognized bucket %s", string(bucket))) assert.FatalError(t, errors.Errorf("unexpected bucket %s", string(bucket)))
return nil, errors.New("force") return nil, errors.New("force")
} }
}, },
@ -462,7 +463,7 @@ func TestDB_CreateAccount(t *testing.T) {
assert.True(t, dbacc.DeactivatedAt.IsZero()) assert.True(t, dbacc.DeactivatedAt.IsZero())
return nil, false, errors.New("force") return nil, false, errors.New("force")
default: default:
assert.FatalError(t, errors.Errorf("unrecognized bucket %s", string(bucket))) assert.FatalError(t, errors.Errorf("unexpected bucket %s", string(bucket)))
return nil, false, errors.New("force") return nil, false, errors.New("force")
} }
}, },
@ -506,7 +507,7 @@ func TestDB_CreateAccount(t *testing.T) {
assert.True(t, dbacc.DeactivatedAt.IsZero()) assert.True(t, dbacc.DeactivatedAt.IsZero())
return nu, true, nil return nu, true, nil
default: default:
assert.FatalError(t, errors.Errorf("unrecognized bucket %s", string(bucket))) assert.FatalError(t, errors.Errorf("unexpected bucket %s", string(bucket)))
return nil, false, errors.New("force") return nil, false, errors.New("force")
} }
}, },
@ -699,6 +700,7 @@ func TestDB_UpdateAccount(t *testing.T) {
func TestDB_getDBExternalAccountKey(t *testing.T) { func TestDB_getDBExternalAccountKey(t *testing.T) {
keyID := "keyID" keyID := "keyID"
provID := "provID"
type test struct { type test struct {
db nosql.DB db nosql.DB
err error err error
@ -710,7 +712,7 @@ func TestDB_getDBExternalAccountKey(t *testing.T) {
now := clock.Now() now := clock.Now()
dbeak := &dbExternalAccountKey{ dbeak := &dbExternalAccountKey{
ID: keyID, ID: keyID,
Provisioner: "prov", ProvisionerID: provID,
Reference: "ref", Reference: "ref",
AccountID: "", AccountID: "",
KeyBytes: []byte{1, 3, 3, 7}, KeyBytes: []byte{1, 3, 3, 7},
@ -790,7 +792,7 @@ func TestDB_getDBExternalAccountKey(t *testing.T) {
} else if assert.Nil(t, tc.err) { } else if assert.Nil(t, tc.err) {
assert.Equals(t, dbeak.ID, tc.dbeak.ID) assert.Equals(t, dbeak.ID, tc.dbeak.ID)
assert.Equals(t, dbeak.KeyBytes, tc.dbeak.KeyBytes) assert.Equals(t, dbeak.KeyBytes, tc.dbeak.KeyBytes)
assert.Equals(t, dbeak.Provisioner, tc.dbeak.Provisioner) assert.Equals(t, dbeak.ProvisionerID, tc.dbeak.ProvisionerID)
assert.Equals(t, dbeak.Reference, tc.dbeak.Reference) assert.Equals(t, dbeak.Reference, tc.dbeak.Reference)
assert.Equals(t, dbeak.CreatedAt, tc.dbeak.CreatedAt) assert.Equals(t, dbeak.CreatedAt, tc.dbeak.CreatedAt)
assert.Equals(t, dbeak.AccountID, tc.dbeak.AccountID) assert.Equals(t, dbeak.AccountID, tc.dbeak.AccountID)
@ -802,7 +804,7 @@ func TestDB_getDBExternalAccountKey(t *testing.T) {
func TestDB_GetExternalAccountKey(t *testing.T) { func TestDB_GetExternalAccountKey(t *testing.T) {
keyID := "keyID" keyID := "keyID"
prov := "acmeProv" provID := "provID"
type test struct { type test struct {
db nosql.DB db nosql.DB
err error err error
@ -814,7 +816,7 @@ func TestDB_GetExternalAccountKey(t *testing.T) {
now := clock.Now() now := clock.Now()
dbeak := &dbExternalAccountKey{ dbeak := &dbExternalAccountKey{
ID: keyID, ID: keyID,
Provisioner: prov, ProvisionerID: provID,
Reference: "ref", Reference: "ref",
AccountID: "", AccountID: "",
KeyBytes: []byte{1, 3, 3, 7}, KeyBytes: []byte{1, 3, 3, 7},
@ -832,7 +834,7 @@ func TestDB_GetExternalAccountKey(t *testing.T) {
}, },
eak: &acme.ExternalAccountKey{ eak: &acme.ExternalAccountKey{
ID: keyID, ID: keyID,
Provisioner: prov, ProvisionerID: provID,
Reference: "ref", Reference: "ref",
AccountID: "", AccountID: "",
KeyBytes: []byte{1, 3, 3, 7}, KeyBytes: []byte{1, 3, 3, 7},
@ -857,7 +859,7 @@ func TestDB_GetExternalAccountKey(t *testing.T) {
now := clock.Now() now := clock.Now()
dbeak := &dbExternalAccountKey{ dbeak := &dbExternalAccountKey{
ID: keyID, ID: keyID,
Provisioner: "aDifferentProv", ProvisionerID: "aDifferentProvID",
Reference: "ref", Reference: "ref",
AccountID: "", AccountID: "",
KeyBytes: []byte{1, 3, 3, 7}, KeyBytes: []byte{1, 3, 3, 7},
@ -875,13 +877,13 @@ func TestDB_GetExternalAccountKey(t *testing.T) {
}, },
eak: &acme.ExternalAccountKey{ eak: &acme.ExternalAccountKey{
ID: keyID, ID: keyID,
Provisioner: prov, ProvisionerID: provID,
Reference: "ref", Reference: "ref",
AccountID: "", AccountID: "",
KeyBytes: []byte{1, 3, 3, 7}, KeyBytes: []byte{1, 3, 3, 7},
CreatedAt: now, CreatedAt: now,
}, },
acmeErr: acme.NewError(acme.ErrorUnauthorizedType, "name of provisioner does not match provisioner for which the EAB key was created"), acmeErr: acme.NewError(acme.ErrorUnauthorizedType, "provisioner does not match provisioner for which the EAB key was created"),
} }
}, },
} }
@ -889,7 +891,7 @@ func TestDB_GetExternalAccountKey(t *testing.T) {
tc := run(t) tc := run(t)
t.Run(name, func(t *testing.T) { t.Run(name, func(t *testing.T) {
d := DB{db: tc.db} d := DB{db: tc.db}
if eak, err := d.GetExternalAccountKey(context.Background(), prov, keyID); err != nil { if eak, err := d.GetExternalAccountKey(context.Background(), provID, keyID); err != nil {
switch k := err.(type) { switch k := err.(type) {
case *acme.Error: case *acme.Error:
if assert.NotNil(t, tc.acmeErr) { if assert.NotNil(t, tc.acmeErr) {
@ -907,7 +909,7 @@ func TestDB_GetExternalAccountKey(t *testing.T) {
} else if assert.Nil(t, tc.err) { } else if assert.Nil(t, tc.err) {
assert.Equals(t, eak.ID, tc.eak.ID) assert.Equals(t, eak.ID, tc.eak.ID)
assert.Equals(t, eak.KeyBytes, tc.eak.KeyBytes) assert.Equals(t, eak.KeyBytes, tc.eak.KeyBytes)
assert.Equals(t, eak.Provisioner, tc.eak.Provisioner) assert.Equals(t, eak.ProvisionerID, tc.eak.ProvisionerID)
assert.Equals(t, eak.Reference, tc.eak.Reference) assert.Equals(t, eak.Reference, tc.eak.Reference)
assert.Equals(t, eak.CreatedAt, tc.eak.CreatedAt) assert.Equals(t, eak.CreatedAt, tc.eak.CreatedAt)
assert.Equals(t, eak.AccountID, tc.eak.AccountID) assert.Equals(t, eak.AccountID, tc.eak.AccountID)
@ -919,7 +921,7 @@ func TestDB_GetExternalAccountKey(t *testing.T) {
func TestDB_GetExternalAccountKeyByReference(t *testing.T) { func TestDB_GetExternalAccountKeyByReference(t *testing.T) {
keyID := "keyID" keyID := "keyID"
prov := "acmeProv" provID := "provID"
ref := "ref" ref := "ref"
type test struct { type test struct {
db nosql.DB db nosql.DB
@ -933,7 +935,7 @@ func TestDB_GetExternalAccountKeyByReference(t *testing.T) {
now := clock.Now() now := clock.Now()
dbeak := &dbExternalAccountKey{ dbeak := &dbExternalAccountKey{
ID: keyID, ID: keyID,
Provisioner: prov, ProvisionerID: provID,
Reference: ref, Reference: ref,
AccountID: "", AccountID: "",
KeyBytes: []byte{1, 3, 3, 7}, KeyBytes: []byte{1, 3, 3, 7},
@ -953,20 +955,20 @@ func TestDB_GetExternalAccountKeyByReference(t *testing.T) {
MGet: func(bucket, key []byte) ([]byte, error) { MGet: func(bucket, key []byte) ([]byte, error) {
switch string(bucket) { switch string(bucket) {
case string(externalAccountKeysByReferenceTable): case string(externalAccountKeysByReferenceTable):
assert.Equals(t, string(key), ref) assert.Equals(t, string(key), provID+"."+ref)
return dbrefBytes, nil return dbrefBytes, nil
case string(externalAccountKeyTable): case string(externalAccountKeyTable):
assert.Equals(t, string(key), keyID) assert.Equals(t, string(key), keyID)
return b, nil return b, nil
default: default:
assert.FatalError(t, errors.Errorf("unrecognized bucket %s", string(bucket))) assert.FatalError(t, errors.Errorf("unexpected bucket %s", string(bucket)))
return nil, errors.New("force") return nil, errors.New("force")
} }
}, },
}, },
eak: &acme.ExternalAccountKey{ eak: &acme.ExternalAccountKey{
ID: keyID, ID: keyID,
Provisioner: prov, ProvisionerID: provID,
Reference: ref, Reference: ref,
AccountID: "", AccountID: "",
KeyBytes: []byte{1, 3, 3, 7}, KeyBytes: []byte{1, 3, 3, 7},
@ -988,7 +990,7 @@ func TestDB_GetExternalAccountKeyByReference(t *testing.T) {
db: &db.MockNoSQLDB{ db: &db.MockNoSQLDB{
MGet: func(bucket, key []byte) ([]byte, error) { MGet: func(bucket, key []byte) ([]byte, error) {
assert.Equals(t, string(bucket), string(externalAccountKeysByReferenceTable)) assert.Equals(t, string(bucket), string(externalAccountKeysByReferenceTable))
assert.Equals(t, string(key), ref) assert.Equals(t, string(key), provID+"."+ref)
return nil, nosqldb.ErrNotFound return nil, nosqldb.ErrNotFound
}, },
}, },
@ -1001,7 +1003,7 @@ func TestDB_GetExternalAccountKeyByReference(t *testing.T) {
db: &db.MockNoSQLDB{ db: &db.MockNoSQLDB{
MGet: func(bucket, key []byte) ([]byte, error) { MGet: func(bucket, key []byte) ([]byte, error) {
assert.Equals(t, string(bucket), string(externalAccountKeysByReferenceTable)) assert.Equals(t, string(bucket), string(externalAccountKeysByReferenceTable))
assert.Equals(t, string(key), ref) assert.Equals(t, string(key), provID+"."+ref)
return nil, errors.New("force") return nil, errors.New("force")
}, },
}, },
@ -1014,7 +1016,7 @@ func TestDB_GetExternalAccountKeyByReference(t *testing.T) {
db: &db.MockNoSQLDB{ db: &db.MockNoSQLDB{
MGet: func(bucket, key []byte) ([]byte, error) { MGet: func(bucket, key []byte) ([]byte, error) {
assert.Equals(t, string(bucket), string(externalAccountKeysByReferenceTable)) assert.Equals(t, string(bucket), string(externalAccountKeysByReferenceTable))
assert.Equals(t, string(key), ref) assert.Equals(t, string(key), provID+"."+ref)
return []byte{0}, nil return []byte{0}, nil
}, },
}, },
@ -1034,13 +1036,13 @@ func TestDB_GetExternalAccountKeyByReference(t *testing.T) {
MGet: func(bucket, key []byte) ([]byte, error) { MGet: func(bucket, key []byte) ([]byte, error) {
switch string(bucket) { switch string(bucket) {
case string(externalAccountKeysByReferenceTable): case string(externalAccountKeysByReferenceTable):
assert.Equals(t, string(key), ref) assert.Equals(t, string(key), provID+"."+ref)
return dbrefBytes, nil return dbrefBytes, nil
case string(externalAccountKeyTable): case string(externalAccountKeyTable):
assert.Equals(t, string(key), keyID) assert.Equals(t, string(key), keyID)
return nil, errors.New("force") return nil, errors.New("force")
default: default:
assert.FatalError(t, errors.Errorf("unrecognized bucket %s", string(bucket))) assert.FatalError(t, errors.Errorf("unexpected bucket %s", string(bucket)))
return nil, errors.New("force") return nil, errors.New("force")
} }
}, },
@ -1053,7 +1055,7 @@ func TestDB_GetExternalAccountKeyByReference(t *testing.T) {
tc := run(t) tc := run(t)
t.Run(name, func(t *testing.T) { t.Run(name, func(t *testing.T) {
d := DB{db: tc.db} d := DB{db: tc.db}
if eak, err := d.GetExternalAccountKeyByReference(context.Background(), prov, tc.ref); err != nil { if eak, err := d.GetExternalAccountKeyByReference(context.Background(), provID, tc.ref); err != nil {
switch k := err.(type) { switch k := err.(type) {
case *acme.Error: case *acme.Error:
if assert.NotNil(t, tc.acmeErr) { if assert.NotNil(t, tc.acmeErr) {
@ -1074,7 +1076,7 @@ func TestDB_GetExternalAccountKeyByReference(t *testing.T) {
assert.Equals(t, eak.BoundAt, tc.eak.BoundAt) assert.Equals(t, eak.BoundAt, tc.eak.BoundAt)
assert.Equals(t, eak.CreatedAt, tc.eak.CreatedAt) assert.Equals(t, eak.CreatedAt, tc.eak.CreatedAt)
assert.Equals(t, eak.KeyBytes, tc.eak.KeyBytes) assert.Equals(t, eak.KeyBytes, tc.eak.KeyBytes)
assert.Equals(t, eak.Provisioner, tc.eak.Provisioner) assert.Equals(t, eak.ProvisionerID, tc.eak.ProvisionerID)
assert.Equals(t, eak.Reference, tc.eak.Reference) assert.Equals(t, eak.Reference, tc.eak.Reference)
} }
}) })
@ -1085,7 +1087,7 @@ func TestDB_GetExternalAccountKeys(t *testing.T) {
keyID1 := "keyID1" keyID1 := "keyID1"
keyID2 := "keyID2" keyID2 := "keyID2"
keyID3 := "keyID3" keyID3 := "keyID3"
prov := "acmeProv" provID := "provID"
ref := "ref" ref := "ref"
type test struct { type test struct {
db nosql.DB db nosql.DB
@ -1098,7 +1100,7 @@ func TestDB_GetExternalAccountKeys(t *testing.T) {
now := clock.Now() now := clock.Now()
dbeak1 := &dbExternalAccountKey{ dbeak1 := &dbExternalAccountKey{
ID: keyID1, ID: keyID1,
Provisioner: prov, ProvisionerID: provID,
Reference: ref, Reference: ref,
AccountID: "", AccountID: "",
KeyBytes: []byte{1, 3, 3, 7}, KeyBytes: []byte{1, 3, 3, 7},
@ -1108,7 +1110,7 @@ func TestDB_GetExternalAccountKeys(t *testing.T) {
assert.FatalError(t, err) assert.FatalError(t, err)
dbeak2 := &dbExternalAccountKey{ dbeak2 := &dbExternalAccountKey{
ID: keyID2, ID: keyID2,
Provisioner: prov, ProvisionerID: provID,
Reference: ref, Reference: ref,
AccountID: "", AccountID: "",
KeyBytes: []byte{1, 3, 3, 7}, KeyBytes: []byte{1, 3, 3, 7},
@ -1118,7 +1120,7 @@ func TestDB_GetExternalAccountKeys(t *testing.T) {
assert.FatalError(t, err) assert.FatalError(t, err)
dbeak3 := &dbExternalAccountKey{ dbeak3 := &dbExternalAccountKey{
ID: keyID3, ID: keyID3,
Provisioner: "differentProvisioner", ProvisionerID: "aDifferentProvID",
Reference: ref, Reference: ref,
AccountID: "", AccountID: "",
KeyBytes: []byte{1, 3, 3, 7}, KeyBytes: []byte{1, 3, 3, 7},
@ -1128,8 +1130,32 @@ func TestDB_GetExternalAccountKeys(t *testing.T) {
assert.FatalError(t, err) assert.FatalError(t, err)
return test{ return test{
db: &db.MockNoSQLDB{ db: &db.MockNoSQLDB{
MGet: func(bucket, key []byte) ([]byte, error) {
switch string(bucket) {
case string(externalAccountKeysByProvisionerIDTable):
keys := []string{keyID1, keyID2}
b, err := json.Marshal(keys)
assert.FatalError(t, err)
return b, nil
case string(externalAccountKeyTable):
switch string(key) {
case keyID1:
return b1, nil
case keyID2:
return b2, nil
default:
assert.FatalError(t, errors.Errorf("unexpected key %s", string(key)))
return nil, errors.New("force")
}
default:
assert.FatalError(t, errors.Errorf("unexpected bucket %s", string(bucket)))
return nil, errors.New("force")
}
},
// TODO: remove the MList
MList: func(bucket []byte) ([]*nosqldb.Entry, error) { MList: func(bucket []byte) ([]*nosqldb.Entry, error) {
assert.Equals(t, bucket, externalAccountKeyTable) switch string(bucket) {
case string(externalAccountKeyTable):
return []*nosqldb.Entry{ return []*nosqldb.Entry{
{ {
Bucket: bucket, Bucket: bucket,
@ -1147,12 +1173,27 @@ func TestDB_GetExternalAccountKeys(t *testing.T) {
Value: b3, Value: b3,
}, },
}, nil }, nil
case string(externalAccountKeysByProvisionerIDTable):
keys := []string{keyID1, keyID2}
b, err := json.Marshal(keys)
assert.FatalError(t, err)
return []*nosqldb.Entry{
{
Bucket: bucket,
Key: []byte(provID),
Value: b,
},
}, nil
default:
assert.FatalError(t, errors.Errorf("unexpected bucket %s", string(bucket)))
return nil, errors.New("force default")
}
}, },
}, },
eaks: []*acme.ExternalAccountKey{ eaks: []*acme.ExternalAccountKey{
{ {
ID: keyID1, ID: keyID1,
Provisioner: prov, ProvisionerID: provID,
Reference: ref, Reference: ref,
AccountID: "", AccountID: "",
KeyBytes: []byte{1, 3, 3, 7}, KeyBytes: []byte{1, 3, 3, 7},
@ -1160,7 +1201,7 @@ func TestDB_GetExternalAccountKeys(t *testing.T) {
}, },
{ {
ID: keyID2, ID: keyID2,
Provisioner: prov, ProvisionerID: provID,
Reference: ref, Reference: ref,
AccountID: "", AccountID: "",
KeyBytes: []byte{1, 3, 3, 7}, KeyBytes: []byte{1, 3, 3, 7},
@ -1169,33 +1210,36 @@ func TestDB_GetExternalAccountKeys(t *testing.T) {
}, },
} }
}, },
"fail/db.List-error": func(t *testing.T) test { "fail/db.Get-externalAccountKeysByProvisionerIDTable": func(t *testing.T) test {
return test{ return test{
db: &db.MockNoSQLDB{ db: &db.MockNoSQLDB{
MList: func(bucket []byte) ([]*nosqldb.Entry, error) { MGet: func(bucket, key []byte) ([]byte, error) {
assert.Equals(t, string(bucket), string(externalAccountKeyTable)) assert.Equals(t, string(bucket), string(externalAccountKeysByProvisionerIDTable))
return nil, errors.New("force") return nil, errors.New("force")
}, },
}, },
err: errors.New("force"), err: errors.New("error loading ACME EAB Key IDs for provisioner provID: force"),
} }
}, },
"fail/unmarshal-error": func(t *testing.T) test { "fail/db.getDBExternalAccountKey": func(t *testing.T) test {
return test{ return test{
db: &db.MockNoSQLDB{ db: &db.MockNoSQLDB{
MList: func(bucket []byte) ([]*nosqldb.Entry, error) { MGet: func(bucket, key []byte) ([]byte, error) {
assert.Equals(t, bucket, externalAccountKeyTable) switch string(bucket) {
return []*nosqldb.Entry{ case string(externalAccountKeysByProvisionerIDTable):
{ keys := []string{keyID1, keyID2}
Bucket: bucket, b, err := json.Marshal(keys)
Key: []byte(keyID1), assert.FatalError(t, err)
Value: []byte("foo"), return b, nil
}, case string(externalAccountKeyTable):
}, nil return nil, errors.New("force")
default:
assert.FatalError(t, errors.Errorf("unexpected bucket %s", string(bucket)))
return nil, errors.New("force bucket")
}
}, },
}, },
eaks: []*acme.ExternalAccountKey{}, err: errors.New("error retrieving ACME EAB Key for provisioner provID and keyID keyID1: error loading external account key keyID1: force"),
err: errors.Errorf("error unmarshaling external account key %s into ExternalAccountKey", keyID1),
} }
}, },
} }
@ -1203,7 +1247,7 @@ func TestDB_GetExternalAccountKeys(t *testing.T) {
tc := run(t) tc := run(t)
t.Run(name, func(t *testing.T) { t.Run(name, func(t *testing.T) {
d := DB{db: tc.db} d := DB{db: tc.db}
if eaks, err := d.GetExternalAccountKeys(context.Background(), prov); err != nil { if eaks, err := d.GetExternalAccountKeys(context.Background(), provID); err != nil {
switch k := err.(type) { switch k := err.(type) {
case *acme.Error: case *acme.Error:
if assert.NotNil(t, tc.acmeErr) { if assert.NotNil(t, tc.acmeErr) {
@ -1215,7 +1259,7 @@ func TestDB_GetExternalAccountKeys(t *testing.T) {
} }
default: default:
if assert.NotNil(t, tc.err) { if assert.NotNil(t, tc.err) {
assert.HasPrefix(t, err.Error(), tc.err.Error()) assert.Equals(t, tc.err.Error(), err.Error())
} }
} }
} else if assert.Nil(t, tc.err) { } else if assert.Nil(t, tc.err) {
@ -1223,7 +1267,7 @@ func TestDB_GetExternalAccountKeys(t *testing.T) {
for i, eak := range eaks { for i, eak := range eaks {
assert.Equals(t, eak.ID, tc.eaks[i].ID) assert.Equals(t, eak.ID, tc.eaks[i].ID)
assert.Equals(t, eak.KeyBytes, tc.eaks[i].KeyBytes) assert.Equals(t, eak.KeyBytes, tc.eaks[i].KeyBytes)
assert.Equals(t, eak.Provisioner, tc.eaks[i].Provisioner) assert.Equals(t, eak.ProvisionerID, tc.eaks[i].ProvisionerID)
assert.Equals(t, eak.Reference, tc.eaks[i].Reference) assert.Equals(t, eak.Reference, tc.eaks[i].Reference)
assert.Equals(t, eak.CreatedAt, tc.eaks[i].CreatedAt) assert.Equals(t, eak.CreatedAt, tc.eaks[i].CreatedAt)
assert.Equals(t, eak.AccountID, tc.eaks[i].AccountID) assert.Equals(t, eak.AccountID, tc.eaks[i].AccountID)
@ -1236,7 +1280,7 @@ func TestDB_GetExternalAccountKeys(t *testing.T) {
func TestDB_DeleteExternalAccountKey(t *testing.T) { func TestDB_DeleteExternalAccountKey(t *testing.T) {
keyID := "keyID" keyID := "keyID"
prov := "acmeProv" provID := "provID"
ref := "ref" ref := "ref"
type test struct { type test struct {
db nosql.DB db nosql.DB
@ -1248,7 +1292,7 @@ func TestDB_DeleteExternalAccountKey(t *testing.T) {
now := clock.Now() now := clock.Now()
dbeak := &dbExternalAccountKey{ dbeak := &dbExternalAccountKey{
ID: keyID, ID: keyID,
Provisioner: prov, ProvisionerID: provID,
Reference: ref, Reference: ref,
AccountID: "", AccountID: "",
KeyBytes: []byte{1, 3, 3, 7}, KeyBytes: []byte{1, 3, 3, 7},
@ -1267,27 +1311,46 @@ func TestDB_DeleteExternalAccountKey(t *testing.T) {
MGet: func(bucket, key []byte) ([]byte, error) { MGet: func(bucket, key []byte) ([]byte, error) {
switch string(bucket) { switch string(bucket) {
case string(externalAccountKeysByReferenceTable): case string(externalAccountKeysByReferenceTable):
assert.Equals(t, string(key), ref) assert.Equals(t, string(key), provID+"."+ref)
return dbrefBytes, nil return dbrefBytes, nil
case string(externalAccountKeyTable): case string(externalAccountKeyTable):
assert.Equals(t, string(key), keyID) assert.Equals(t, string(key), keyID)
return b, nil return b, nil
case string(externalAccountKeysByProvisionerIDTable):
assert.Equals(t, provID, string(key))
b, err := json.Marshal([]string{keyID})
assert.FatalError(t, err)
return b, nil
default: default:
assert.FatalError(t, errors.Errorf("unrecognized bucket %s", string(bucket))) assert.FatalError(t, errors.Errorf("unexpected bucket %s", string(bucket)))
return nil, errors.New("force") return nil, errors.New("force default")
} }
}, },
MDel: func(bucket, key []byte) error { MDel: func(bucket, key []byte) error {
switch string(bucket) { switch string(bucket) {
case string(externalAccountKeysByReferenceTable): case string(externalAccountKeysByReferenceTable):
assert.Equals(t, string(key), ref) assert.Equals(t, string(key), provID+"."+ref)
return nil return nil
case string(externalAccountKeyTable): case string(externalAccountKeyTable):
assert.Equals(t, string(key), keyID) assert.Equals(t, string(key), keyID)
return nil return nil
default: default:
assert.FatalError(t, errors.Errorf("unrecognized bucket %s", string(bucket))) assert.FatalError(t, errors.Errorf("unexpected bucket %s", string(bucket)))
return errors.New("force") return errors.New("force default")
}
},
MCmpAndSwap: func(bucket, key, old, new []byte) ([]byte, bool, error) {
fmt.Println(string(bucket))
switch string(bucket) {
case string(externalAccountKeysByReferenceTable):
assert.Equals(t, provID+"."+ref, string(key))
return nil, true, nil
case string(externalAccountKeysByProvisionerIDTable):
assert.Equals(t, provID, string(key))
return nil, true, nil
default:
assert.FatalError(t, errors.Errorf("unexpected bucket %s", string(bucket)))
return nil, false, errors.New("force default")
} }
}, },
}, },
@ -1302,14 +1365,14 @@ func TestDB_DeleteExternalAccountKey(t *testing.T) {
return nil, nosqldb.ErrNotFound return nil, nosqldb.ErrNotFound
}, },
}, },
err: errors.New("error loading ACME EAB Key with Key ID keyID"), err: errors.New("error loading ACME EAB Key with Key ID keyID: not found"),
} }
}, },
"fail/non-matching-provisioner": func(t *testing.T) test { "fail/non-matching-provisioner": func(t *testing.T) test {
now := clock.Now() now := clock.Now()
dbeak := &dbExternalAccountKey{ dbeak := &dbExternalAccountKey{
ID: keyID, ID: keyID,
Provisioner: "differentProvisioner", ProvisionerID: "aDifferentProvID",
Reference: ref, Reference: ref,
AccountID: "", AccountID: "",
KeyBytes: []byte{1, 3, 3, 7}, KeyBytes: []byte{1, 3, 3, 7},
@ -1325,14 +1388,14 @@ func TestDB_DeleteExternalAccountKey(t *testing.T) {
return b, nil return b, nil
}, },
}, },
err: errors.New("name of provisioner does not match provisioner for which the EAB key was created"), err: errors.New("provisioner does not match provisioner for which the EAB key was created"),
} }
}, },
"fail/delete-reference": func(t *testing.T) test { "fail/delete-reference": func(t *testing.T) test {
now := clock.Now() now := clock.Now()
dbeak := &dbExternalAccountKey{ dbeak := &dbExternalAccountKey{
ID: keyID, ID: keyID,
Provisioner: prov, ProvisionerID: provID,
Reference: ref, Reference: ref,
AccountID: "", AccountID: "",
KeyBytes: []byte{1, 3, 3, 7}, KeyBytes: []byte{1, 3, 3, 7},
@ -1357,32 +1420,32 @@ func TestDB_DeleteExternalAccountKey(t *testing.T) {
assert.Equals(t, string(key), keyID) assert.Equals(t, string(key), keyID)
return b, nil return b, nil
default: default:
assert.FatalError(t, errors.Errorf("unrecognized bucket %s", string(bucket))) assert.FatalError(t, errors.Errorf("unexpected bucket %s", string(bucket)))
return nil, errors.New("force") return nil, errors.New("force default")
} }
}, },
MDel: func(bucket, key []byte) error { MDel: func(bucket, key []byte) error {
switch string(bucket) { switch string(bucket) {
case string(externalAccountKeysByReferenceTable): case string(externalAccountKeysByReferenceTable):
assert.Equals(t, string(key), ref) assert.Equals(t, string(key), provID+"."+ref)
return errors.New("force") return errors.New("force")
case string(externalAccountKeyTable): case string(externalAccountKeyTable):
assert.Equals(t, string(key), keyID) assert.Equals(t, string(key), keyID)
return nil return nil
default: default:
assert.FatalError(t, errors.Errorf("unrecognized bucket %s", string(bucket))) assert.FatalError(t, errors.Errorf("unexpected bucket %s", string(bucket)))
return errors.New("force") return errors.New("force default")
} }
}, },
}, },
err: errors.New("error deleting ACME EAB Key Reference with Key ID keyID and reference ref"), err: errors.New("error deleting ACME EAB Key reference with Key ID keyID and reference ref: force"),
} }
}, },
"fail/delete-eak": func(t *testing.T) test { "fail/delete-eak": func(t *testing.T) test {
now := clock.Now() now := clock.Now()
dbeak := &dbExternalAccountKey{ dbeak := &dbExternalAccountKey{
ID: keyID, ID: keyID,
Provisioner: prov, ProvisionerID: provID,
Reference: ref, Reference: ref,
AccountID: "", AccountID: "",
KeyBytes: []byte{1, 3, 3, 7}, KeyBytes: []byte{1, 3, 3, 7},
@ -1407,25 +1470,25 @@ func TestDB_DeleteExternalAccountKey(t *testing.T) {
assert.Equals(t, string(key), keyID) assert.Equals(t, string(key), keyID)
return b, nil return b, nil
default: default:
assert.FatalError(t, errors.Errorf("unrecognized bucket %s", string(bucket))) assert.FatalError(t, errors.Errorf("unexpected bucket %s", string(bucket)))
return nil, errors.New("force") return nil, errors.New("force")
} }
}, },
MDel: func(bucket, key []byte) error { MDel: func(bucket, key []byte) error {
switch string(bucket) { switch string(bucket) {
case string(externalAccountKeysByReferenceTable): case string(externalAccountKeysByReferenceTable):
assert.Equals(t, string(key), ref) assert.Equals(t, string(key), provID+"."+ref)
return nil return nil
case string(externalAccountKeyTable): case string(externalAccountKeyTable):
assert.Equals(t, string(key), keyID) assert.Equals(t, string(key), keyID)
return errors.New("force") return errors.New("force")
default: default:
assert.FatalError(t, errors.Errorf("unrecognized bucket %s", string(bucket))) assert.FatalError(t, errors.Errorf("unexpected bucket %s", string(bucket)))
return errors.New("force") return errors.New("force")
} }
}, },
}, },
err: errors.New("error deleting ACME EAB Key with Key ID keyID"), err: errors.New("error deleting ACME EAB Key with Key ID keyID: force"),
} }
}, },
} }
@ -1433,7 +1496,7 @@ func TestDB_DeleteExternalAccountKey(t *testing.T) {
tc := run(t) tc := run(t)
t.Run(name, func(t *testing.T) { t.Run(name, func(t *testing.T) {
d := DB{db: tc.db} d := DB{db: tc.db}
if err := d.DeleteExternalAccountKey(context.Background(), prov, keyID); err != nil { if err := d.DeleteExternalAccountKey(context.Background(), provID, keyID); err != nil {
switch k := err.(type) { switch k := err.(type) {
case *acme.Error: case *acme.Error:
if assert.NotNil(t, tc.acmeErr) { if assert.NotNil(t, tc.acmeErr) {
@ -1445,7 +1508,7 @@ func TestDB_DeleteExternalAccountKey(t *testing.T) {
} }
default: default:
if assert.NotNil(t, tc.err) { if assert.NotNil(t, tc.err) {
assert.HasPrefix(t, err.Error(), tc.err.Error()) assert.Equals(t, err.Error(), tc.err.Error())
} }
} }
} else { } else {
@ -1457,7 +1520,7 @@ func TestDB_DeleteExternalAccountKey(t *testing.T) {
func TestDB_CreateExternalAccountKey(t *testing.T) { func TestDB_CreateExternalAccountKey(t *testing.T) {
keyID := "keyID" keyID := "keyID"
prov := "acmeProv" provID := "provID"
ref := "ref" ref := "ref"
type test struct { type test struct {
db nosql.DB db nosql.DB
@ -1474,29 +1537,37 @@ func TestDB_CreateExternalAccountKey(t *testing.T) {
now := clock.Now() now := clock.Now()
eak := &acme.ExternalAccountKey{ eak := &acme.ExternalAccountKey{
ID: keyID, ID: keyID,
Provisioner: prov, ProvisionerID: provID,
Reference: "ref", Reference: "ref",
AccountID: "", AccountID: "",
CreatedAt: now, CreatedAt: now,
} }
return test{ return test{
db: &db.MockNoSQLDB{ db: &db.MockNoSQLDB{
MGet: func(bucket, key []byte) ([]byte, error) {
assert.Equals(t, string(bucket), string(externalAccountKeysByProvisionerIDTable))
assert.Equals(t, provID, string(key))
b, _ := json.Marshal([]string{})
return b, nil
},
MCmpAndSwap: func(bucket, key, old, nu []byte) ([]byte, bool, error) { MCmpAndSwap: func(bucket, key, old, nu []byte) ([]byte, bool, error) {
switch string(bucket) { switch string(bucket) {
case string(externalAccountKeysByProvisionerIDTable):
assert.Equals(t, provID, string(key))
return nu, true, nil
case string(externalAccountKeysByReferenceTable): case string(externalAccountKeysByReferenceTable):
assert.Equals(t, string(key), ref) assert.Equals(t, provID+"."+ref, string(key))
assert.Equals(t, old, nil) assert.Equals(t, nil, old)
return nu, true, nil return nu, true, nil
case string(externalAccountKeyTable): case string(externalAccountKeyTable):
assert.Equals(t, old, nil) assert.Equals(t, nil, old)
id = string(key) id = string(key)
dbeak := new(dbExternalAccountKey) dbeak := new(dbExternalAccountKey)
assert.FatalError(t, json.Unmarshal(nu, dbeak)) assert.FatalError(t, json.Unmarshal(nu, dbeak))
assert.Equals(t, string(key), dbeak.ID) assert.Equals(t, string(key), dbeak.ID)
assert.Equals(t, eak.Provisioner, dbeak.Provisioner) assert.Equals(t, eak.ProvisionerID, dbeak.ProvisionerID)
assert.Equals(t, eak.Reference, dbeak.Reference) assert.Equals(t, eak.Reference, dbeak.Reference)
assert.Equals(t, 32, len(dbeak.KeyBytes)) assert.Equals(t, 32, len(dbeak.KeyBytes))
assert.False(t, dbeak.CreatedAt.IsZero()) assert.False(t, dbeak.CreatedAt.IsZero())
@ -1504,8 +1575,8 @@ func TestDB_CreateExternalAccountKey(t *testing.T) {
assert.True(t, dbeak.BoundAt.IsZero()) assert.True(t, dbeak.BoundAt.IsZero())
return nu, true, nil return nu, true, nil
default: default:
assert.FatalError(t, errors.Errorf("unrecognized bucket %s", string(bucket))) assert.FatalError(t, errors.Errorf("unexpected bucket %s", string(bucket)))
return nil, false, errors.New("force") return nil, false, errors.New("force default")
} }
}, },
}, },
@ -1527,34 +1598,42 @@ func TestDB_CreateExternalAccountKey(t *testing.T) {
assert.Equals(t, old, nil) assert.Equals(t, old, nil)
return nu, true, errors.New("force") return nu, true, errors.New("force")
default: default:
assert.FatalError(t, errors.Errorf("unrecognized bucket %s", string(bucket))) assert.FatalError(t, errors.Errorf("unexpected bucket %s", string(bucket)))
return nil, false, errors.New("force") return nil, false, errors.New("force")
} }
}, },
}, },
err: errors.New("error saving acme external_account_key"), err: errors.New("error saving acme external_account_key: force"),
} }
}, },
"fail/externalAccountKeyReference-cmpAndSwap-error": func(t *testing.T) test { "fail/externalAccountKeyReference-cmpAndSwap-error": func(t *testing.T) test {
return test{ return test{
db: &db.MockNoSQLDB{ db: &db.MockNoSQLDB{
MGet: func(bucket, key []byte) ([]byte, error) {
assert.Equals(t, string(bucket), string(externalAccountKeysByProvisionerIDTable))
assert.Equals(t, provID, string(key))
b, _ := json.Marshal([]string{})
return b, nil
},
MCmpAndSwap: func(bucket, key, old, nu []byte) ([]byte, bool, error) { MCmpAndSwap: func(bucket, key, old, nu []byte) ([]byte, bool, error) {
switch string(bucket) { switch string(bucket) {
case string(externalAccountKeysByProvisionerIDTable):
assert.Equals(t, provID, string(key))
return nu, true, nil
case string(externalAccountKeysByReferenceTable): case string(externalAccountKeysByReferenceTable):
assert.Equals(t, string(key), ref) assert.Equals(t, provID+"."+ref, string(key))
assert.Equals(t, old, nil) assert.Equals(t, old, nil)
return nu, true, errors.New("force") return nu, true, errors.New("force")
case string(externalAccountKeyTable): case string(externalAccountKeyTable):
assert.Equals(t, old, nil) assert.Equals(t, old, nil)
return nu, true, nil return nu, true, nil
default: default:
assert.FatalError(t, errors.Errorf("unrecognized bucket %s", string(bucket))) assert.FatalError(t, errors.Errorf("unexpected bucket %s", string(bucket)))
return nil, false, errors.New("force") return nil, false, errors.New("force")
} }
}, },
}, },
err: errors.New("error saving acme external_account_key"), err: errors.New("error saving acme external_account_key_reference: force"),
} }
}, },
} }
@ -1562,14 +1641,15 @@ func TestDB_CreateExternalAccountKey(t *testing.T) {
tc := run(t) tc := run(t)
t.Run(name, func(t *testing.T) { t.Run(name, func(t *testing.T) {
d := DB{db: tc.db} d := DB{db: tc.db}
eak, err := d.CreateExternalAccountKey(context.Background(), prov, ref) eak, err := d.CreateExternalAccountKey(context.Background(), provID, ref)
fmt.Println(name, err)
if err != nil { if err != nil {
if assert.NotNil(t, tc.err) { if assert.NotNil(t, tc.err) {
assert.HasPrefix(t, err.Error(), tc.err.Error()) assert.Equals(t, err.Error(), tc.err.Error())
} }
} else if assert.Nil(t, tc.err) { } else if assert.Nil(t, tc.err) {
assert.Equals(t, *tc._id, eak.ID) assert.Equals(t, *tc._id, eak.ID)
assert.Equals(t, prov, eak.Provisioner) assert.Equals(t, provID, eak.ProvisionerID)
assert.Equals(t, ref, eak.Reference) assert.Equals(t, ref, eak.Reference)
assert.Equals(t, "", eak.AccountID) assert.Equals(t, "", eak.AccountID)
assert.False(t, eak.CreatedAt.IsZero()) assert.False(t, eak.CreatedAt.IsZero())
@ -1582,12 +1662,12 @@ func TestDB_CreateExternalAccountKey(t *testing.T) {
func TestDB_UpdateExternalAccountKey(t *testing.T) { func TestDB_UpdateExternalAccountKey(t *testing.T) {
keyID := "keyID" keyID := "keyID"
prov := "acmeProv" provID := "provID"
ref := "ref" ref := "ref"
now := clock.Now() now := clock.Now()
dbeak := &dbExternalAccountKey{ dbeak := &dbExternalAccountKey{
ID: keyID, ID: keyID,
Provisioner: prov, ProvisionerID: provID,
Reference: ref, Reference: ref,
AccountID: "", AccountID: "",
KeyBytes: []byte{1, 3, 3, 7}, KeyBytes: []byte{1, 3, 3, 7},
@ -1605,7 +1685,7 @@ func TestDB_UpdateExternalAccountKey(t *testing.T) {
"ok": func(t *testing.T) test { "ok": func(t *testing.T) test {
eak := &acme.ExternalAccountKey{ eak := &acme.ExternalAccountKey{
ID: keyID, ID: keyID,
Provisioner: prov, ProvisionerID: provID,
Reference: ref, Reference: ref,
AccountID: "", AccountID: "",
KeyBytes: []byte{1, 3, 3, 7}, KeyBytes: []byte{1, 3, 3, 7},
@ -1627,7 +1707,7 @@ func TestDB_UpdateExternalAccountKey(t *testing.T) {
dbNew := new(dbExternalAccountKey) dbNew := new(dbExternalAccountKey)
assert.FatalError(t, json.Unmarshal(nu, dbNew)) assert.FatalError(t, json.Unmarshal(nu, dbNew))
assert.Equals(t, dbNew.ID, dbeak.ID) assert.Equals(t, dbNew.ID, dbeak.ID)
assert.Equals(t, dbNew.Provisioner, dbeak.Provisioner) assert.Equals(t, dbNew.ProvisionerID, dbeak.ProvisionerID)
assert.Equals(t, dbNew.Reference, dbeak.Reference) assert.Equals(t, dbNew.Reference, dbeak.Reference)
assert.Equals(t, dbNew.AccountID, dbeak.AccountID) assert.Equals(t, dbNew.AccountID, dbeak.AccountID)
assert.Equals(t, dbNew.CreatedAt, dbeak.CreatedAt) assert.Equals(t, dbNew.CreatedAt, dbeak.CreatedAt)
@ -1641,7 +1721,7 @@ func TestDB_UpdateExternalAccountKey(t *testing.T) {
"fail/provisioner-mismatch": func(t *testing.T) test { "fail/provisioner-mismatch": func(t *testing.T) test {
newDBEAK := &dbExternalAccountKey{ newDBEAK := &dbExternalAccountKey{
ID: keyID, ID: keyID,
Provisioner: "differentProvisioner", ProvisionerID: "aDifferentProvID",
Reference: ref, Reference: ref,
AccountID: "", AccountID: "",
KeyBytes: []byte{1, 3, 3, 7}, KeyBytes: []byte{1, 3, 3, 7},
@ -1661,7 +1741,7 @@ func TestDB_UpdateExternalAccountKey(t *testing.T) {
return b, nil return b, nil
}, },
}, },
err: errors.New("name of provisioner does not match provisioner for which the EAB key was created"), err: errors.New("provisioner does not match provisioner for which the EAB key was created"),
} }
}, },
"fail/db.Get-error": func(t *testing.T) test { "fail/db.Get-error": func(t *testing.T) test {
@ -1685,13 +1765,13 @@ func TestDB_UpdateExternalAccountKey(t *testing.T) {
tc := run(t) tc := run(t)
t.Run(name, func(t *testing.T) { t.Run(name, func(t *testing.T) {
d := DB{db: tc.db} d := DB{db: tc.db}
if err := d.UpdateExternalAccountKey(context.Background(), prov, tc.eak); err != nil { if err := d.UpdateExternalAccountKey(context.Background(), provID, tc.eak); err != nil {
if assert.NotNil(t, tc.err) { if assert.NotNil(t, tc.err) {
assert.HasPrefix(t, err.Error(), tc.err.Error()) assert.HasPrefix(t, err.Error(), tc.err.Error())
} }
} else if assert.Nil(t, tc.err) { } else if assert.Nil(t, tc.err) {
assert.Equals(t, dbeak.ID, tc.eak.ID) assert.Equals(t, dbeak.ID, tc.eak.ID)
assert.Equals(t, dbeak.Provisioner, tc.eak.Provisioner) assert.Equals(t, dbeak.ProvisionerID, tc.eak.ProvisionerID)
assert.Equals(t, dbeak.Reference, tc.eak.Reference) assert.Equals(t, dbeak.Reference, tc.eak.Reference)
assert.Equals(t, dbeak.AccountID, tc.eak.AccountID) assert.Equals(t, dbeak.AccountID, tc.eak.AccountID)
assert.Equals(t, dbeak.CreatedAt, tc.eak.CreatedAt) assert.Equals(t, dbeak.CreatedAt, tc.eak.CreatedAt)

View file

@ -22,6 +22,7 @@ var (
certBySerialTable = []byte("acme_serial_certs_index") certBySerialTable = []byte("acme_serial_certs_index")
externalAccountKeyTable = []byte("acme_external_account_keys") externalAccountKeyTable = []byte("acme_external_account_keys")
externalAccountKeysByReferenceTable = []byte("acme_external_account_key_reference_index") externalAccountKeysByReferenceTable = []byte("acme_external_account_key_reference_index")
externalAccountKeysByProvisionerIDTable = []byte("acme_external_account_keyID_provisionerID_index")
) )
// DB is a struct that implements the AcmeDB interface. // DB is a struct that implements the AcmeDB interface.
@ -33,7 +34,8 @@ type DB struct {
func New(db nosqlDB.DB) (*DB, error) { func New(db nosqlDB.DB) (*DB, error) {
tables := [][]byte{accountTable, accountByKeyIDTable, authzTable, tables := [][]byte{accountTable, accountByKeyIDTable, authzTable,
challengeTable, nonceTable, orderTable, ordersByAccountIDTable, challengeTable, nonceTable, orderTable, ordersByAccountIDTable,
certTable, certBySerialTable, externalAccountKeyTable, externalAccountKeysByReferenceTable, certTable, certBySerialTable, externalAccountKeyTable,
externalAccountKeysByReferenceTable, externalAccountKeysByProvisionerIDTable,
} }
for _, b := range tables { for _, b := range tables {
if err := db.CreateTable(b); err != nil { if err := db.CreateTable(b); err != nil {

View file

@ -15,6 +15,11 @@ import (
"google.golang.org/protobuf/types/known/timestamppb" "google.golang.org/protobuf/types/known/timestamppb"
) )
const (
// provisionerContextKey provisioner key
provisionerContextKey = ContextKey("provisioner")
)
// CreateExternalAccountKeyRequest is the type for POST /admin/acme/eab requests // CreateExternalAccountKeyRequest is the type for POST /admin/acme/eab requests
type CreateExternalAccountKeyRequest struct { type CreateExternalAccountKeyRequest struct {
Reference string `json:"reference"` Reference string `json:"reference"`
@ -37,47 +42,63 @@ type GetExternalAccountKeysResponse struct {
// before serving requests that act on ACME EAB credentials. // before serving requests that act on ACME EAB credentials.
func (h *Handler) requireEABEnabled(next nextHTTP) nextHTTP { func (h *Handler) requireEABEnabled(next nextHTTP) nextHTTP {
return func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) {
prov := chi.URLParam(r, "prov") ctx := r.Context()
eabEnabled, err := h.provisionerHasEABEnabled(r.Context(), prov) provName := chi.URLParam(r, "prov")
eabEnabled, prov, err := h.provisionerHasEABEnabled(ctx, provName)
if err != nil { if err != nil {
api.WriteError(w, err) api.WriteError(w, err)
return return
} }
if !eabEnabled { if !eabEnabled {
api.WriteError(w, admin.NewError(admin.ErrorBadRequestType, "ACME EAB not enabled for provisioner %s", prov)) api.WriteError(w, admin.NewError(admin.ErrorBadRequestType, "ACME EAB not enabled for provisioner %s", prov.GetName()))
return return
} }
next(w, r) ctx = context.WithValue(ctx, provisionerContextKey, prov)
next(w, r.WithContext(ctx))
} }
} }
// provisionerHasEABEnabled determines if the "requireEAB" setting for an ACME // provisionerHasEABEnabled determines if the "requireEAB" setting for an ACME
// provisioner is set to true and thus has EAB enabled. // provisioner is set to true and thus has EAB enabled.
func (h *Handler) provisionerHasEABEnabled(ctx context.Context, provisionerName string) (bool, error) { func (h *Handler) provisionerHasEABEnabled(ctx context.Context, provisionerName string) (bool, *linkedca.Provisioner, error) {
var ( var (
p provisioner.Interface p provisioner.Interface
err error err error
) )
if p, err = h.auth.LoadProvisionerByName(provisionerName); err != nil { if p, err = h.auth.LoadProvisionerByName(provisionerName); err != nil {
return false, admin.WrapErrorISE(err, "error loading provisioner %s", provisionerName) return false, nil, admin.WrapErrorISE(err, "error loading provisioner %s", provisionerName)
} }
prov, err := h.db.GetProvisioner(ctx, p.GetID()) prov, err := h.db.GetProvisioner(ctx, p.GetID())
if err != nil { if err != nil {
return false, admin.WrapErrorISE(err, "error getting provisioner with ID: %s", p.GetID()) return false, nil, admin.WrapErrorISE(err, "error getting provisioner with ID: %s", p.GetID())
} }
details := prov.GetDetails() details := prov.GetDetails()
if details == nil { if details == nil {
return false, admin.NewErrorISE("error getting details for provisioner with ID: %s", p.GetID()) return false, nil, admin.NewErrorISE("error getting details for provisioner with ID: %s", p.GetID())
} }
acmeProvisioner := details.GetACME() acmeProvisioner := details.GetACME()
if acmeProvisioner == nil { if acmeProvisioner == nil {
return false, admin.NewErrorISE("error getting ACME details for provisioner with ID: %s", p.GetID()) return false, nil, admin.NewErrorISE("error getting ACME details for provisioner with ID: %s", p.GetID())
} }
return acmeProvisioner.GetRequireEab(), nil 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
} }
// CreateExternalAccountKey creates a new External Account Binding key // CreateExternalAccountKey creates a new External Account Binding key
@ -93,12 +114,17 @@ func (h *Handler) CreateExternalAccountKey(w http.ResponseWriter, r *http.Reques
return return
} }
prov := chi.URLParam(r, "prov") ctx := r.Context()
reference := body.Reference 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) // check if a key with the reference does not exist (only when a reference was in the request)
reference := body.Reference
if reference != "" { if reference != "" {
k, err := h.acmeDB.GetExternalAccountKeyByReference(r.Context(), prov, 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, // 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. // 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 { if shouldWriteError := err != nil && !errors.Is(err, acme.ErrNotFound); shouldWriteError {
@ -107,7 +133,7 @@ func (h *Handler) CreateExternalAccountKey(w http.ResponseWriter, r *http.Reques
} }
// if a key was found, return HTTP 409 conflict // if a key was found, return HTTP 409 conflict
if k != nil { if k != nil {
err := admin.NewError(admin.ErrorBadRequestType, "an ACME EAB key for provisioner %s with reference %s already exists", prov, reference) err := admin.NewError(admin.ErrorBadRequestType, "an ACME EAB key for provisioner '%s' with reference '%s' already exists", prov.GetName(), reference)
err.Status = 409 err.Status = 409
api.WriteError(w, err) api.WriteError(w, err)
return return
@ -115,9 +141,9 @@ func (h *Handler) CreateExternalAccountKey(w http.ResponseWriter, r *http.Reques
// continue execution if no key was found for the reference // continue execution if no key was found for the reference
} }
eak, err := h.acmeDB.CreateExternalAccountKey(r.Context(), prov, reference) eak, err := h.acmeDB.CreateExternalAccountKey(ctx, prov.GetId(), reference)
if err != nil { if err != nil {
msg := fmt.Sprintf("error creating ACME EAB key for provisioner '%s'", prov) msg := fmt.Sprintf("error creating ACME EAB key for provisioner '%s'", prov.GetName())
if reference != "" { if reference != "" {
msg += fmt.Sprintf(" and reference '%s'", reference) msg += fmt.Sprintf(" and reference '%s'", reference)
} }
@ -128,7 +154,7 @@ func (h *Handler) CreateExternalAccountKey(w http.ResponseWriter, r *http.Reques
response := &linkedca.EABKey{ response := &linkedca.EABKey{
Id: eak.ID, Id: eak.ID,
HmacKey: eak.KeyBytes, HmacKey: eak.KeyBytes,
Provisioner: eak.Provisioner, Provisioner: prov.GetName(),
Reference: eak.Reference, Reference: eak.Reference,
} }
@ -137,10 +163,17 @@ func (h *Handler) CreateExternalAccountKey(w http.ResponseWriter, r *http.Reques
// DeleteExternalAccountKey deletes an ACME External Account Key. // DeleteExternalAccountKey deletes an ACME External Account Key.
func (h *Handler) DeleteExternalAccountKey(w http.ResponseWriter, r *http.Request) { func (h *Handler) DeleteExternalAccountKey(w http.ResponseWriter, r *http.Request) {
prov := chi.URLParam(r, "prov")
keyID := chi.URLParam(r, "id") keyID := chi.URLParam(r, "id")
if err := h.acmeDB.DeleteExternalAccountKey(r.Context(), prov, keyID); err != nil { 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)) api.WriteError(w, admin.WrapErrorISE(err, "error deleting ACME EAB Key '%s'", keyID))
return return
} }
@ -152,8 +185,6 @@ func (h *Handler) DeleteExternalAccountKey(w http.ResponseWriter, r *http.Reques
// only the ExternalAccountKey with that reference is returned. Otherwise all // only the ExternalAccountKey with that reference is returned. Otherwise all
// ExternalAccountKeys in the system for a specific provisioner are returned. // ExternalAccountKeys in the system for a specific provisioner are returned.
func (h *Handler) GetExternalAccountKeys(w http.ResponseWriter, r *http.Request) { func (h *Handler) GetExternalAccountKeys(w http.ResponseWriter, r *http.Request) {
prov := chi.URLParam(r, "prov")
reference := chi.URLParam(r, "ref")
var ( var (
key *acme.ExternalAccountKey key *acme.ExternalAccountKey
@ -161,8 +192,16 @@ func (h *Handler) GetExternalAccountKeys(w http.ResponseWriter, r *http.Request)
err error err error
) )
ctx := r.Context()
prov, err := provisionerFromContext(ctx)
if err != nil {
api.WriteError(w, admin.WrapErrorISE(err, "error getting provisioner from context"))
return
}
reference := chi.URLParam(r, "ref")
if reference != "" { if reference != "" {
if key, err = h.acmeDB.GetExternalAccountKeyByReference(r.Context(), prov, reference); err != nil { 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)) api.WriteError(w, admin.WrapErrorISE(err, "error retrieving external account key with reference '%s'", reference))
return return
} }
@ -170,18 +209,19 @@ func (h *Handler) GetExternalAccountKeys(w http.ResponseWriter, r *http.Request)
keys = []*acme.ExternalAccountKey{key} keys = []*acme.ExternalAccountKey{key}
} }
} else { } else {
if keys, err = h.acmeDB.GetExternalAccountKeys(r.Context(), prov); err != nil { if keys, err = h.acmeDB.GetExternalAccountKeys(ctx, prov.GetId()); err != nil {
api.WriteError(w, admin.WrapErrorISE(err, "error retrieving external account keys")) api.WriteError(w, admin.WrapErrorISE(err, "error retrieving external account keys"))
return return
} }
} }
provisionerName := prov.GetName()
eaks := make([]*linkedca.EABKey, len(keys)) eaks := make([]*linkedca.EABKey, len(keys))
for i, k := range keys { for i, k := range keys {
eaks[i] = &linkedca.EABKey{ eaks[i] = &linkedca.EABKey{
Id: k.ID, Id: k.ID,
HmacKey: []byte{}, HmacKey: []byte{},
Provisioner: k.Provisioner, Provisioner: provisionerName,
Reference: k.Reference, Reference: k.Reference,
Account: k.AccountID, Account: k.AccountID,
CreatedAt: timestamppb.New(k.CreatedAt), CreatedAt: timestamppb.New(k.CreatedAt),

View file

@ -368,12 +368,13 @@ func TestHandler_provisionerHasEABEnabled(t *testing.T) {
auth: tc.auth, auth: tc.auth,
acmeDB: nil, acmeDB: nil,
} }
got, err := h.provisionerHasEABEnabled(context.TODO(), tc.provisionerName) got, prov, err := h.provisionerHasEABEnabled(context.TODO(), tc.provisionerName)
if (err != nil) != (tc.err != nil) { if (err != nil) != (tc.err != nil) {
t.Errorf("Handler.provisionerHasEABEnabled() error = %v, want err %v", err, tc.err) t.Errorf("Handler.provisionerHasEABEnabled() error = %v, want err %v", err, tc.err)
return return
} }
if tc.err != nil { if tc.err != nil {
assert.Type(t, &linkedca.Provisioner{}, prov)
assert.Type(t, &admin.Error{}, err) assert.Type(t, &admin.Error{}, err)
adminError, _ := err.(*admin.Error) adminError, _ := err.(*admin.Error)
assert.Equals(t, tc.err.Type, adminError.Type) assert.Equals(t, tc.err.Type, adminError.Type)
@ -434,6 +435,10 @@ func TestCreateExternalAccountKeyRequest_Validate(t *testing.T) {
} }
func TestHandler_CreateExternalAccountKey(t *testing.T) { func TestHandler_CreateExternalAccountKey(t *testing.T) {
prov := &linkedca.Provisioner{
Id: "provID",
Name: "provName",
}
type test struct { type test struct {
ctx context.Context ctx context.Context
db acme.DB db acme.DB
@ -487,14 +492,15 @@ func TestHandler_CreateExternalAccountKey(t *testing.T) {
chiCtx := chi.NewRouteContext() chiCtx := chi.NewRouteContext()
chiCtx.URLParams.Add("prov", "provName") chiCtx.URLParams.Add("prov", "provName")
ctx := context.WithValue(context.Background(), chi.RouteCtxKey, chiCtx) ctx := context.WithValue(context.Background(), chi.RouteCtxKey, chiCtx)
ctx = context.WithValue(ctx, provisionerContextKey, prov)
req := CreateExternalAccountKeyRequest{ req := CreateExternalAccountKeyRequest{
Reference: "an-external-key-reference", Reference: "an-external-key-reference",
} }
body, err := json.Marshal(req) body, err := json.Marshal(req)
assert.FatalError(t, err) assert.FatalError(t, err)
db := &acme.MockDB{ db := &acme.MockDB{
MockGetExternalAccountKeyByReference: func(ctx context.Context, provisionerName, reference string) (*acme.ExternalAccountKey, error) { MockGetExternalAccountKeyByReference: func(ctx context.Context, provisionerID, reference string) (*acme.ExternalAccountKey, error) {
assert.Equals(t, "provName", provisionerName) assert.Equals(t, "provID", provisionerID)
assert.Equals(t, "an-external-key-reference", reference) assert.Equals(t, "an-external-key-reference", reference)
return nil, errors.New("force") return nil, errors.New("force")
}, },
@ -517,19 +523,20 @@ func TestHandler_CreateExternalAccountKey(t *testing.T) {
chiCtx := chi.NewRouteContext() chiCtx := chi.NewRouteContext()
chiCtx.URLParams.Add("prov", "provName") chiCtx.URLParams.Add("prov", "provName")
ctx := context.WithValue(context.Background(), chi.RouteCtxKey, chiCtx) ctx := context.WithValue(context.Background(), chi.RouteCtxKey, chiCtx)
ctx = context.WithValue(ctx, provisionerContextKey, prov)
req := CreateExternalAccountKeyRequest{ req := CreateExternalAccountKeyRequest{
Reference: "an-external-key-reference", Reference: "an-external-key-reference",
} }
body, err := json.Marshal(req) body, err := json.Marshal(req)
assert.FatalError(t, err) assert.FatalError(t, err)
db := &acme.MockDB{ db := &acme.MockDB{
MockGetExternalAccountKeyByReference: func(ctx context.Context, provisionerName, reference string) (*acme.ExternalAccountKey, error) { MockGetExternalAccountKeyByReference: func(ctx context.Context, provisionerID, reference string) (*acme.ExternalAccountKey, error) {
assert.Equals(t, "provName", provisionerName) assert.Equals(t, "provID", provisionerID)
assert.Equals(t, "an-external-key-reference", reference) assert.Equals(t, "an-external-key-reference", reference)
past := time.Now().Add(-24 * time.Hour) past := time.Now().Add(-24 * time.Hour)
return &acme.ExternalAccountKey{ return &acme.ExternalAccountKey{
ID: "eakID", ID: "eakID",
Provisioner: "provName", ProvisionerID: "provID",
Reference: "an-external-key-reference", Reference: "an-external-key-reference",
KeyBytes: []byte{1, 3, 3, 7}, KeyBytes: []byte{1, 3, 3, 7},
CreatedAt: past, CreatedAt: past,
@ -546,7 +553,7 @@ func TestHandler_CreateExternalAccountKey(t *testing.T) {
Type: admin.ErrorBadRequestType.String(), Type: admin.ErrorBadRequestType.String(),
Status: 409, Status: 409,
Detail: "bad request", Detail: "bad request",
Message: "an ACME EAB key for provisioner provName with reference an-external-key-reference already exists", Message: "an ACME EAB key for provisioner 'provName' with reference 'an-external-key-reference' already exists",
}, },
} }
}, },
@ -554,14 +561,15 @@ func TestHandler_CreateExternalAccountKey(t *testing.T) {
chiCtx := chi.NewRouteContext() chiCtx := chi.NewRouteContext()
chiCtx.URLParams.Add("prov", "provName") chiCtx.URLParams.Add("prov", "provName")
ctx := context.WithValue(context.Background(), chi.RouteCtxKey, chiCtx) ctx := context.WithValue(context.Background(), chi.RouteCtxKey, chiCtx)
ctx = context.WithValue(ctx, provisionerContextKey, prov)
req := CreateExternalAccountKeyRequest{ req := CreateExternalAccountKeyRequest{
Reference: "", Reference: "",
} }
body, err := json.Marshal(req) body, err := json.Marshal(req)
assert.FatalError(t, err) assert.FatalError(t, err)
db := &acme.MockDB{ db := &acme.MockDB{
MockCreateExternalAccountKey: func(ctx context.Context, provisionerName, reference string) (*acme.ExternalAccountKey, error) { MockCreateExternalAccountKey: func(ctx context.Context, provisionerID, reference string) (*acme.ExternalAccountKey, error) {
assert.Equals(t, "provName", provisionerName) assert.Equals(t, "provID", provisionerID)
assert.Equals(t, "", reference) assert.Equals(t, "", reference)
return nil, errors.New("force") return nil, errors.New("force")
}, },
@ -583,19 +591,20 @@ func TestHandler_CreateExternalAccountKey(t *testing.T) {
chiCtx := chi.NewRouteContext() chiCtx := chi.NewRouteContext()
chiCtx.URLParams.Add("prov", "provName") chiCtx.URLParams.Add("prov", "provName")
ctx := context.WithValue(context.Background(), chi.RouteCtxKey, chiCtx) ctx := context.WithValue(context.Background(), chi.RouteCtxKey, chiCtx)
ctx = context.WithValue(ctx, provisionerContextKey, prov)
req := CreateExternalAccountKeyRequest{ req := CreateExternalAccountKeyRequest{
Reference: "an-external-key-reference", Reference: "an-external-key-reference",
} }
body, err := json.Marshal(req) body, err := json.Marshal(req)
assert.FatalError(t, err) assert.FatalError(t, err)
db := &acme.MockDB{ db := &acme.MockDB{
MockGetExternalAccountKeyByReference: func(ctx context.Context, provisionerName, reference string) (*acme.ExternalAccountKey, error) { MockGetExternalAccountKeyByReference: func(ctx context.Context, provisionerID, reference string) (*acme.ExternalAccountKey, error) {
assert.Equals(t, "provName", provisionerName) assert.Equals(t, "provID", provisionerID)
assert.Equals(t, "an-external-key-reference", reference) assert.Equals(t, "an-external-key-reference", reference)
return nil, acme.ErrNotFound // simulating not found; skipping 409 conflict return nil, acme.ErrNotFound // simulating not found; skipping 409 conflict
}, },
MockCreateExternalAccountKey: func(ctx context.Context, provisionerName, reference string) (*acme.ExternalAccountKey, error) { MockCreateExternalAccountKey: func(ctx context.Context, provisionerID, reference string) (*acme.ExternalAccountKey, error) {
assert.Equals(t, "provName", provisionerName) assert.Equals(t, "provID", provisionerID)
assert.Equals(t, "an-external-key-reference", reference) assert.Equals(t, "an-external-key-reference", reference)
return nil, errors.New("force") return nil, errors.New("force")
}, },
@ -617,6 +626,7 @@ func TestHandler_CreateExternalAccountKey(t *testing.T) {
chiCtx := chi.NewRouteContext() chiCtx := chi.NewRouteContext()
chiCtx.URLParams.Add("prov", "provName") chiCtx.URLParams.Add("prov", "provName")
ctx := context.WithValue(context.Background(), chi.RouteCtxKey, chiCtx) ctx := context.WithValue(context.Background(), chi.RouteCtxKey, chiCtx)
ctx = context.WithValue(ctx, provisionerContextKey, prov)
req := CreateExternalAccountKeyRequest{ req := CreateExternalAccountKeyRequest{
Reference: "", Reference: "",
} }
@ -624,12 +634,12 @@ func TestHandler_CreateExternalAccountKey(t *testing.T) {
assert.FatalError(t, err) assert.FatalError(t, err)
now := time.Now() now := time.Now()
db := &acme.MockDB{ db := &acme.MockDB{
MockCreateExternalAccountKey: func(ctx context.Context, provisionerName, reference string) (*acme.ExternalAccountKey, error) { MockCreateExternalAccountKey: func(ctx context.Context, provisionerID, reference string) (*acme.ExternalAccountKey, error) {
assert.Equals(t, "provName", provisionerName) assert.Equals(t, "provID", provisionerID)
assert.Equals(t, "", reference) assert.Equals(t, "", reference)
return &acme.ExternalAccountKey{ return &acme.ExternalAccountKey{
ID: "eakID", ID: "eakID",
Provisioner: "provName", ProvisionerID: "provID",
Reference: "", Reference: "",
KeyBytes: []byte{1, 3, 3, 7}, KeyBytes: []byte{1, 3, 3, 7},
CreatedAt: now, CreatedAt: now,
@ -653,6 +663,7 @@ func TestHandler_CreateExternalAccountKey(t *testing.T) {
chiCtx := chi.NewRouteContext() chiCtx := chi.NewRouteContext()
chiCtx.URLParams.Add("prov", "provName") chiCtx.URLParams.Add("prov", "provName")
ctx := context.WithValue(context.Background(), chi.RouteCtxKey, chiCtx) ctx := context.WithValue(context.Background(), chi.RouteCtxKey, chiCtx)
ctx = context.WithValue(ctx, provisionerContextKey, prov)
req := CreateExternalAccountKeyRequest{ req := CreateExternalAccountKeyRequest{
Reference: "an-external-key-reference", Reference: "an-external-key-reference",
} }
@ -660,17 +671,17 @@ func TestHandler_CreateExternalAccountKey(t *testing.T) {
assert.FatalError(t, err) assert.FatalError(t, err)
now := time.Now() now := time.Now()
db := &acme.MockDB{ db := &acme.MockDB{
MockGetExternalAccountKeyByReference: func(ctx context.Context, provisionerName, reference string) (*acme.ExternalAccountKey, error) { MockGetExternalAccountKeyByReference: func(ctx context.Context, provisionerID, reference string) (*acme.ExternalAccountKey, error) {
assert.Equals(t, "provName", provisionerName) assert.Equals(t, "provID", provisionerID)
assert.Equals(t, "an-external-key-reference", reference) assert.Equals(t, "an-external-key-reference", reference)
return nil, acme.ErrNotFound // simulating not found; skipping 409 conflict return nil, acme.ErrNotFound // simulating not found; skipping 409 conflict
}, },
MockCreateExternalAccountKey: func(ctx context.Context, provisionerName, reference string) (*acme.ExternalAccountKey, error) { MockCreateExternalAccountKey: func(ctx context.Context, provisionerID, reference string) (*acme.ExternalAccountKey, error) {
assert.Equals(t, "provName", provisionerName) assert.Equals(t, "provID", provisionerID)
assert.Equals(t, "an-external-key-reference", reference) assert.Equals(t, "an-external-key-reference", reference)
return &acme.ExternalAccountKey{ return &acme.ExternalAccountKey{
ID: "eakID", ID: "eakID",
Provisioner: "provName", ProvisionerID: "provID",
Reference: "an-external-key-reference", Reference: "an-external-key-reference",
KeyBytes: []byte{1, 3, 3, 7}, KeyBytes: []byte{1, 3, 3, 7},
CreatedAt: now, CreatedAt: now,
@ -737,6 +748,10 @@ func TestHandler_CreateExternalAccountKey(t *testing.T) {
} }
func TestHandler_DeleteExternalAccountKey(t *testing.T) { func TestHandler_DeleteExternalAccountKey(t *testing.T) {
prov := &linkedca.Provisioner{
Id: "provID",
Name: "provName",
}
type test struct { type test struct {
ctx context.Context ctx context.Context
db acme.DB db acme.DB
@ -749,9 +764,10 @@ func TestHandler_DeleteExternalAccountKey(t *testing.T) {
chiCtx.URLParams.Add("prov", "provName") chiCtx.URLParams.Add("prov", "provName")
chiCtx.URLParams.Add("id", "keyID") chiCtx.URLParams.Add("id", "keyID")
ctx := context.WithValue(context.Background(), chi.RouteCtxKey, chiCtx) ctx := context.WithValue(context.Background(), chi.RouteCtxKey, chiCtx)
ctx = context.WithValue(ctx, provisionerContextKey, prov)
db := &acme.MockDB{ db := &acme.MockDB{
MockDeleteExternalAccountKey: func(ctx context.Context, provisionerName, keyID string) error { MockDeleteExternalAccountKey: func(ctx context.Context, provisionerID, keyID string) error {
assert.Equals(t, "provName", provisionerName) assert.Equals(t, "provID", provisionerID)
assert.Equals(t, "keyID", keyID) assert.Equals(t, "keyID", keyID)
return errors.New("force") return errors.New("force")
}, },
@ -773,9 +789,10 @@ func TestHandler_DeleteExternalAccountKey(t *testing.T) {
chiCtx.URLParams.Add("prov", "provName") chiCtx.URLParams.Add("prov", "provName")
chiCtx.URLParams.Add("id", "keyID") chiCtx.URLParams.Add("id", "keyID")
ctx := context.WithValue(context.Background(), chi.RouteCtxKey, chiCtx) ctx := context.WithValue(context.Background(), chi.RouteCtxKey, chiCtx)
ctx = context.WithValue(ctx, provisionerContextKey, prov)
db := &acme.MockDB{ db := &acme.MockDB{
MockDeleteExternalAccountKey: func(ctx context.Context, provisionerName, keyID string) error { MockDeleteExternalAccountKey: func(ctx context.Context, provisionerID, keyID string) error {
assert.Equals(t, "provName", provisionerName) assert.Equals(t, "provID", provisionerID)
assert.Equals(t, "keyID", keyID) assert.Equals(t, "keyID", keyID)
return nil return nil
}, },
@ -831,6 +848,10 @@ func TestHandler_DeleteExternalAccountKey(t *testing.T) {
} }
func TestHandler_GetExternalAccountKeys(t *testing.T) { func TestHandler_GetExternalAccountKeys(t *testing.T) {
prov := &linkedca.Provisioner{
Id: "provID",
Name: "provName",
}
type test struct { type test struct {
ctx context.Context ctx context.Context
db acme.DB db acme.DB
@ -846,9 +867,10 @@ func TestHandler_GetExternalAccountKeys(t *testing.T) {
chiCtx.URLParams.Add("ref", "an-external-key-reference") chiCtx.URLParams.Add("ref", "an-external-key-reference")
req := httptest.NewRequest("GET", "/foo", nil) req := httptest.NewRequest("GET", "/foo", nil)
ctx := context.WithValue(context.Background(), chi.RouteCtxKey, chiCtx) ctx := context.WithValue(context.Background(), chi.RouteCtxKey, chiCtx)
ctx = context.WithValue(ctx, provisionerContextKey, prov)
db := &acme.MockDB{ db := &acme.MockDB{
MockGetExternalAccountKeyByReference: func(ctx context.Context, provisionerName, reference string) (*acme.ExternalAccountKey, error) { MockGetExternalAccountKeyByReference: func(ctx context.Context, provisionerID, reference string) (*acme.ExternalAccountKey, error) {
assert.Equals(t, "provName", provisionerName) assert.Equals(t, "provID", provisionerID)
assert.Equals(t, "an-external-key-reference", reference) assert.Equals(t, "an-external-key-reference", reference)
return nil, errors.New("force") return nil, errors.New("force")
}, },
@ -871,9 +893,10 @@ func TestHandler_GetExternalAccountKeys(t *testing.T) {
chiCtx.URLParams.Add("prov", "provName") chiCtx.URLParams.Add("prov", "provName")
req := httptest.NewRequest("GET", "/foo", nil) req := httptest.NewRequest("GET", "/foo", nil)
ctx := context.WithValue(context.Background(), chi.RouteCtxKey, chiCtx) ctx := context.WithValue(context.Background(), chi.RouteCtxKey, chiCtx)
ctx = context.WithValue(ctx, provisionerContextKey, prov)
db := &acme.MockDB{ db := &acme.MockDB{
MockGetExternalAccountKeys: func(ctx context.Context, provisionerName string) ([]*acme.ExternalAccountKey, error) { MockGetExternalAccountKeys: func(ctx context.Context, provisionerID string) ([]*acme.ExternalAccountKey, error) {
assert.Equals(t, "provName", provisionerName) assert.Equals(t, "provID", provisionerID)
return nil, errors.New("force") return nil, errors.New("force")
}, },
} }
@ -896,9 +919,10 @@ func TestHandler_GetExternalAccountKeys(t *testing.T) {
chiCtx.URLParams.Add("ref", "an-external-key-reference") chiCtx.URLParams.Add("ref", "an-external-key-reference")
req := httptest.NewRequest("GET", "/foo", nil) req := httptest.NewRequest("GET", "/foo", nil)
ctx := context.WithValue(context.Background(), chi.RouteCtxKey, chiCtx) ctx := context.WithValue(context.Background(), chi.RouteCtxKey, chiCtx)
ctx = context.WithValue(ctx, provisionerContextKey, prov)
db := &acme.MockDB{ db := &acme.MockDB{
MockGetExternalAccountKeyByReference: func(ctx context.Context, provisionerName, reference string) (*acme.ExternalAccountKey, error) { MockGetExternalAccountKeyByReference: func(ctx context.Context, provisionerID, reference string) (*acme.ExternalAccountKey, error) {
assert.Equals(t, "provName", provisionerName) assert.Equals(t, "provID", provisionerID)
assert.Equals(t, "an-external-key-reference", reference) assert.Equals(t, "an-external-key-reference", reference)
return nil, nil // returning nil; no key found return nil, nil // returning nil; no key found
}, },
@ -920,15 +944,16 @@ func TestHandler_GetExternalAccountKeys(t *testing.T) {
chiCtx.URLParams.Add("ref", "an-external-key-reference") chiCtx.URLParams.Add("ref", "an-external-key-reference")
req := httptest.NewRequest("GET", "/foo", nil) req := httptest.NewRequest("GET", "/foo", nil)
ctx := context.WithValue(context.Background(), chi.RouteCtxKey, chiCtx) ctx := context.WithValue(context.Background(), chi.RouteCtxKey, chiCtx)
ctx = context.WithValue(ctx, provisionerContextKey, prov)
createdAt := time.Now().Add(-24 * time.Hour) createdAt := time.Now().Add(-24 * time.Hour)
var boundAt time.Time var boundAt time.Time
db := &acme.MockDB{ db := &acme.MockDB{
MockGetExternalAccountKeyByReference: func(ctx context.Context, provisionerName, reference string) (*acme.ExternalAccountKey, error) { MockGetExternalAccountKeyByReference: func(ctx context.Context, provisionerID, reference string) (*acme.ExternalAccountKey, error) {
assert.Equals(t, "provName", provisionerName) assert.Equals(t, "provID", provisionerID)
assert.Equals(t, "an-external-key-reference", reference) assert.Equals(t, "an-external-key-reference", reference)
return &acme.ExternalAccountKey{ return &acme.ExternalAccountKey{
ID: "eakID", ID: "eakID",
Provisioner: "provName", ProvisionerID: "provID",
Reference: "an-external-key-reference", Reference: "an-external-key-reference",
CreatedAt: createdAt, CreatedAt: createdAt,
}, nil }, nil
@ -958,102 +983,31 @@ func TestHandler_GetExternalAccountKeys(t *testing.T) {
chiCtx.URLParams.Add("prov", "provName") chiCtx.URLParams.Add("prov", "provName")
req := httptest.NewRequest("GET", "/foo", nil) req := httptest.NewRequest("GET", "/foo", nil)
ctx := context.WithValue(context.Background(), chi.RouteCtxKey, chiCtx) ctx := context.WithValue(context.Background(), chi.RouteCtxKey, chiCtx)
ctx = context.WithValue(ctx, provisionerContextKey, prov)
createdAt := time.Now().Add(-24 * time.Hour) createdAt := time.Now().Add(-24 * time.Hour)
var boundAt time.Time var boundAt time.Time
boundAtSet := time.Now().Add(-12 * time.Hour) boundAtSet := time.Now().Add(-12 * time.Hour)
db := &acme.MockDB{ db := &acme.MockDB{
MockGetExternalAccountKeys: func(ctx context.Context, provisionerName string) ([]*acme.ExternalAccountKey, error) { MockGetExternalAccountKeys: func(ctx context.Context, provisionerID string) ([]*acme.ExternalAccountKey, error) {
assert.Equals(t, "provName", provisionerName) assert.Equals(t, "provID", provisionerID)
return []*acme.ExternalAccountKey{ return []*acme.ExternalAccountKey{
{ {
ID: "eakID1", ID: "eakID1",
Provisioner: "provName", ProvisionerID: "provID",
Reference: "some-external-key-reference", Reference: "some-external-key-reference",
KeyBytes: []byte{1, 3, 3, 7}, KeyBytes: []byte{1, 3, 3, 7},
CreatedAt: createdAt, CreatedAt: createdAt,
}, },
{ {
ID: "eakID2", ID: "eakID2",
Provisioner: "provName", ProvisionerID: "provID",
Reference: "some-other-external-key-reference", Reference: "some-other-external-key-reference",
KeyBytes: []byte{1, 3, 3, 7}, KeyBytes: []byte{1, 3, 3, 7},
CreatedAt: createdAt.Add(1 * time.Hour), CreatedAt: createdAt.Add(1 * time.Hour),
}, },
{ {
ID: "eakID3", ID: "eakID3",
Provisioner: "provName", 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,
}
},
"ok/multiple-keys-with-cursor-and-limit": func(t *testing.T) test {
chiCtx := chi.NewRouteContext()
chiCtx.URLParams.Add("prov", "provName")
req := httptest.NewRequest("GET", "/foo?cursor=eakID1&limit=10", nil)
ctx := context.WithValue(context.Background(), chi.RouteCtxKey, chiCtx)
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, provisionerName string) ([]*acme.ExternalAccountKey, error) {
assert.Equals(t, "provName", provisionerName)
return []*acme.ExternalAccountKey{
{
ID: "eakID1",
Provisioner: "provName",
Reference: "some-external-key-reference",
KeyBytes: []byte{1, 3, 3, 7},
CreatedAt: createdAt,
},
{
ID: "eakID2",
Provisioner: "provName",
Reference: "some-other-external-key-reference",
KeyBytes: []byte{1, 3, 3, 7},
CreatedAt: createdAt.Add(1 * time.Hour),
},
{
ID: "eakID3",
Provisioner: "provName",
Reference: "another-external-key-reference", Reference: "another-external-key-reference",
KeyBytes: []byte{1, 3, 3, 7}, KeyBytes: []byte{1, 3, 3, 7},
CreatedAt: createdAt, CreatedAt: createdAt,