diff --git a/acme/db.go b/acme/db.go index a4173ce5..73029231 100644 --- a/acme/db.go +++ b/acme/db.go @@ -21,7 +21,7 @@ type DB interface { CreateExternalAccountKey(ctx context.Context, provisionerName, name 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) DeleteExternalAccountKey(ctx context.Context, provisionerName, keyID string) 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) 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) MockDeleteExternalAccountKey func(ctx context.Context, provisionerName, keyID string) 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 -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 { - return m.MockGetExternalAccountKeys(ctx, provisionerName) + return m.MockGetExternalAccountKeys(ctx, provisionerName, cursor, limit) } 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 diff --git a/acme/db/nosql/account.go b/acme/db/nosql/account.go index aca85a76..9464e9a4 100644 --- a/acme/db/nosql/account.go +++ b/acme/db/nosql/account.go @@ -259,21 +259,42 @@ func (db *DB) DeleteExternalAccountKey(ctx context.Context, provisionerName, key } // 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) 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{} - 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) 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 { 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{ ID: dbeak.ID, 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 diff --git a/acme/db/nosql/account_test.go b/acme/db/nosql/account_test.go index 4b94e40f..168e93c1 100644 --- a/acme/db/nosql/account_test.go +++ b/acme/db/nosql/account_test.go @@ -1085,13 +1085,17 @@ func TestDB_GetExternalAccountKeys(t *testing.T) { keyID1 := "keyID1" keyID2 := "keyID2" keyID3 := "keyID3" + keyID4 := "keyID4" prov := "acmeProv" ref := "ref" type test struct { - db nosql.DB - err error - acmeErr *acme.Error - eaks []*acme.ExternalAccountKey + db nosql.DB + err error + cursor string + nextCursor string + limit int + acmeErr *acme.Error + eaks []*acme.ExternalAccountKey } var tests = map[string]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 { return test{ db: &db.MockNoSQLDB{ @@ -1203,7 +1304,7 @@ func TestDB_GetExternalAccountKeys(t *testing.T) { tc := run(t) t.Run(name, func(t *testing.T) { 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) { case *acme.Error: 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.BoundAt, tc.eaks[i].BoundAt) } + assert.Equals(t, nextCursor, tc.nextCursor) } }) } diff --git a/authority/admin/api/acme.go b/authority/admin/api/acme.go index 6ca8deab..8cba39c4 100644 --- a/authority/admin/api/acme.go +++ b/authority/admin/api/acme.go @@ -146,13 +146,22 @@ func (h *Handler) GetExternalAccountKeys(w http.ResponseWriter, r *http.Request) prov := chi.URLParam(r, "prov") 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 ( - key *acme.ExternalAccountKey - keys []*acme.ExternalAccountKey - err error + key *acme.ExternalAccountKey + keys []*acme.ExternalAccountKey + 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 != "" { key, err = h.acmeDB.GetExternalAccountKeyByReference(r.Context(), prov, reference) if err != nil { @@ -161,7 +170,7 @@ func (h *Handler) GetExternalAccountKeys(w http.ResponseWriter, r *http.Request) } keys = []*acme.ExternalAccountKey{key} } else { - keys, err = h.acmeDB.GetExternalAccountKeys(r.Context(), prov) + keys, nextCursor, err = h.acmeDB.GetExternalAccountKeys(r.Context(), prov, cursor, limit) if err != nil { api.WriteError(w, admin.WrapErrorISE(err, "error getting external account keys")) return @@ -181,7 +190,6 @@ func (h *Handler) GetExternalAccountKeys(w http.ResponseWriter, r *http.Request) } } - nextCursor := "" api.JSON(w, &GetExternalAccountKeysResponse{ EAKs: eaks, NextCursor: nextCursor,