Add pagination to ACME EAB credentials endpoint

This commit is contained in:
Herman Slatman 2021-10-17 22:42:36 +02:00
parent bc5f0e429b
commit 4d726d6b4c
No known key found for this signature in database
GPG key ID: F4D8A44EA0A75A4F
4 changed files with 154 additions and 23 deletions

View file

@ -21,7 +21,7 @@ type DB interface {
CreateExternalAccountKey(ctx context.Context, provisionerName, name string) (*ExternalAccountKey, error) CreateExternalAccountKey(ctx context.Context, provisionerName, name string) (*ExternalAccountKey, error)
GetExternalAccountKey(ctx context.Context, provisionerName, keyID string) (*ExternalAccountKey, error) GetExternalAccountKey(ctx context.Context, provisionerName, keyID string) (*ExternalAccountKey, error)
GetExternalAccountKeys(ctx context.Context, provisionerName string) ([]*ExternalAccountKey, error) GetExternalAccountKeys(ctx context.Context, provisionerName, cursor string, limit int) ([]*ExternalAccountKey, string, error)
GetExternalAccountKeyByReference(ctx context.Context, provisionerName, reference string) (*ExternalAccountKey, error) GetExternalAccountKeyByReference(ctx context.Context, provisionerName, reference string) (*ExternalAccountKey, error)
DeleteExternalAccountKey(ctx context.Context, provisionerName, keyID string) error DeleteExternalAccountKey(ctx context.Context, provisionerName, keyID string) error
UpdateExternalAccountKey(ctx context.Context, provisionerName string, eak *ExternalAccountKey) error UpdateExternalAccountKey(ctx context.Context, provisionerName string, eak *ExternalAccountKey) error
@ -56,7 +56,7 @@ type MockDB struct {
MockCreateExternalAccountKey func(ctx context.Context, provisionerName, name string) (*ExternalAccountKey, error) MockCreateExternalAccountKey func(ctx context.Context, provisionerName, name string) (*ExternalAccountKey, error)
MockGetExternalAccountKey func(ctx context.Context, provisionerName, keyID string) (*ExternalAccountKey, error) MockGetExternalAccountKey func(ctx context.Context, provisionerName, keyID string) (*ExternalAccountKey, error)
MockGetExternalAccountKeys func(ctx context.Context, provisionerName string) ([]*ExternalAccountKey, error) MockGetExternalAccountKeys func(ctx context.Context, provisionerName string, cursor string, limit int) ([]*ExternalAccountKey, string, error)
MockGetExternalAccountKeyByReference func(ctx context.Context, provisionerName, reference string) (*ExternalAccountKey, error) MockGetExternalAccountKeyByReference func(ctx context.Context, provisionerName, reference string) (*ExternalAccountKey, error)
MockDeleteExternalAccountKey func(ctx context.Context, provisionerName, keyID string) error MockDeleteExternalAccountKey func(ctx context.Context, provisionerName, keyID string) error
MockUpdateExternalAccountKey func(ctx context.Context, provisionerName string, eak *ExternalAccountKey) error MockUpdateExternalAccountKey func(ctx context.Context, provisionerName string, eak *ExternalAccountKey) error
@ -145,13 +145,13 @@ 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, provisionerName, cursor string, limit int) ([]*ExternalAccountKey, string, error) {
if m.MockGetExternalAccountKeys != nil { if m.MockGetExternalAccountKeys != nil {
return m.MockGetExternalAccountKeys(ctx, provisionerName) return m.MockGetExternalAccountKeys(ctx, provisionerName, cursor, limit)
} else if m.MockError != nil { } else if m.MockError != nil {
return nil, m.MockError return nil, "", m.MockError
} }
return m.MockRet1.([]*ExternalAccountKey), m.MockError return m.MockRet1.([]*ExternalAccountKey), "", m.MockError
} }
// GetExternalAccountKeyByReference mock // GetExternalAccountKeyByReference mock

View file

