diff --git a/pkg/server/router/did.go b/pkg/server/router/did.go index 94b32ee3b..2bc1702f0 100644 --- a/pkg/server/router/did.go +++ b/pkg/server/router/did.go @@ -152,3 +152,39 @@ func (dr DIDRouter) GetDIDByMethod(ctx context.Context, w http.ResponseWriter, _ resp := GetDIDByMethodResponse{DID: gotDID.DID} return framework.Respond(ctx, w, resp, http.StatusOK) } + +type GetDIDsByMethodResponse struct { + DIDs []didsdk.DIDDocument `json:"dids,omitempty"` +} + +// GetDIDsByMethod godoc +// @Summary Get DIDs +// @Description Get DIDs by method +// @Tags DecentralizedIdentityAPI +// @Accept json +// @Produce json +// @Param method path string true "Method" +// @Success 200 {object} GetDIDsByMethodResponse +// @Failure 400 {string} string "Bad request" +// @Router /v1/dids/{method} [get] +func (dr DIDRouter) GetDIDsByMethod(ctx context.Context, w http.ResponseWriter, _ *http.Request) error { + method := framework.GetParam(ctx, MethodParam) + if method == nil { + errMsg := "get DIDs by method request missing method parameter" + logrus.Error(errMsg) + return framework.NewRequestErrorMsg(errMsg, http.StatusBadRequest) + } + + // TODO(gabe) check if the method is supported, to tell whether this is a bad req or internal error + // TODO(gabe) differentiate between internal errors and not found DIDs + getDIDsRequest := did.GetDIDsRequest{Method: did.Method(*method)} + gotDIDs, err := dr.service.GetDIDsByMethod(getDIDsRequest) + if err != nil { + errMsg := fmt.Sprintf("could not get DIDs for method: %s", *method) + logrus.WithError(err).Error(errMsg) + return framework.NewRequestError(errors.Wrap(err, errMsg), http.StatusBadRequest) + } + + resp := GetDIDsByMethodResponse{DIDs: gotDIDs.DIDs} + return framework.Respond(ctx, w, resp, http.StatusOK) +} diff --git a/pkg/server/router/did_test.go b/pkg/server/router/did_test.go index 33c51220e..3799ed9b7 100644 --- a/pkg/server/router/did_test.go +++ b/pkg/server/router/did_test.go @@ -79,5 +79,26 @@ func TestDIDRouter(t *testing.T) { // make sure it's the same value assert.Equal(tt, createDIDResponse.DID.ID, getDIDResponse.DID.ID) + + // create a second DID + createDIDResponse2, err := didService.CreateDIDByMethod(did.CreateDIDRequest{Method: did.KeyMethod, KeyType: crypto.Ed25519}) + assert.NoError(tt, err) + assert.NotEmpty(tt, createDIDResponse2) + + // get all DIDs back + getDIDsResponse, err := didService.GetDIDsByMethod(did.GetDIDsRequest{Method: did.KeyMethod}) + assert.NoError(tt, err) + assert.NotEmpty(tt, getDIDsResponse) + assert.Len(tt, getDIDsResponse.DIDs, 2) + + knownDIDs := map[string]bool{createDIDResponse.DID.ID: true, createDIDResponse2.DID.ID: true} + for _, did := range getDIDsResponse.DIDs { + if _, ok := knownDIDs[did.ID]; !ok { + tt.Error("got unknown DID") + } else { + delete(knownDIDs, did.ID) + } + } + assert.Len(tt, knownDIDs, 0) }) } diff --git a/pkg/server/server.go b/pkg/server/server.go index b9f5c6df3..60267f29f 100644 --- a/pkg/server/server.go +++ b/pkg/server/server.go @@ -115,6 +115,7 @@ func (s *SSIServer) DecentralizedIdentityAPI(service svcframework.Service) (err s.Handle(http.MethodGet, handlerPath, didRouter.GetDIDMethods) s.Handle(http.MethodPut, path.Join(handlerPath, "/:method"), didRouter.CreateDIDByMethod) + s.Handle(http.MethodGet, path.Join(handlerPath, "/:method"), didRouter.GetDIDsByMethod) s.Handle(http.MethodGet, path.Join(handlerPath, "/:method/:id"), didRouter.GetDIDByMethod) return } diff --git a/pkg/server/server_test.go b/pkg/server/server_test.go index b2eaf5513..4dcb68b56 100644 --- a/pkg/server/server_test.go +++ b/pkg/server/server_test.go @@ -146,21 +146,25 @@ func TestDIDAPI(t *testing.T) { assert.Error(tt, err) assert.Contains(tt, err.Error(), "invalid create DID request") + // reset recorder between calls + w.Flush() + // with body, bad key type createDIDRequest := router.CreateDIDByMethodRequest{KeyType: "bad"} requestReader := newRequestValue(tt, createDIDRequest) req = httptest.NewRequest(http.MethodPut, "https://ssi-service.com/v1/dids/key", requestReader) - w = httptest.NewRecorder() err = didService.CreateDIDByMethod(newRequestContextWithParams(params), w, req) assert.Error(tt, err) assert.Contains(tt, err.Error(), "could not create DID for method with key type: bad") + // reset recorder between calls + w.Flush() + // with body, good key type createDIDRequest = router.CreateDIDByMethodRequest{KeyType: crypto.Ed25519} requestReader = newRequestValue(tt, createDIDRequest) req = httptest.NewRequest(http.MethodPut, "https://ssi-service.com/v1/dids/key", requestReader) - w = httptest.NewRecorder() err = didService.CreateDIDByMethod(newRequestContextWithParams(params), w, req) assert.NoError(tt, err) @@ -197,6 +201,9 @@ func TestDIDAPI(t *testing.T) { assert.Error(tt, err) assert.Contains(tt, err.Error(), "could not get DID for method") + // reset recorder between calls + w.Flush() + // good method, bad id badParams1 := map[string]string{ "method": "key", @@ -206,12 +213,13 @@ func TestDIDAPI(t *testing.T) { assert.Error(tt, err) assert.Contains(tt, err.Error(), "could not get DID for method with id: worse") + // reset recorder between calls + w.Flush() // store a DID createDIDRequest := router.CreateDIDByMethodRequest{KeyType: crypto.Ed25519} requestReader := newRequestValue(tt, createDIDRequest) params := map[string]string{"method": "key"} req = httptest.NewRequest(http.MethodPut, "https://ssi-service.com/v1/dids/key", requestReader) - w = httptest.NewRecorder() err = didService.CreateDIDByMethod(newRequestContextWithParams(params), w, req) assert.NoError(tt, err) @@ -220,11 +228,13 @@ func TestDIDAPI(t *testing.T) { err = json.NewDecoder(w.Body).Decode(&createdDID) assert.NoError(tt, err) + // reset recorder between calls + w.Flush() + // get it back createdID := createdDID.DID.ID getDIDPath := fmt.Sprintf("https://ssi-service.com/v1/dids/key/%s", createdID) req = httptest.NewRequest(http.MethodGet, getDIDPath, nil) - w = httptest.NewRecorder() // good params goodParams := map[string]string{ @@ -239,6 +249,94 @@ func TestDIDAPI(t *testing.T) { assert.NoError(tt, err) assert.Equal(tt, createdID, resp.DID.ID) }) + + t.Run("Test Get DIDs By Method", func(tt *testing.T) { + bolt, err := storage.NewBoltDB() + + // remove the db file after the test + tt.Cleanup(func() { + _ = bolt.Close() + _ = os.Remove(storage.DBFile) + }) + + _, keyStore := testKeyStore(tt, bolt) + didService := testDIDRouter(tt, bolt, keyStore) + + // get DIDs by method + req := httptest.NewRequest(http.MethodGet, "https://ssi-service.com/v1/dids/bad", nil) + w := httptest.NewRecorder() + + // bad params + badParams := map[string]string{ + "method": "bad", + } + err = didService.GetDIDsByMethod(newRequestContextWithParams(badParams), w, req) + assert.Error(tt, err) + assert.Contains(tt, err.Error(), "could not get DIDs for method: bad") + + // good method + goodParams := map[string]string{ + "method": "key", + } + err = didService.GetDIDsByMethod(newRequestContextWithParams(goodParams), w, req) + assert.NoError(tt, err) + var gotDIDs router.GetDIDByMethodResponse + err = json.NewDecoder(w.Body).Decode(&gotDIDs) + assert.NoError(tt, err) + assert.Empty(tt, gotDIDs) + + // reset recorder between calls + w.Flush() + + // store two DIDs + createDIDRequest := router.CreateDIDByMethodRequest{KeyType: crypto.Ed25519} + requestReader := newRequestValue(tt, createDIDRequest) + params := map[string]string{"method": "key"} + req = httptest.NewRequest(http.MethodPut, "https://ssi-service.com/v1/dids/key", requestReader) + + err = didService.CreateDIDByMethod(newRequestContextWithParams(params), w, req) + assert.NoError(tt, err) + + var createdDID router.CreateDIDByMethodResponse + err = json.NewDecoder(w.Body).Decode(&createdDID) + assert.NoError(tt, err) + + // reset recorder between calls + w.Flush() + + requestReader = newRequestValue(tt, createDIDRequest) + req = httptest.NewRequest(http.MethodPut, "https://ssi-service.com/v1/dids/key", requestReader) + + err = didService.CreateDIDByMethod(newRequestContextWithParams(params), w, req) + assert.NoError(tt, err) + + var createdDID2 router.CreateDIDByMethodResponse + err = json.NewDecoder(w.Body).Decode(&createdDID2) + assert.NoError(tt, err) + + // reset recorder between calls + w.Flush() + + // get all dids for method + + req = httptest.NewRequest(http.MethodGet, "https://ssi-service.com/v1/dids/key", requestReader) + err = didService.GetDIDsByMethod(newRequestContextWithParams(params), w, req) + assert.NoError(tt, err) + + var gotDIDsResponse router.GetDIDsByMethodResponse + err = json.NewDecoder(w.Body).Decode(&gotDIDsResponse) + assert.NoError(tt, err) + + knownDIDs := map[string]bool{createdDID.DID.ID: true, createdDID2.DID.ID: true} + for _, did := range gotDIDsResponse.DIDs { + if _, ok := knownDIDs[did.ID]; !ok { + tt.Error("got unknown DID") + } else { + delete(knownDIDs, did.ID) + } + } + assert.Len(tt, knownDIDs, 0) + }) } func TestSchemaAPI(t *testing.T) { diff --git a/pkg/service/did/did.go b/pkg/service/did/did.go index 449732fa6..3571c7d31 100644 --- a/pkg/service/did/did.go +++ b/pkg/service/did/did.go @@ -33,6 +33,7 @@ type Service struct { type MethodHandler interface { CreateDID(request CreateDIDRequest) (*CreateDIDResponse, error) GetDID(request GetDIDRequest) (*GetDIDResponse, error) + GetDIDs(method Method) (*GetDIDsResponse, error) } func NewDIDService(config config.DIDServiceConfig, s storage.ServiceStorage, keyStore *keystore.Service) (*Service, error) { @@ -116,6 +117,16 @@ func (s *Service) GetDIDByMethod(request GetDIDRequest) (*GetDIDResponse, error) return handler.GetDID(request) } +func (s *Service) GetDIDsByMethod(request GetDIDsRequest) (*GetDIDsResponse, error) { + method := request.Method + handler, err := s.getHandler(method) + if err != nil { + errMsg := fmt.Sprintf("could not get handler for method<%s>", method) + return nil, util.LoggingErrorMsg(err, errMsg) + } + return handler.GetDIDs(method) +} + func (s *Service) getHandler(method Method) (MethodHandler, error) { handler, ok := s.handlers[method] if !ok { diff --git a/pkg/service/did/key.go b/pkg/service/did/key.go index 822a55774..e56de59b5 100644 --- a/pkg/service/did/key.go +++ b/pkg/service/did/key.go @@ -87,6 +87,21 @@ func (h *keyDIDHandler) GetDID(request GetDIDRequest) (*GetDIDResponse, error) { return &GetDIDResponse{DID: gotDID.DID}, nil } +func (h *keyDIDHandler) GetDIDs(method Method) (*GetDIDsResponse, error) { + + logrus.Debugf("getting DIDs for method: %s", method) + + gotDIDs, err := h.storage.GetDIDs(string(method)) + if err != nil { + return nil, fmt.Errorf("error getting DIDs for method: %s", method) + } + var dids []did.DIDDocument + for _, did := range gotDIDs { + dids = append(dids, did.DID) + } + return &GetDIDsResponse{DIDs: dids}, nil +} + func privateKeyToBase58(privKey interface{}) (string, error) { if haveBytes, ok := privKey.([]byte); ok { return base58.Encode(haveBytes), nil diff --git a/pkg/service/did/model.go b/pkg/service/did/model.go index 8625d27df..ea49ec1b0 100644 --- a/pkg/service/did/model.go +++ b/pkg/service/did/model.go @@ -31,3 +31,12 @@ type GetDIDRequest struct { type GetDIDResponse struct { DID didsdk.DIDDocument `json:"did"` } + +type GetDIDsRequest struct { + Method Method `json:"method" validate:"required"` +} + +// GetDIDsResponse is the JSON-serializable response for getting all DIDs for a given method +type GetDIDsResponse struct { + DIDs []didsdk.DIDDocument `json:"dids"` +} diff --git a/pkg/service/did/storage/bolt.go b/pkg/service/did/storage/bolt.go index df2b9b159..2f2604428 100644 --- a/pkg/service/did/storage/bolt.go +++ b/pkg/service/did/storage/bolt.go @@ -5,6 +5,7 @@ import ( "github.com/goccy/go-json" "github.com/pkg/errors" + "github.com/sirupsen/logrus" "github.com/tbd54566975/ssi-service/internal/util" "github.com/tbd54566975/ssi-service/pkg/storage" @@ -79,8 +80,8 @@ func (b BoltDIDStorage) GetDIDs(method string) ([]StoredDID, error) { return nil, util.LoggingErrorMsg(err, couldNotGetDIDsErr) } if len(gotDIDs) == 0 { - err := fmt.Errorf("no DIDs found for method: %s", method) - return nil, util.LoggingErrorMsg(err, "could not get stored DIDs") + logrus.Infof("no DIDs found for method: %s", method) + return nil, nil } var stored []StoredDID for _, didBytes := range gotDIDs {