From 62b812c1148b7c4adec03de855b1b9a713325ae1 Mon Sep 17 00:00:00 2001 From: nitro-neal Date: Fri, 9 Sep 2022 18:31:36 -0500 Subject: [PATCH] SIP 2 - Create Credential Manifests CRUD (#91) * wip * adding complete manifest crud route * cleaning up test * changing model to have concrete definition instead of mapping Co-authored-by: Neal Roessler Co-authored-by: Gabe --- config/config.go | 5 + go.mod | 2 + go.sum | 1 + pkg/server/router/manifest.go | 184 +++++++++++++++ pkg/server/router/manifest_test.go | 95 ++++++++ pkg/server/router/router_test.go | 1 + pkg/server/router/schema.go | 6 +- pkg/server/router/schema_test.go | 4 +- pkg/server/server.go | 20 +- pkg/server/server_test.go | 284 +++++++++++++++++++++++- pkg/service/framework/framework.go | 1 + pkg/service/manifest/manifest.go | 165 ++++++++++++++ pkg/service/manifest/model.go | 38 ++++ pkg/service/manifest/storage/bolt.go | 92 ++++++++ pkg/service/manifest/storage/storage.go | 42 ++++ pkg/service/schema/model.go | 12 +- pkg/service/schema/schema.go | 4 +- pkg/service/service.go | 9 +- 18 files changed, 947 insertions(+), 18 deletions(-) create mode 100644 pkg/server/router/manifest.go create mode 100644 pkg/server/router/manifest_test.go create mode 100644 pkg/service/manifest/manifest.go create mode 100644 pkg/service/manifest/model.go create mode 100644 pkg/service/manifest/storage/bolt.go create mode 100644 pkg/service/manifest/storage/storage.go diff --git a/config/config.go b/config/config.go index 3e8563178..d2b26fe7c 100644 --- a/config/config.go +++ b/config/config.go @@ -52,6 +52,7 @@ type ServicesConfig struct { SchemaConfig SchemaServiceConfig `toml:"schema,omitempty"` CredentialConfig CredentialServiceConfig `toml:"credential,omitempty"` KeyStoreConfig KeyStoreServiceConfig `toml:"keystore,omitempty"` + ManifestConfig ManifestServiceConfig `toml:"manifest,omitempty"` } // BaseServiceConfig represents configurable properties for a specific component of the SSI Service @@ -88,6 +89,10 @@ type CredentialServiceConfig struct { // TODO(gabe) supported key and signature types } +type ManifestServiceConfig struct { + *BaseServiceConfig +} + type KeyStoreServiceConfig struct { *BaseServiceConfig // Service key password. Used by a KDF whose key is used by a symmetric cypher for key encryption. diff --git a/go.mod b/go.mod index d8dfe1368..176345f94 100644 --- a/go.mod +++ b/go.mod @@ -24,6 +24,8 @@ require ( require go.etcd.io/bbolt v1.3.6 +require github.com/oliveagle/jsonpath v0.0.0-20180606110733-2e52cf6e6852 // indirect + require ( github.com/davecgh/go-spew v1.1.1 // indirect github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1 // indirect diff --git a/go.sum b/go.sum index b8df68db6..bde0148d9 100644 --- a/go.sum +++ b/go.sum @@ -75,6 +75,7 @@ github.com/multiformats/go-multicodec v0.5.0 h1:EgU6cBe/D7WRwQb1KmnBvU7lrcFGMggZ github.com/multiformats/go-multicodec v0.5.0/go.mod h1:DiY2HFaEp5EhEXb/iYzVAunmyX/aSFMxq2KMKfWEues= github.com/multiformats/go-varint v0.0.6 h1:gk85QWKxh3TazbLxED/NlDVv8+q+ReFJk7Y2W/KhfNY= github.com/multiformats/go-varint v0.0.6/go.mod h1:3Ls8CIEsrijN6+B7PbrXRPxHRPuXSrVKRY101jdMZYE= +github.com/oliveagle/jsonpath v0.0.0-20180606110733-2e52cf6e6852 h1:Yl0tPBa8QPjGmesFh1D0rDy+q1Twx6FyU7VWHi8wZbI= github.com/oliveagle/jsonpath v0.0.0-20180606110733-2e52cf6e6852/go.mod h1:eqOVx5Vwu4gd2mmMZvVZsgIqNSaW3xxRThUJ0k/TPk4= github.com/piprate/json-gold v0.4.1 h1:JYbYN36n6YcAYipKy3ttv3X2HDQPeqWqmwta35NPj04= github.com/piprate/json-gold v0.4.1/go.mod h1:OK1z7UgtBZk06n2cDE2OSq1kffmjFFp5/2yhLLCz9UM= diff --git a/pkg/server/router/manifest.go b/pkg/server/router/manifest.go new file mode 100644 index 000000000..6a0589285 --- /dev/null +++ b/pkg/server/router/manifest.go @@ -0,0 +1,184 @@ +package router + +import ( + "context" + "fmt" + "github.com/TBD54566975/ssi-sdk/credential/exchange" + manifestsdk "github.com/TBD54566975/ssi-sdk/credential/manifest" + "github.com/tbd54566975/ssi-service/pkg/service/manifest" + "net/http" + + "github.com/pkg/errors" + "github.com/sirupsen/logrus" + + "github.com/tbd54566975/ssi-service/pkg/server/framework" + svcframework "github.com/tbd54566975/ssi-service/pkg/service/framework" +) + +type ManifestRouter struct { + service *manifest.Service +} + +func NewManifestRouter(s svcframework.Service) (*ManifestRouter, error) { + if s == nil { + return nil, errors.New("service cannot be nil") + } + credService, ok := s.(*manifest.Service) + if !ok { + return nil, fmt.Errorf("could not create manifest router with service type: %s", s.Type()) + } + return &ManifestRouter{ + service: credService, + }, nil +} + +type CreateManifestRequest struct { + Issuer string `json:"issuer" validate:"required"` + // A context is optional. If not present, we'll apply default, required context values. + Context string `json:"@context"` + OutputDescriptors []manifestsdk.OutputDescriptor `json:"outputDescriptors" validate:"required"` + PresentationDefinition exchange.PresentationDefinition `json:"presentationDefinition" validate:"required"` +} + +func (c CreateManifestRequest) ToServiceRequest() manifest.CreateManifestRequest { + return manifest.CreateManifestRequest{ + Issuer: c.Issuer, + Context: c.Context, + OutputDescriptors: c.OutputDescriptors, + PresentationDefinition: c.PresentationDefinition, + } +} + +type CreateManifestResponse struct { + Manifest manifestsdk.CredentialManifest `json:"manifest"` +} + +// CreateManifest godoc +// @Summary Create manifest +// @Description Create manifest +// @Tags ManifestAPI +// @Accept json +// @Produce json +// @Param request body CreateManifestRequest true "request body" +// @Success 201 {object} CreateManifestResponse +// @Failure 400 {string} string "Bad request" +// @Failure 500 {string} string "Internal server error" +// @Router /v1/manifests [put] +func (mr ManifestRouter) CreateManifest(ctx context.Context, w http.ResponseWriter, r *http.Request) error { + var request CreateManifestRequest + if err := framework.Decode(r, &request); err != nil { + errMsg := "invalid create manifest request" + logrus.WithError(err).Error(errMsg) + return framework.NewRequestError(errors.Wrap(err, errMsg), http.StatusBadRequest) + } + + req := request.ToServiceRequest() + createManifestResponse, err := mr.service.CreateManifest(req) + if err != nil { + errMsg := "could not create manifest" + logrus.WithError(err).Error(errMsg) + return framework.NewRequestError(errors.Wrap(err, errMsg), http.StatusInternalServerError) + } + + resp := CreateManifestResponse{Manifest: createManifestResponse.Manifest} + + return framework.Respond(ctx, w, resp, http.StatusCreated) +} + +type GetManifestResponse struct { + ID string `json:"id"` + Manifest manifestsdk.CredentialManifest `json:"manifest"` +} + +// GetManifest godoc +// @Summary Get manifest +// @Description Get manifest by id +// @Tags ManifestAPI +// @Accept json +// @Produce json +// @Param id path string true "ID" +// @Success 200 {object} GetManifestResponse +// @Failure 400 {string} string "Bad request" +// @Router /v1/manifests/{id} [get] +func (mr ManifestRouter) GetManifest(ctx context.Context, w http.ResponseWriter, r *http.Request) error { + id := framework.GetParam(ctx, IDParam) + if id == nil { + errMsg := "cannot get manifest without ID parameter" + logrus.Error(errMsg) + return framework.NewRequestErrorMsg(errMsg, http.StatusBadRequest) + } + + gotManifest, err := mr.service.GetManifest(manifest.GetManifestRequest{ID: *id}) + if err != nil { + errMsg := fmt.Sprintf("could not get manifest with id: %s", *id) + logrus.WithError(err).Error(errMsg) + return framework.NewRequestError(errors.Wrap(err, errMsg), http.StatusBadRequest) + } + + resp := GetManifestResponse{ + ID: gotManifest.Manifest.ID, + Manifest: gotManifest.Manifest, + } + return framework.Respond(ctx, w, resp, http.StatusOK) +} + +type GetManifestsResponse struct { + Manifests []manifestsdk.CredentialManifest `json:"manifests"` +} + +// GetManifests godoc +// @Summary Get manifests +// @Description Checks for the presence of a query parameter and calls the associated filtered get method +// @Tags ManifestAPI +// @Accept json +// @Produce json +// @Param issuer query string false "string issuer" +// @Param schema query string false "string schema" +// @Param subject query string false "string subject" +// @Success 200 {object} GetManifestsResponse +// @Failure 400 {string} string "Bad request" +// @Failure 500 {string} string "Internal server error" +// @Router /v1/manifests [get] +func (mr ManifestRouter) GetManifests(ctx context.Context, w http.ResponseWriter, r *http.Request) error { + gotManifests, err := mr.service.GetManifests() + + if err != nil { + errMsg := fmt.Sprintf("could not get manifests") + logrus.WithError(err).Error(errMsg) + return framework.NewRequestError(errors.Wrap(err, errMsg), http.StatusBadRequest) + } + + resp := GetManifestsResponse{ + Manifests: gotManifests.Manifests, + } + + return framework.Respond(ctx, w, resp, http.StatusOK) +} + +// DeleteManifest godoc +// @Summary Delete manifests +// @Description Delete manifest by ID +// @Tags ManifestAPI +// @Accept json +// @Produce json +// @Param id path string true "ID" +// @Success 200 {string} string "OK" +// @Failure 400 {string} string "Bad request" +// @Failure 500 {string} string "Internal server error" +// @Router /v1/manifests/{id} [delete] +func (mr ManifestRouter) DeleteManifest(ctx context.Context, w http.ResponseWriter, r *http.Request) error { + id := framework.GetParam(ctx, IDParam) + if id == nil { + errMsg := "cannot delete manifest without ID parameter" + logrus.Error(errMsg) + return framework.NewRequestErrorMsg(errMsg, http.StatusBadRequest) + } + + if err := mr.service.DeleteManifest(manifest.DeleteManifestRequest{ID: *id}); err != nil { + errMsg := fmt.Sprintf("could not delete manifest with id: %s", *id) + logrus.WithError(err).Error(errMsg) + return framework.NewRequestError(errors.Wrap(err, errMsg), http.StatusInternalServerError) + } + + return framework.Respond(ctx, w, nil, http.StatusOK) +} diff --git a/pkg/server/router/manifest_test.go b/pkg/server/router/manifest_test.go new file mode 100644 index 000000000..57b564b54 --- /dev/null +++ b/pkg/server/router/manifest_test.go @@ -0,0 +1,95 @@ +package router + +import ( + "github.com/TBD54566975/ssi-sdk/credential/exchange" + manifestsdk "github.com/TBD54566975/ssi-sdk/credential/manifest" + "github.com/stretchr/testify/assert" + "github.com/tbd54566975/ssi-service/config" + "github.com/tbd54566975/ssi-service/pkg/service/framework" + "github.com/tbd54566975/ssi-service/pkg/service/manifest" + "github.com/tbd54566975/ssi-service/pkg/storage" + "os" + "testing" +) + +func TestManifestRouter(t *testing.T) { + // remove the db file after the test + t.Cleanup(func() { + _ = os.Remove(storage.DBFile) + }) + + t.Run("Nil Service", func(tt *testing.T) { + manifestRouter, err := NewManifestRouter(nil) + assert.Error(tt, err) + assert.Empty(tt, manifestRouter) + assert.Contains(tt, err.Error(), "service cannot be nil") + }) + + t.Run("Bad Service", func(tt *testing.T) { + manifestRouter, err := NewManifestRouter(&testService{}) + assert.Error(tt, err) + assert.Empty(tt, manifestRouter) + assert.Contains(tt, err.Error(), "could not create manifest router with service type: test") + }) + + t.Run("Manifest Service Test", func(tt *testing.T) { + bolt, err := storage.NewBoltDB() + assert.NoError(tt, err) + assert.NotEmpty(tt, bolt) + + serviceConfig := config.ManifestServiceConfig{BaseServiceConfig: &config.BaseServiceConfig{Name: "manifest"}} + manifestService, err := manifest.NewManifestService(serviceConfig, bolt) + assert.NoError(tt, err) + assert.NotEmpty(tt, manifestService) + + // check type and status + assert.Equal(tt, framework.Manifest, manifestService.Type()) + assert.Equal(tt, framework.StatusReady, manifestService.Status().Status) + + // good request + createManifestRequest := getValidManifestRequest() + + createdManifest, err := manifestService.CreateManifest(createManifestRequest) + assert.NoError(tt, err) + assert.NotEmpty(tt, createdManifest) + assert.NotEmpty(tt, createdManifest.Manifest) + }) +} + +func getValidManifestRequest() manifest.CreateManifestRequest { + createManifestRequest := manifest.CreateManifestRequest{ + Issuer: "did:abc:123", + Context: "context123", + PresentationDefinition: exchange.PresentationDefinition{ + ID: "pres-def-id", + InputDescriptors: []exchange.InputDescriptor{ + { + ID: "test-id", + Constraints: &exchange.Constraints{ + Fields: []exchange.Field{ + { + Path: []string{".vc.id"}, + }, + }, + }, + }, + }, + }, + OutputDescriptors: []manifestsdk.OutputDescriptor{ + { + ID: "id1", + Schema: "https://test.com/schema", + Name: "good ID", + Description: "it's all good", + }, + { + ID: "id2", + Schema: "https://test.com/schema", + Name: "good ID", + Description: "it's all good", + }, + }, + } + + return createManifestRequest +} diff --git a/pkg/server/router/router_test.go b/pkg/server/router/router_test.go index 5d29aaa43..7af4d714b 100644 --- a/pkg/server/router/router_test.go +++ b/pkg/server/router/router_test.go @@ -25,5 +25,6 @@ func (s *testService) Config() config.ServicesConfig { SchemaConfig: config.SchemaServiceConfig{}, CredentialConfig: config.CredentialServiceConfig{}, KeyStoreConfig: config.KeyStoreServiceConfig{}, + ManifestConfig: config.ManifestServiceConfig{}, } } diff --git a/pkg/server/router/schema.go b/pkg/server/router/schema.go index a5093e6d6..29020bdbf 100644 --- a/pkg/server/router/schema.go +++ b/pkg/server/router/schema.go @@ -100,7 +100,7 @@ type GetSchemaResponse struct { Schema schemalib.VCJSONSchema `json:"schema,omitempty"` } -// GetSchemaByID godoc +// GetSchema godoc // @Summary Get Schema // @Description Get schema by ID // @Tags SchemaAPI @@ -110,7 +110,7 @@ type GetSchemaResponse struct { // @Success 200 {object} GetSchemaResponse // @Failure 400 {string} string "Bad request" // @Router /v1/schemas/{id} [get] -func (sr SchemaRouter) GetSchemaByID(ctx context.Context, w http.ResponseWriter, r *http.Request) error { +func (sr SchemaRouter) GetSchema(ctx context.Context, w http.ResponseWriter, r *http.Request) error { id := framework.GetParam(ctx, IDParam) if id == nil { errMsg := "cannot get schema without ID parameter" @@ -119,7 +119,7 @@ func (sr SchemaRouter) GetSchemaByID(ctx context.Context, w http.ResponseWriter, } // TODO(gabe) differentiate between internal errors and not found schemas - gotSchema, err := sr.service.GetSchemaByID(schema.GetSchemaByIDRequest{ID: *id}) + gotSchema, err := sr.service.GetSchema(schema.GetSchemaRequest{ID: *id}) if err != nil { errMsg := fmt.Sprintf("could not get schema with id: %s", *id) logrus.WithError(err).Error(errMsg) diff --git a/pkg/server/router/schema_test.go b/pkg/server/router/schema_test.go index 5c9aaf328..4c7151e39 100644 --- a/pkg/server/router/schema_test.go +++ b/pkg/server/router/schema_test.go @@ -52,7 +52,7 @@ func TestSchemaRouter(t *testing.T) { assert.Equal(tt, 0, len(gotSchemas.Schemas)) // get schema that doesn't exist - _, err = schemaService.GetSchemaByID(schema.GetSchemaByIDRequest{ID: "bad"}) + _, err = schemaService.GetSchema(schema.GetSchemaRequest{ID: "bad"}) assert.Error(tt, err) assert.Contains(tt, err.Error(), "error getting schema") @@ -75,7 +75,7 @@ func TestSchemaRouter(t *testing.T) { assert.Equal(tt, "simple schema", createdSchema.Schema.Name) // get schema by ID - gotSchema, err := schemaService.GetSchemaByID(schema.GetSchemaByIDRequest{ID: createdSchema.ID}) + gotSchema, err := schemaService.GetSchema(schema.GetSchemaRequest{ID: createdSchema.ID}) assert.NoError(tt, err) assert.NotEmpty(tt, gotSchema) assert.EqualValues(tt, createdSchema.Schema, gotSchema.Schema) diff --git a/pkg/server/server.go b/pkg/server/server.go index 8c5979892..96bda5ee3 100644 --- a/pkg/server/server.go +++ b/pkg/server/server.go @@ -26,6 +26,7 @@ const ( DIDsPrefix = "/dids" SchemasPrefix = "/schemas" CredentialsPrefix = "/credentials" + ManifestsPrefix = "/manifests" KeyStorePrefix = "/keys" ) @@ -91,6 +92,8 @@ func (s *SSIServer) instantiateRouter(service svcframework.Service) error { return s.CredentialAPI(service) case svcframework.KeyStore: return s.KeyStoreAPI(service) + case svcframework.Manifest: + return s.ManifestAPI(service) default: return fmt.Errorf("could not instantiate API for service: %s", serviceType) } @@ -122,7 +125,7 @@ func (s *SSIServer) SchemaAPI(service svcframework.Service) (err error) { s.Handle(http.MethodPut, handlerPath, schemaRouter.CreateSchema) s.Handle(http.MethodGet, handlerPath, schemaRouter.GetSchemas) - s.Handle(http.MethodGet, path.Join(handlerPath, "/:id"), schemaRouter.GetSchemaByID) + s.Handle(http.MethodGet, path.Join(handlerPath, "/:id"), schemaRouter.GetSchema) return } @@ -153,3 +156,18 @@ func (s *SSIServer) KeyStoreAPI(service svcframework.Service) (err error) { s.Handle(http.MethodGet, path.Join(handlerPath, "/:id"), keyStoreRouter.GetKeyDetails) return } + +func (s *SSIServer) ManifestAPI(service svcframework.Service) (err error) { + manifestRouter, err := router.NewManifestRouter(service) + if err != nil { + return util.LoggingErrorMsg(err, "could not create manifest router") + } + + handlerPath := V1Prefix + ManifestsPrefix + + s.Handle(http.MethodPut, handlerPath, manifestRouter.CreateManifest) + s.Handle(http.MethodGet, handlerPath, manifestRouter.GetManifests) + s.Handle(http.MethodGet, path.Join(handlerPath, "/:id"), manifestRouter.GetManifest) + s.Handle(http.MethodDelete, path.Join(handlerPath, "/:id"), manifestRouter.DeleteManifest) + return +} diff --git a/pkg/server/server_test.go b/pkg/server/server_test.go index 2966d896e..6854dc80f 100644 --- a/pkg/server/server_test.go +++ b/pkg/server/server_test.go @@ -4,6 +4,8 @@ import ( "bytes" "context" "fmt" + "github.com/TBD54566975/ssi-sdk/credential/exchange" + "io" "net/http" "net/http/httptest" @@ -12,6 +14,7 @@ import ( "time" credsdk "github.com/TBD54566975/ssi-sdk/credential" + manifestsdk "github.com/TBD54566975/ssi-sdk/credential/manifest" "github.com/TBD54566975/ssi-sdk/crypto" "github.com/dimfeld/httptreemux/v5" "github.com/goccy/go-json" @@ -27,6 +30,7 @@ import ( "github.com/tbd54566975/ssi-service/pkg/service/did" svcframework "github.com/tbd54566975/ssi-service/pkg/service/framework" "github.com/tbd54566975/ssi-service/pkg/service/keystore" + "github.com/tbd54566975/ssi-service/pkg/service/manifest" "github.com/tbd54566975/ssi-service/pkg/service/schema" "github.com/tbd54566975/ssi-service/pkg/storage" ) @@ -309,7 +313,7 @@ func TestSchemaAPI(t *testing.T) { // get schema that doesn't exist w := httptest.NewRecorder() req := httptest.NewRequest(http.MethodGet, "https://ssi-service.com/v1/schemas/bad", nil) - err = schemaService.GetSchemaByID(newRequestContext(), w, req) + err = schemaService.GetSchema(newRequestContext(), w, req) assert.Error(tt, err) assert.Contains(tt, err.Error(), "cannot get schema without ID parameter") @@ -318,7 +322,7 @@ func TestSchemaAPI(t *testing.T) { // get schema with invalid id req = httptest.NewRequest(http.MethodGet, "https://ssi-service.com/v1/schemas/bad", nil) - err = schemaService.GetSchemaByID(newRequestContextWithParams(map[string]string{"id": "bad"}), w, req) + err = schemaService.GetSchema(newRequestContextWithParams(map[string]string{"id": "bad"}), w, req) assert.Error(tt, err) assert.Contains(tt, err.Error(), "could not get schema with id: bad") @@ -367,7 +371,7 @@ func TestSchemaAPI(t *testing.T) { // get it back req = httptest.NewRequest(http.MethodGet, fmt.Sprintf("https://ssi-service.com/v1/schemas/%s", createResp.ID), nil) - err = schemaService.GetSchemaByID(newRequestContextWithParams(map[string]string{"id": createResp.ID}), w, req) + err = schemaService.GetSchema(newRequestContextWithParams(map[string]string{"id": createResp.ID}), w, req) assert.NoError(tt, err) var gotSchemaResp router.GetSchemaResponse @@ -732,6 +736,242 @@ func newCredentialService(t *testing.T, bolt *storage.BoltDB) *router.Credential return credentialRouter } +func TestManifestAPI(t *testing.T) { + t.Run("Test Create Manifest", func(tt *testing.T) { + bolt, err := storage.NewBoltDB() + + // remove the db file after the test + tt.Cleanup(func() { + _ = bolt.Close() + _ = os.Remove(storage.DBFile) + }) + + manifestService := newManifestService(tt, bolt) + + // missing required field: OutputDescriptors + badManifestRequest := router.CreateManifestRequest{ + Issuer: "did:abc:123", + } + + badRequestValue := newRequestValue(tt, badManifestRequest) + req := httptest.NewRequest(http.MethodPut, "https://ssi-service.com/v1/manifests", badRequestValue) + w := httptest.NewRecorder() + + err = manifestService.CreateManifest(newRequestContext(), w, req) + assert.Error(tt, err) + assert.Contains(tt, err.Error(), "invalid create manifest request") + + // reset the http recorder + w.Flush() + + // good request + createManifestRequest := router.CreateManifestRequest{ + Issuer: "did:abc:123", + Context: "context123", + PresentationDefinition: exchange.PresentationDefinition{ + ID: "pres-def-id", + InputDescriptors: []exchange.InputDescriptor{ + { + ID: "test-id", + Constraints: &exchange.Constraints{ + Fields: []exchange.Field{ + { + Path: []string{".vc.id"}, + }, + }, + }, + }, + }, + }, + OutputDescriptors: []manifestsdk.OutputDescriptor{ + { + ID: "id1", + Schema: "https://test.com/schema", + Name: "good ID", + Description: "it's all good", + }, + { + ID: "id2", + Schema: "https://test.com/schema", + Name: "good ID", + Description: "it's all good", + }, + }, + } + + requestValue := newRequestValue(tt, createManifestRequest) + req = httptest.NewRequest(http.MethodPut, "https://ssi-service.com/v1/manifests", requestValue) + err = manifestService.CreateManifest(newRequestContext(), w, req) + assert.NoError(tt, err) + + var resp router.CreateManifestResponse + err = json.NewDecoder(w.Body).Decode(&resp) + assert.NoError(tt, err) + + assert.NotEmpty(tt, resp.Manifest) + assert.Equal(tt, resp.Manifest.Issuer.ID, "did:abc:123") + }) + + t.Run("Test Get Manifest By ID", func(tt *testing.T) { + bolt, err := storage.NewBoltDB() + + // remove the db file after the test + tt.Cleanup(func() { + _ = bolt.Close() + _ = os.Remove(storage.DBFile) + }) + + manifestService := newManifestService(tt, bolt) + + w := httptest.NewRecorder() + + // get a manifest that doesn't exit + req := httptest.NewRequest(http.MethodGet, "https://ssi-service.com/v1/manifests/bad", nil) + err = manifestService.GetManifest(newRequestContext(), w, req) + assert.Error(tt, err) + assert.Contains(tt, err.Error(), "cannot get manifest without ID parameter") + + // reset recorder between calls + w.Flush() + + // get a manifest with an invalid id parameter + req = httptest.NewRequest(http.MethodGet, "https://ssi-service.com/v1/manifests/bad", nil) + err = manifestService.GetManifest(newRequestContextWithParams(map[string]string{"id": "bad"}), w, req) + assert.Error(tt, err) + assert.Contains(tt, err.Error(), "could not get manifest with id: bad") + + // reset recorder between calls + w.Flush() + + // good request + createManifestRequest := getValidManifestRequest() + + requestValue := newRequestValue(tt, createManifestRequest) + req = httptest.NewRequest(http.MethodPut, "https://ssi-service.com/v1/manifests", requestValue) + err = manifestService.CreateManifest(newRequestContext(), w, req) + assert.NoError(tt, err) + + var resp router.CreateManifestResponse + err = json.NewDecoder(w.Body).Decode(&resp) + assert.NoError(tt, err) + + // get manifest by id + req = httptest.NewRequest(http.MethodGet, fmt.Sprintf("https://ssi-service.com/v1/manifests/%s", resp.Manifest.ID), nil) + err = manifestService.GetManifest(newRequestContextWithParams(map[string]string{"id": resp.Manifest.ID}), w, req) + assert.NoError(tt, err) + + var getManifestResp router.GetManifestResponse + err = json.NewDecoder(w.Body).Decode(&getManifestResp) + assert.NoError(tt, err) + assert.NotEmpty(tt, getManifestResp) + assert.Equal(tt, resp.Manifest.ID, getManifestResp.ID) + }) + + t.Run("Test Get Manifests", func(tt *testing.T) { + bolt, err := storage.NewBoltDB() + + // remove the db file after the test + tt.Cleanup(func() { + _ = bolt.Close() + _ = os.Remove(storage.DBFile) + }) + + manifestService := newManifestService(tt, bolt) + + w := httptest.NewRecorder() + + // good request + createManifestRequest := getValidManifestRequest() + + requestValue := newRequestValue(tt, createManifestRequest) + req := httptest.NewRequest(http.MethodPut, "https://ssi-service.com/v1/manifests", requestValue) + err = manifestService.CreateManifest(newRequestContext(), w, req) + assert.NoError(tt, err) + + var resp router.CreateManifestResponse + err = json.NewDecoder(w.Body).Decode(&resp) + assert.NoError(tt, err) + + // get manifest by id + req = httptest.NewRequest(http.MethodGet, fmt.Sprintf("https://ssi-service.com/v1/manifests"), nil) + err = manifestService.GetManifests(newRequestContext(), w, req) + assert.NoError(tt, err) + + var getManifestsResp router.GetManifestsResponse + err = json.NewDecoder(w.Body).Decode(&getManifestsResp) + assert.NoError(tt, err) + assert.NotEmpty(tt, getManifestsResp) + assert.Len(tt, getManifestsResp.Manifests, 1) + assert.Equal(tt, resp.Manifest.ID, getManifestsResp.Manifests[0].ID) + }) + + t.Run("Test Delete Manifest", func(tt *testing.T) { + bolt, err := storage.NewBoltDB() + + // remove the db file after the test + tt.Cleanup(func() { + _ = bolt.Close() + _ = os.Remove(storage.DBFile) + }) + + manifestService := newManifestService(tt, bolt) + + // good request + createManifestRequest := getValidManifestRequest() + + requestValue := newRequestValue(tt, createManifestRequest) + req := httptest.NewRequest(http.MethodPut, "https://ssi-service.com/v1/manifests", requestValue) + w := httptest.NewRecorder() + err = manifestService.CreateManifest(newRequestContext(), w, req) + assert.NoError(tt, err) + + var resp router.CreateManifestResponse + err = json.NewDecoder(w.Body).Decode(&resp) + assert.NoError(tt, err) + + w.Flush() + + // get credential by id + req = httptest.NewRequest(http.MethodGet, fmt.Sprintf("https://ssi-service.com/v1/manifests/%s", resp.Manifest.ID), nil) + err = manifestService.GetManifest(newRequestContextWithParams(map[string]string{"id": resp.Manifest.ID}), w, req) + assert.NoError(tt, err) + + var getManifestResp router.GetCredentialResponse + err = json.NewDecoder(w.Body).Decode(&getManifestResp) + assert.NoError(tt, err) + assert.NotEmpty(tt, getManifestResp) + assert.Equal(tt, resp.Manifest.ID, getManifestResp.ID) + + w.Flush() + + // delete it + req = httptest.NewRequest(http.MethodDelete, fmt.Sprintf("https://ssi-service.com/v1/manifests/%s", resp.Manifest.ID), nil) + err = manifestService.DeleteManifest(newRequestContextWithParams(map[string]string{"id": resp.Manifest.ID}), w, req) + assert.NoError(tt, err) + + w.Flush() + + // get it back + req = httptest.NewRequest(http.MethodGet, fmt.Sprintf("https://ssi-service.com/v1/manifests/%s", resp.Manifest.ID), nil) + err = manifestService.GetManifest(newRequestContextWithParams(map[string]string{"id": resp.Manifest.ID}), w, req) + assert.Error(tt, err) + assert.Contains(tt, err.Error(), fmt.Sprintf("could not get manifest with id: %s", resp.Manifest.ID)) + }) +} + +func newManifestService(t *testing.T, bolt *storage.BoltDB) *router.ManifestRouter { + manifestService, err := manifest.NewManifestService(config.ManifestServiceConfig{}, bolt) + require.NoError(t, err) + require.NotEmpty(t, manifestService) + + // create router for service + manifestRouter, err := router.NewManifestRouter(manifestService) + require.NoError(t, err) + require.NotEmpty(t, manifestRouter) + + return manifestRouter +} + func TestKeyStoreAPI(t *testing.T) { t.Run("Test Store Key", func(tt *testing.T) { bolt, err := storage.NewBoltDB() @@ -870,3 +1110,41 @@ func newRequestContextWithParams(params map[string]string) context.Context { }) return httptreemux.AddParamsToContext(ctx, params) } + +func getValidManifestRequest() router.CreateManifestRequest { + createManifestRequest := router.CreateManifestRequest{ + Issuer: "did:abc:123", + Context: "context123", + PresentationDefinition: exchange.PresentationDefinition{ + ID: "pres-def-id", + InputDescriptors: []exchange.InputDescriptor{ + { + ID: "test-id", + Constraints: &exchange.Constraints{ + Fields: []exchange.Field{ + { + Path: []string{".vc.id"}, + }, + }, + }, + }, + }, + }, + OutputDescriptors: []manifestsdk.OutputDescriptor{ + { + ID: "id1", + Schema: "https://test.com/schema", + Name: "good ID", + Description: "it's all good", + }, + { + ID: "id2", + Schema: "https://test.com/schema", + Name: "good ID", + Description: "it's all good", + }, + }, + } + + return createManifestRequest +} diff --git a/pkg/service/framework/framework.go b/pkg/service/framework/framework.go index 8b6b25cdb..416f0206f 100644 --- a/pkg/service/framework/framework.go +++ b/pkg/service/framework/framework.go @@ -12,6 +12,7 @@ const ( Schema Type = "schema" Credential Type = "credential" KeyStore Type = "keystore" + Manifest Type = "manifest" StatusReady StatusState = "ready" StatusNotReady StatusState = "not_ready" diff --git a/pkg/service/manifest/manifest.go b/pkg/service/manifest/manifest.go new file mode 100644 index 000000000..8abb7354b --- /dev/null +++ b/pkg/service/manifest/manifest.go @@ -0,0 +1,165 @@ +package manifest + +import ( + "encoding/json" + "fmt" + "github.com/TBD54566975/ssi-sdk/credential/exchange" + "github.com/TBD54566975/ssi-sdk/credential/manifest" + "github.com/TBD54566975/ssi-sdk/crypto" + "github.com/sirupsen/logrus" + "github.com/tbd54566975/ssi-service/config" + "github.com/tbd54566975/ssi-service/internal/util" + "github.com/tbd54566975/ssi-service/pkg/service/framework" + manifeststorage "github.com/tbd54566975/ssi-service/pkg/service/manifest/storage" + "github.com/tbd54566975/ssi-service/pkg/storage" +) + +type Service struct { + storage manifeststorage.Storage + config config.ManifestServiceConfig +} + +func (s Service) Type() framework.Type { + return framework.Manifest +} + +func (s Service) Status() framework.Status { + if s.storage == nil { + return framework.Status{ + Status: framework.StatusNotReady, + Message: "no storage", + } + } + return framework.Status{Status: framework.StatusReady} +} + +func (s Service) Config() config.ManifestServiceConfig { + return s.config +} + +func NewManifestService(config config.ManifestServiceConfig, s storage.ServiceStorage) (*Service, error) { + manifestStorage, err := manifeststorage.NewManifestStorage(s) + if err != nil { + errMsg := "could not instantiate storage for the manifest service" + return nil, util.LoggingErrorMsg(err, errMsg) + } + return &Service{ + storage: manifestStorage, + config: config, + }, nil +} + +func (s Service) CreateManifest(request CreateManifestRequest) (*CreateManifestResponse, error) { + logrus.Debugf("creating manifest: %+v", request) + + builder := manifest.NewCredentialManifestBuilder() + issuer := manifest.Issuer{ID: request.Issuer, Name: request.Issuer} + + if err := builder.SetIssuer(issuer); err != nil { + errMsg := fmt.Sprintf("could not build manifest when setting issuer: %s", request.Issuer) + return nil, util.LoggingErrorMsg(err, errMsg) + } + + // TODO: (Neal) Add dynamic claim formats + if err := builder.SetClaimFormat(exchange.ClaimFormat{ + JWT: &exchange.JWTType{Alg: []crypto.SignatureAlgorithm{crypto.EdDSA}}, + }); err != nil { + errMsg := fmt.Sprintf("could not build manifest when setting claim format") + return nil, util.LoggingErrorMsg(err, errMsg) + } + + // parse OutputDescriptors + odJsonString, err := json.Marshal(request.OutputDescriptors) + if err != nil { + errMsg := "could not marshal request output descriptors" + return nil, util.LoggingErrorMsg(err, errMsg) + } + + od := []manifest.OutputDescriptor{} + err = json.Unmarshal(odJsonString, &od) + if err != nil { + errMsg := "could not unmarshal output descriptors" + return nil, util.LoggingErrorMsg(err, errMsg) + } + + builder.SetOutputDescriptors(od) + + // parse PresentationDefinition + pdJsonString, err := json.Marshal(request.PresentationDefinition) + if err != nil { + errMsg := "could not marshal request presentation definition" + return nil, util.LoggingErrorMsg(err, errMsg) + } + + pd := exchange.PresentationDefinition{} + err = json.Unmarshal(pdJsonString, &pd) + if err != nil { + errMsg := "could not unmarshal presentation definition" + return nil, util.LoggingErrorMsg(err, errMsg) + } + builder.SetPresentationDefinition(pd) + + mfst, err := builder.Build() + if err != nil { + errMsg := "could not build manifest" + return nil, util.LoggingErrorMsg(err, errMsg) + } + + // store the manifest + storageRequest := manifeststorage.StoredManifest{ + ID: mfst.ID, + Manifest: *mfst, + Issuer: request.Issuer, + } + + if err := s.storage.StoreManifest(storageRequest); err != nil { + errMsg := "could not store manifest" + return nil, util.LoggingErrorMsg(err, errMsg) + } + + // return the result + response := CreateManifestResponse{Manifest: *mfst} + return &response, nil +} + +func (s Service) GetManifest(request GetManifestRequest) (*GetManifestResponse, error) { + + logrus.Debugf("getting manifest: %s", request.ID) + + gotManifest, err := s.storage.GetManifest(request.ID) + if err != nil { + errMsg := fmt.Sprintf("could not get manifest: %s", request.ID) + return nil, util.LoggingErrorMsg(err, errMsg) + } + + response := GetManifestResponse{Manifest: gotManifest.Manifest} + return &response, nil +} + +func (s Service) GetManifests() (*GetManifestsResponse, error) { + gotManifests, err := s.storage.GetManifests() + + if err != nil { + errMsg := fmt.Sprintf("could not get manifests(s)") + return nil, util.LoggingErrorMsg(err, errMsg) + } + + var manifests []manifest.CredentialManifest + for _, manifest := range gotManifests { + manifests = append(manifests, manifest.Manifest) + } + response := GetManifestsResponse{Manifests: manifests} + return &response, nil +} + +func (s Service) DeleteManifest(request DeleteManifestRequest) error { + + logrus.Debugf("deleting manifest: %s", request.ID) + + if err := s.storage.DeleteManifest(request.ID); err != nil { + errMsg := fmt.Sprintf("could not delete manifest with id: %s", request.ID) + return util.LoggingErrorMsg(err, errMsg) + } + + return nil +} diff --git a/pkg/service/manifest/model.go b/pkg/service/manifest/model.go new file mode 100644 index 000000000..8ad3105ae --- /dev/null +++ b/pkg/service/manifest/model.go @@ -0,0 +1,38 @@ +package manifest + +import ( + "github.com/TBD54566975/ssi-sdk/credential/exchange" + manifestsdk "github.com/TBD54566975/ssi-sdk/credential/manifest" +) + +type CreateManifestRequest struct { + Issuer string + // A context is optional. If not present, we'll apply default, required context values. + Context string + OutputDescriptors []manifestsdk.OutputDescriptor + PresentationDefinition exchange.PresentationDefinition +} + +type CreateManifestResponse struct { + Manifest manifestsdk.CredentialManifest +} + +type GetManifestRequest struct { + ID string +} + +type GetManifestResponse struct { + Manifest manifestsdk.CredentialManifest +} + +type GetManifestByIssuerRequest struct { + Issuer string +} + +type GetManifestsResponse struct { + Manifests []manifestsdk.CredentialManifest +} + +type DeleteManifestRequest struct { + ID string +} diff --git a/pkg/service/manifest/storage/bolt.go b/pkg/service/manifest/storage/bolt.go new file mode 100644 index 000000000..6be43ede0 --- /dev/null +++ b/pkg/service/manifest/storage/bolt.go @@ -0,0 +1,92 @@ +package storage + +import ( + "fmt" + "github.com/goccy/go-json" + "github.com/pkg/errors" + "github.com/sirupsen/logrus" + "github.com/tbd54566975/ssi-service/pkg/storage" +) + +const ( + namespace = "manifest" +) + +type BoltManifestStorage struct { + db *storage.BoltDB +} + +func NewBoltManifestStorage(db *storage.BoltDB) (*BoltManifestStorage, error) { + if db == nil { + return nil, errors.New("bolt db reference is nil") + } + return &BoltManifestStorage{db: db}, nil +} + +func (b BoltManifestStorage) StoreManifest(manifest StoredManifest) error { + id := manifest.Manifest.ID + if id == "" { + err := errors.New("could not store manifest without an ID") + logrus.WithError(err).Error() + return err + } + manifestBytes, err := json.Marshal(manifest) + if err != nil { + errMsg := fmt.Sprintf("could not store manifest: %s", id) + logrus.WithError(err).Error(errMsg) + return errors.Wrapf(err, errMsg) + } + return b.db.Write(namespace, id, manifestBytes) +} + +func (b BoltManifestStorage) GetManifest(id string) (*StoredManifest, error) { + manifestBytes, err := b.db.Read(namespace, id) + if err != nil { + errMsg := fmt.Sprintf("could not get manifest: %s", id) + logrus.WithError(err).Error(errMsg) + return nil, errors.Wrapf(err, errMsg) + } + if len(manifestBytes) == 0 { + err := fmt.Errorf("manifest not found with id: %s", id) + logrus.WithError(err).Error("could not get manifest from storage") + return nil, err + } + var stored StoredManifest + if err := json.Unmarshal(manifestBytes, &stored); err != nil { + errMsg := fmt.Sprintf("could not unmarshal stored manifest: %s", id) + logrus.WithError(err).Error(errMsg) + return nil, errors.Wrapf(err, errMsg) + } + return &stored, nil +} + +// GetManifests attempts to get all stored manifests. It will return those it can even if it has trouble with some. +func (b BoltManifestStorage) GetManifests() ([]StoredManifest, error) { + gotManifests, err := b.db.ReadAll(namespace) + if err != nil { + errMsg := "could not get all manifests" + logrus.WithError(err).Error(errMsg) + return nil, errors.Wrap(err, errMsg) + } + if len(gotManifests) == 0 { + logrus.Info("no manifests to get") + return nil, nil + } + var stored []StoredManifest + for _, manifestBytes := range gotManifests { + var nextManifest StoredManifest + if err := json.Unmarshal(manifestBytes, &nextManifest); err == nil { + stored = append(stored, nextManifest) + } + } + return stored, nil +} + +func (b BoltManifestStorage) DeleteManifest(id string) error { + if err := b.db.Delete(namespace, id); err != nil { + errMsg := fmt.Sprintf("could not delete manifest: %s", id) + logrus.WithError(err).Error(errMsg) + return errors.Wrapf(err, errMsg) + } + return nil +} diff --git a/pkg/service/manifest/storage/storage.go b/pkg/service/manifest/storage/storage.go new file mode 100644 index 000000000..b78a39c10 --- /dev/null +++ b/pkg/service/manifest/storage/storage.go @@ -0,0 +1,42 @@ +package storage + +import ( + "fmt" + "github.com/TBD54566975/ssi-sdk/credential/manifest" + + "github.com/tbd54566975/ssi-service/internal/util" + "github.com/tbd54566975/ssi-service/pkg/storage" +) + +type StoredManifest struct { + ID string `json:"id"` + Manifest manifest.CredentialManifest `json:"manifest"` + Issuer string `json:"issuer"` +} + +type Storage interface { + StoreManifest(manifest StoredManifest) error + GetManifest(id string) (*StoredManifest, error) + GetManifests() ([]StoredManifest, error) + DeleteManifest(id string) error +} + +// NewManifestStorage finds the manifest storage impl for a given ServiceStorage value +func NewManifestStorage(s storage.ServiceStorage) (Storage, error) { + switch s.Type() { + case storage.Bolt: + gotBolt, ok := s.(*storage.BoltDB) + if !ok { + errMsg := fmt.Sprintf("trouble instantiating : %s", s.Type()) + return nil, util.LoggingNewError(errMsg) + } + boltStorage, err := NewBoltManifestStorage(gotBolt) + if err != nil { + return nil, util.LoggingErrorMsg(err, "could not instantiate manifest bolt storage") + } + return boltStorage, err + default: + errMsg := fmt.Errorf("unsupported storage type: %s", s.Type()) + return nil, util.LoggingError(errMsg) + } +} diff --git a/pkg/service/schema/model.go b/pkg/service/schema/model.go index c083a5061..9037e7f7e 100644 --- a/pkg/service/schema/model.go +++ b/pkg/service/schema/model.go @@ -6,10 +6,6 @@ const ( Version1 string = "1.0.0" ) -type GetSchemasResponse struct { - Schemas []schema.VCJSONSchema `json:"schemas,omitempty"` -} - type CreateSchemaRequest struct { Author string `json:"author" validate:"required"` Name string `json:"name" validate:"required"` @@ -21,10 +17,14 @@ type CreateSchemaResponse struct { Schema schema.VCJSONSchema `json:"schema"` } -type GetSchemaByIDRequest struct { +type GetSchemasResponse struct { + Schemas []schema.VCJSONSchema `json:"schemas,omitempty"` +} + +type GetSchemaRequest struct { ID string `json:"id" validate:"required"` } -type GetSchemaByIDResponse struct { +type GetSchemaResponse struct { Schema schema.VCJSONSchema `json:"schema"` } diff --git a/pkg/service/schema/schema.go b/pkg/service/schema/schema.go index 774993213..40c52c572 100644 --- a/pkg/service/schema/schema.go +++ b/pkg/service/schema/schema.go @@ -95,7 +95,7 @@ func (s Service) GetSchemas() (*GetSchemasResponse, error) { }, nil } -func (s Service) GetSchemaByID(request GetSchemaByIDRequest) (*GetSchemaByIDResponse, error) { +func (s Service) GetSchema(request GetSchemaRequest) (*GetSchemaResponse, error) { gotSchema, err := s.storage.GetSchema(request.ID) if err != nil { err := errors.Wrapf(err, "error getting schema: %s", request.ID) @@ -105,5 +105,5 @@ func (s Service) GetSchemaByID(request GetSchemaByIDRequest) (*GetSchemaByIDResp err := fmt.Errorf("schema with id<%s> could not be found", request.ID) return nil, util.LoggingError(err) } - return &GetSchemaByIDResponse{Schema: gotSchema.Schema}, nil + return &GetSchemaResponse{Schema: gotSchema.Schema}, nil } diff --git a/pkg/service/service.go b/pkg/service/service.go index c71e15e2a..8945daa2d 100644 --- a/pkg/service/service.go +++ b/pkg/service/service.go @@ -2,6 +2,7 @@ package service import ( "fmt" + "github.com/tbd54566975/ssi-service/pkg/service/manifest" "github.com/tbd54566975/ssi-service/config" "github.com/tbd54566975/ssi-service/internal/util" @@ -77,5 +78,11 @@ func instantiateServices(config config.ServicesConfig) ([]framework.Service, err return nil, util.LoggingErrorMsg(err, errMsg) } - return []framework.Service{didService, schemaService, credentialService}, nil + manifestService, err := manifest.NewManifestService(config.ManifestConfig, storageProvider) + if err != nil { + errMsg := "could not instantiate the manifest service" + return nil, util.LoggingErrorMsg(err, errMsg) + } + + return []framework.Service{didService, schemaService, credentialService, manifestService}, nil }