diff --git a/acme/db.go b/acme/db.go index 9df0a221..bed55c85 100644 --- a/acme/db.go +++ b/acme/db.go @@ -21,7 +21,7 @@ type DB interface { CreateExternalAccountKey(ctx context.Context, provisionerName, reference string) (*ExternalAccountKey, error) GetExternalAccountKey(ctx context.Context, provisionerName, keyID string) (*ExternalAccountKey, error) - GetExternalAccountKeys(ctx context.Context, provisionerName, cursor string, limit int) ([]*ExternalAccountKey, string, error) + GetExternalAccountKeys(ctx context.Context, provisionerName string) ([]*ExternalAccountKey, 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 @@ -58,7 +58,7 @@ type MockDB struct { MockCreateExternalAccountKey func(ctx context.Context, provisionerName, reference string) (*ExternalAccountKey, error) MockGetExternalAccountKey func(ctx context.Context, provisionerName, keyID string) (*ExternalAccountKey, error) - MockGetExternalAccountKeys func(ctx context.Context, provisionerName string, cursor string, limit int) ([]*ExternalAccountKey, string, error) + MockGetExternalAccountKeys func(ctx context.Context, provisionerName string) ([]*ExternalAccountKey, 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 @@ -149,13 +149,13 @@ func (m *MockDB) GetExternalAccountKey(ctx context.Context, provisionerName, key } // GetExternalAccountKeys mock -func (m *MockDB) GetExternalAccountKeys(ctx context.Context, provisionerName, cursor string, limit int) ([]*ExternalAccountKey, string, error) { +func (m *MockDB) GetExternalAccountKeys(ctx context.Context, provisionerName string) ([]*ExternalAccountKey, error) { if m.MockGetExternalAccountKeys != nil { - return m.MockGetExternalAccountKeys(ctx, provisionerName, cursor, limit) + return m.MockGetExternalAccountKeys(ctx, provisionerName) } 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 1a611ad2..d340ec5c 100644 --- a/acme/db/nosql/account.go +++ b/acme/db/nosql/account.go @@ -258,44 +258,23 @@ 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, cursor string, limit int) ([]*acme.ExternalAccountKey, string, error) { +func (db *DB) GetExternalAccountKeys(ctx context.Context, provisionerName string) ([]*acme.ExternalAccountKey, error) { + + // TODO: lookup by provisioner based on index entries, err := db.db.List(externalAccountKeyTable) if err != nil { - return nil, "", err - } - - // set sane limits; based on the Admin API limits - switch { - case limit <= 0: - limit = 20 - case limit > 100: - limit = 100 + return nil, err } - foundCursorKey := false keys := []*acme.ExternalAccountKey{} 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 } - // look for the entry pointed to by the cursor (the next item to return) and start selecting items after finding it - if cursor != "" && !foundCursorKey { - if cursor == dbeak.ID { - // from here on, items should be selected for the result. - foundCursorKey = true - } else { - // skip the IDs not matching the cursor to look for. - continue - } - } - // 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, @@ -307,7 +286,7 @@ func (db *DB) GetExternalAccountKeys(ctx context.Context, provisionerName, curso }) } - 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 168e93c1..4b94e40f 100644 --- a/acme/db/nosql/account_test.go +++ b/acme/db/nosql/account_test.go @@ -1085,17 +1085,13 @@ 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 - cursor string - nextCursor string - limit int - acmeErr *acme.Error - eaks []*acme.ExternalAccountKey + db nosql.DB + err error + acmeErr *acme.Error + eaks []*acme.ExternalAccountKey } var tests = map[string]func(t *testing.T) test{ "ok": func(t *testing.T) test { @@ -1173,103 +1169,6 @@ 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{ @@ -1304,7 +1203,7 @@ func TestDB_GetExternalAccountKeys(t *testing.T) { tc := run(t) t.Run(name, func(t *testing.T) { d := DB{db: tc.db} - if eaks, nextCursor, err := d.GetExternalAccountKeys(context.Background(), prov, tc.cursor, tc.limit); err != nil { + if eaks, err := d.GetExternalAccountKeys(context.Background(), prov); err != nil { switch k := err.(type) { case *acme.Error: if assert.NotNil(t, tc.acmeErr) { @@ -1330,7 +1229,6 @@ 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 18959acb..dbcca15b 100644 --- a/authority/admin/api/acme.go +++ b/authority/admin/api/acme.go @@ -30,8 +30,7 @@ func (r *CreateExternalAccountKeyRequest) Validate() error { // GetExternalAccountKeysResponse is the type for GET /admin/acme/eab responses type GetExternalAccountKeysResponse struct { - EAKs []*linkedca.EABKey `json:"eaks"` - NextCursor string `json:"nextCursor"` + EAKs []*linkedca.EABKey `json:"eaks"` } // requireEABEnabled is a middleware that ensures ACME EAB is enabled @@ -149,26 +148,19 @@ func (h *Handler) DeleteExternalAccountKey(w http.ResponseWriter, r *http.Reques api.JSON(w, &DeleteResponse{Status: "ok"}) } -// GetExternalAccountKeys returns a segment of ACME EAB Keys. +// GetExternalAccountKeys returns ACME EAB Keys. If a reference is specified, +// only the ExternalAccountKey with that reference is returned. Otherwise all +// ExternalAccountKeys in the system for a specific provisioner are returned. func (h *Handler) GetExternalAccountKeys(w http.ResponseWriter, r *http.Request) { prov := chi.URLParam(r, "prov") reference := chi.URLParam(r, "ref") var ( - key *acme.ExternalAccountKey - keys []*acme.ExternalAccountKey - err error - cursor string - nextCursor string - limit int + key *acme.ExternalAccountKey + keys []*acme.ExternalAccountKey + err error ) - if cursor, limit, err = api.ParseCursor(r); err != nil { - api.WriteError(w, admin.WrapError(admin.ErrorBadRequestType, err, - "error parsing cursor and limit from query params")) - return - } - if reference != "" { if key, err = h.acmeDB.GetExternalAccountKeyByReference(r.Context(), prov, reference); err != nil { api.WriteError(w, admin.WrapErrorISE(err, "error retrieving external account key with reference '%s'", reference)) @@ -178,7 +170,7 @@ func (h *Handler) GetExternalAccountKeys(w http.ResponseWriter, r *http.Request) keys = []*acme.ExternalAccountKey{key} } } else { - if keys, nextCursor, err = h.acmeDB.GetExternalAccountKeys(r.Context(), prov, cursor, limit); err != nil { + if keys, err = h.acmeDB.GetExternalAccountKeys(r.Context(), prov); err != nil { api.WriteError(w, admin.WrapErrorISE(err, "error retrieving external account keys")) return } @@ -198,7 +190,6 @@ func (h *Handler) GetExternalAccountKeys(w http.ResponseWriter, r *http.Request) } api.JSON(w, &GetExternalAccountKeysResponse{ - EAKs: eaks, - NextCursor: nextCursor, + EAKs: eaks, }) } diff --git a/authority/admin/api/acme_test.go b/authority/admin/api/acme_test.go index ba956f21..7162ea98 100644 --- a/authority/admin/api/acme_test.go +++ b/authority/admin/api/acme_test.go @@ -840,23 +840,6 @@ func TestHandler_GetExternalAccountKeys(t *testing.T) { err *admin.Error } var tests = map[string]func(t *testing.T) test{ - "fail/parse-cursor": func(t *testing.T) test { - chiCtx := chi.NewRouteContext() - chiCtx.URLParams.Add("prov", "provName") - req := httptest.NewRequest("GET", "/foo?limit=A", nil) - ctx := context.WithValue(context.Background(), chi.RouteCtxKey, chiCtx) - return test{ - ctx: ctx, - statusCode: 400, - req: req, - err: &admin.Error{ - Status: 400, - Type: admin.ErrorBadRequestType.String(), - Detail: "bad request", - Message: "error parsing cursor and limit from query params: limit 'A' is not an integer: strconv.Atoi: parsing \"A\": invalid syntax", - }, - } - }, "fail/acmeDB.GetExternalAccountKeyByReference": func(t *testing.T) test { chiCtx := chi.NewRouteContext() chiCtx.URLParams.Add("prov", "provName") @@ -889,11 +872,9 @@ func TestHandler_GetExternalAccountKeys(t *testing.T) { req := httptest.NewRequest("GET", "/foo", nil) ctx := context.WithValue(context.Background(), chi.RouteCtxKey, chiCtx) db := &acme.MockDB{ - MockGetExternalAccountKeys: func(ctx context.Context, provisionerName, cursor string, limit int) ([]*acme.ExternalAccountKey, string, error) { + MockGetExternalAccountKeys: func(ctx context.Context, provisionerName string) ([]*acme.ExternalAccountKey, error) { assert.Equals(t, "provName", provisionerName) - assert.Equals(t, "", cursor) - assert.Equals(t, 0, limit) - return nil, "", errors.New("force") + return nil, errors.New("force") }, } return test{ @@ -927,8 +908,7 @@ func TestHandler_GetExternalAccountKeys(t *testing.T) { statusCode: 200, req: req, resp: GetExternalAccountKeysResponse{ - EAKs: []*linkedca.EABKey{}, - NextCursor: "", + EAKs: []*linkedca.EABKey{}, }, db: db, err: nil, @@ -968,7 +948,6 @@ func TestHandler_GetExternalAccountKeys(t *testing.T) { BoundAt: timestamppb.New(boundAt), }, }, - NextCursor: "", }, db: db, err: nil, @@ -983,10 +962,8 @@ func TestHandler_GetExternalAccountKeys(t *testing.T) { var boundAt time.Time boundAtSet := time.Now().Add(-12 * time.Hour) db := &acme.MockDB{ - MockGetExternalAccountKeys: func(ctx context.Context, provisionerName, cursor string, limit int) ([]*acme.ExternalAccountKey, string, error) { + MockGetExternalAccountKeys: func(ctx context.Context, provisionerName string) ([]*acme.ExternalAccountKey, error) { assert.Equals(t, "provName", provisionerName) - assert.Equals(t, "", cursor) - assert.Equals(t, 0, limit) return []*acme.ExternalAccountKey{ { ID: "eakID1", @@ -1011,7 +988,7 @@ func TestHandler_GetExternalAccountKeys(t *testing.T) { BoundAt: boundAtSet, AccountID: "accountID", }, - }, "nextCursorValue", nil + }, nil }, } return test{ @@ -1043,7 +1020,6 @@ func TestHandler_GetExternalAccountKeys(t *testing.T) { Account: "accountID", }, }, - NextCursor: "nextCursorValue", }, db: db, err: nil, @@ -1058,10 +1034,8 @@ func TestHandler_GetExternalAccountKeys(t *testing.T) { var boundAt time.Time boundAtSet := time.Now().Add(-12 * time.Hour) db := &acme.MockDB{ - MockGetExternalAccountKeys: func(ctx context.Context, provisionerName, cursor string, limit int) ([]*acme.ExternalAccountKey, string, error) { + MockGetExternalAccountKeys: func(ctx context.Context, provisionerName string) ([]*acme.ExternalAccountKey, error) { assert.Equals(t, "provName", provisionerName) - assert.Equals(t, "eakID1", cursor) - assert.Equals(t, 10, limit) return []*acme.ExternalAccountKey{ { ID: "eakID1", @@ -1086,7 +1060,7 @@ func TestHandler_GetExternalAccountKeys(t *testing.T) { BoundAt: boundAtSet, AccountID: "accountID", }, - }, "eakID4", nil + }, nil }, } return test{ @@ -1118,7 +1092,6 @@ func TestHandler_GetExternalAccountKeys(t *testing.T) { Account: "accountID", }, }, - NextCursor: "eakID4", }, db: db, err: nil, @@ -1166,7 +1139,6 @@ func TestHandler_GetExternalAccountKeys(t *testing.T) { if !cmp.Equal(tc.resp, response, opts...) { t.Errorf("h.GetExternalAccountKeys diff =\n%s", cmp.Diff(tc.resp, response, opts...)) } - }) } } diff --git a/ca/adminClient.go b/ca/adminClient.go index f5e90449..ab7c3bbb 100644 --- a/ca/adminClient.go +++ b/ca/adminClient.go @@ -560,7 +560,7 @@ retry: return nil } -// GetExternalAccountKeysPaginate returns a page from the the GET /admin/acme/eab request to the CA. +// GetExternalAccountKeysPaginate returns a page from the GET /admin/acme/eab request to the CA. func (c *AdminClient) GetExternalAccountKeysPaginate(provisionerName, reference string, opts ...AdminOption) (*adminAPI.GetExternalAccountKeysResponse, error) { var retried bool o := new(adminOptions) @@ -669,21 +669,45 @@ retry: // GetExternalAccountKeys returns all ACME EAB Keys from the GET /admin/acme/eab request to the CA. func (c *AdminClient) GetExternalAccountKeys(provisionerName, reference string, opts ...AdminOption) ([]*linkedca.EABKey, error) { - var ( - cursor = "" - eaks = []*linkedca.EABKey{} - ) - for { - resp, err := c.GetExternalAccountKeysPaginate(provisionerName, reference, WithAdminCursor(cursor), WithAdminLimit(100)) - if err != nil { - return nil, err - } - eaks = append(eaks, resp.EAKs...) - if resp.NextCursor == "" { - return eaks, nil + var retried bool + o := new(adminOptions) + if err := o.apply(opts); err != nil { + return nil, err + } + p := path.Join(adminURLPrefix, "acme/eab", provisionerName) + if reference != "" { + p = path.Join(p, "/", reference) + } + u := c.endpoint.ResolveReference(&url.URL{ + Path: p, + RawQuery: o.rawQuery(), + }) + tok, err := c.generateAdminToken(u.Path) + if err != nil { + return nil, errors.Wrapf(err, "error generating admin token") + } + req, err := http.NewRequest("GET", u.String(), http.NoBody) + if err != nil { + return nil, errors.Wrapf(err, "create GET %s request failed", u) + } + req.Header.Add("Authorization", tok) +retry: + resp, err := c.client.Do(req) + if err != nil { + return nil, errors.Wrapf(err, "client GET %s failed", u) + } + if resp.StatusCode >= 400 { + if !retried && c.retryOnError(resp) { + retried = true + goto retry } - cursor = resp.NextCursor + return nil, readAdminError(resp.Body) + } + var body = new(adminAPI.GetExternalAccountKeysResponse) + if err := readJSON(resp.Body, body); err != nil { + return nil, errors.Wrapf(err, "error reading %s", u) } + return body.EAKs, nil } func readAdminError(r io.ReadCloser) error {