@ -259,21 +259,42 @@ func (db *DB) DeleteExternalAccountKey(ctx context.Context, provisionerName, key
} }
// 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, provisionerName, cursor string, limit int) ([]*acme.ExternalAccountKey, string, error) {
entries, err := db.db.List(externalAccountKeyTable) entries, err := db.db.List(externalAccountKeyTable)
if err != nil { if err != nil {
return nil, err return nil, "", err
} }
// set sane limits; based on the Admin API limits
switch {
case limit <= 0:
limit = 20
case limit > 100:
limit = 100
}
foundCursorKey := false
keys := []*acme.ExternalAccountKey{} keys := []*acme.ExternalAccountKey{}
for _, entry := range entries { for _, entry := range entries { // entries is sorted alphabetically on the key (ID) of the EAK; no need to sort this again.
dbeak := new(dbExternalAccountKey) dbeak := new(dbExternalAccountKey)
if err = json.Unmarshal(entry.Value, dbeak); err != nil { if err = json.Unmarshal(entry.Value, dbeak); err != nil {
return nil, errors.Wrapf(err, "error unmarshaling external account key %s into ExternalAccountKey", string(entry.Key)) return nil, "", errors.Wrapf(err, "error unmarshaling external account key %s into ExternalAccountKey", string(entry.Key))
} }
if dbeak.Provisioner != provisionerName { if dbeak.Provisioner != provisionerName {
continue continue
} }
// skip the IDs not matching the cursor to look for in the sorted list.
if cursor != "" && !foundCursorKey && cursor != dbeak.ID {
continue
}
// look for the entry pointed to by the cursor (the next item to return), to start selecting items
if cursor != "" && !foundCursorKey && cursor == dbeak.ID {
foundCursorKey = true
}
// return if the limit of items was found in the previous iteration; the next cursor is set to the next item to return
if len(keys) == limit {
return keys, dbeak.ID, nil
}
keys = append(keys, &acme.ExternalAccountKey{ keys = append(keys, &acme.ExternalAccountKey{
ID: dbeak.ID, ID: dbeak.ID,
KeyBytes: dbeak.KeyBytes, KeyBytes: dbeak.KeyBytes,
@ -285,7 +306,7 @@ func (db *DB) GetExternalAccountKeys(ctx context.Context, provisionerName string
}) })
} }
return keys, nil return keys, "", nil
} }
// GetExternalAccountKeyByReference retrieves an External Account Binding key with unique reference // GetExternalAccountKeyByReference retrieves an External Account Binding key with unique reference

View file

@ -1085,13 +1085,17 @@ func TestDB_GetExternalAccountKeys(t *testing.T) {
keyID1 := "keyID1" keyID1 := "keyID1"
keyID2 := "keyID2" keyID2 := "keyID2"
keyID3 := "keyID3" keyID3 := "keyID3"
keyID4 := "keyID4"
prov := "acmeProv" prov := "acmeProv"
ref := "ref" ref := "ref"
type test struct { type test struct {
db nosql.DB db nosql.DB
err error err error
acmeErr *acme.Error cursor string
eaks []*acme.ExternalAccountKey nextCursor string
limit int
acmeErr *acme.Error
eaks []*acme.ExternalAccountKey
} }
var tests = map[string]func(t *testing.T) test{ var tests = map[string]func(t *testing.T) test{
"ok": func(t *testing.T) test { "ok": func(t *testing.T) test {
@ -1169,6 +1173,103 @@ func TestDB_GetExternalAccountKeys(t *testing.T) {
}, },
} }
}, },
"ok/paging-single-entry": func(t *testing.T) test {
now := clock.Now()
dbeak1 := &dbExternalAccountKey{
ID: keyID1,
Provisioner: prov,
Reference: ref,
AccountID: "",
KeyBytes: []byte{1, 3, 3, 7},
CreatedAt: now,
}
b1, err := json.Marshal(dbeak1)
assert.FatalError(t, err)
dbeak2 := &dbExternalAccountKey{
ID: keyID2,
Provisioner: prov,
Reference: ref,
AccountID: "",
KeyBytes: []byte{1, 3, 3, 7},
CreatedAt: now,
}
b2, err := json.Marshal(dbeak2)
assert.FatalError(t, err)
dbeak3 := &dbExternalAccountKey{
ID: keyID3,
Provisioner: "differentProvisioner",
Reference: ref,
AccountID: "",
KeyBytes: []byte{1, 3, 3, 7},
CreatedAt: now,
}
b3, err := json.Marshal(dbeak3)
assert.FatalError(t, err)
dbeak4 := &dbExternalAccountKey{
ID: keyID4,
Provisioner: prov,
Reference: ref,
AccountID: "",
KeyBytes: []byte{1, 3, 3, 7},
CreatedAt: now,
}
b4, err := json.Marshal(dbeak4)
assert.FatalError(t, err)
return test{
db: &db.MockNoSQLDB{
MList: func(bucket []byte) ([]*nosqldb.Entry, error) {
assert.Equals(t, bucket, externalAccountKeyTable)
return []*nosqldb.Entry{
{
Bucket: bucket,
Key: []byte(keyID1),
Value: b1,
},
{
Bucket: bucket,
Key: []byte(keyID2),
Value: b2,
},
{
Bucket: bucket,
Key: []byte(keyID3),
Value: b3,
},
{
Bucket: bucket,
Key: []byte(keyID4),
Value: b4,
},
}, nil
},
},
cursor: keyID2,
limit: 1,
nextCursor: keyID4,
eaks: []*acme.ExternalAccountKey{
{
ID: keyID2,
Provisioner: prov,
Reference: ref,
AccountID: "",
KeyBytes: []byte{1, 3, 3, 7},
CreatedAt: now,
},
},
}
},
"ok/paging-max-limit": func(t *testing.T) test {
return test{
db: &db.MockNoSQLDB{
MList: func(bucket []byte) ([]*nosqldb.Entry, error) {
assert.Equals(t, bucket, externalAccountKeyTable)
return []*nosqldb.Entry{}, nil
},
},
limit: 1337,
eaks: []*acme.ExternalAccountKey{},
}
},
"fail/db.List-error": func(t *testing.T) test { "fail/db.List-error": func(t *testing.T) test {
return test{ return test{
db: &db.MockNoSQLDB{ db: &db.MockNoSQLDB{
@ -1203,7 +1304,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, nextCursor, err := d.GetExternalAccountKeys(context.Background(), prov, tc.cursor, tc.limit); 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) {
@ -1229,6 +1330,7 @@ func TestDB_GetExternalAccountKeys(t *testing.T) {
assert.Equals(t, eak.AccountID, tc.eaks[i].AccountID) assert.Equals(t, eak.AccountID, tc.eaks[i].AccountID)
assert.Equals(t, eak.BoundAt, tc.eaks[i].BoundAt) assert.Equals(t, eak.BoundAt, tc.eaks[i].BoundAt)
} }
assert.Equals(t, nextCursor, tc.nextCursor)
} }
}) })
} }

View file

@ -146,13 +146,22 @@ func (h *Handler) GetExternalAccountKeys(w http.ResponseWriter, r *http.Request)
prov := chi.URLParam(r, "prov") prov := chi.URLParam(r, "prov")
reference := chi.URLParam(r, "ref") reference := chi.URLParam(r, "ref")
// TODO: support paging? It'll probably leak to the DB layer, as we have to loop through all keys
var ( var (
key *acme.ExternalAccountKey key *acme.ExternalAccountKey
keys []*acme.ExternalAccountKey keys []*acme.ExternalAccountKey
err error err error
cursor string
nextCursor string
limit int
) )
cursor, limit, err = api.ParseCursor(r)
if err != nil {
api.WriteError(w, admin.WrapError(admin.ErrorBadRequestType, err,
"error parsing cursor and limit from query params"))
return
}
if reference != "" { if reference != "" {
key, err = h.acmeDB.GetExternalAccountKeyByReference(r.Context(), prov, reference) key, err = h.acmeDB.GetExternalAccountKeyByReference(r.Context(), prov, reference)
if err != nil { if err != nil {
@ -161,7 +170,7 @@ func (h *Handler) GetExternalAccountKeys(w http.ResponseWriter, r *http.Request)
} }
keys = []*acme.ExternalAccountKey{key} keys = []*acme.ExternalAccountKey{key}
} else { } else {
keys, err = h.acmeDB.GetExternalAccountKeys(r.Context(), prov) keys, nextCursor, err = h.acmeDB.GetExternalAccountKeys(r.Context(), prov, cursor, limit)
if err != nil { if err != nil {
api.WriteError(w, admin.WrapErrorISE(err, "error getting external account keys")) api.WriteError(w, admin.WrapErrorISE(err, "error getting external account keys"))
return return
@ -181,7 +190,6 @@ func (h *Handler) GetExternalAccountKeys(w http.ResponseWriter, r *http.Request)
} }
} }
nextCursor := ""
api.JSON(w, &GetExternalAccountKeysResponse{ api.JSON(w, &GetExternalAccountKeysResponse{
EAKs: eaks, EAKs: eaks,
NextCursor: nextCursor, NextCursor: nextCursor,