diff --git a/doc/swagger.yaml b/doc/swagger.yaml index a5d9f9384..38331609a 100644 --- a/doc/swagger.yaml +++ b/doc/swagger.yaml @@ -479,7 +479,7 @@ definitions: context values. type: string data: - additionalProperties: true + additionalProperties: {} type: object expiry: type: string @@ -770,7 +770,7 @@ definitions: description: The reason why the submission was approved or denied. type: string status: - description: One of {`pending`, `approved`, `denied`}. + description: One of {`pending`, `approved`, `denied`, `cancelled`}. type: string required: - definition_id @@ -790,7 +790,7 @@ definitions: properties: submissions: items: - $ref: '#/definitions/presentation.Submission' + $ref: '#/definitions/model.Submission' type: array type: object github.com_tbd54566975_ssi-service_pkg_server_router.Operation: @@ -846,7 +846,7 @@ definitions: description: The reason why the submission was approved or denied. type: string status: - description: One of {`pending`, `approved`, `denied`}. + description: One of {`pending`, `approved`, `denied`, `cancelled`}. type: string required: - definition_id @@ -1044,6 +1044,28 @@ definitions: - id - schema type: object + model.Submission: + properties: + definition_id: + type: string + descriptor_map: + items: + $ref: '#/definitions/exchange.SubmissionDescriptor' + type: array + id: + type: string + reason: + description: The reason why the submission was approved or denied. + type: string + status: + description: One of {`pending`, `approved`, `denied`, `cancelled`}. + type: string + required: + - definition_id + - descriptor_map + - id + - status + type: object pkg_server_router.CreateCredentialRequest: properties: '@context': @@ -1051,7 +1073,7 @@ definitions: context values. type: string data: - additionalProperties: true + additionalProperties: {} type: object expiry: type: string @@ -1342,7 +1364,7 @@ definitions: description: The reason why the submission was approved or denied. type: string status: - description: One of {`pending`, `approved`, `denied`}. + description: One of {`pending`, `approved`, `denied`, `cancelled`}. type: string required: - definition_id @@ -1362,7 +1384,7 @@ definitions: properties: submissions: items: - $ref: '#/definitions/presentation.Submission' + $ref: '#/definitions/model.Submission' type: array type: object pkg_server_router.Operation: @@ -1418,7 +1440,7 @@ definitions: description: The reason why the submission was approved or denied. type: string status: - description: One of {`pending`, `approved`, `denied`}. + description: One of {`pending`, `approved`, `denied`, `cancelled`}. type: string required: - definition_id @@ -1501,28 +1523,6 @@ definitions: verified: type: boolean type: object - presentation.Submission: - properties: - definition_id: - type: string - descriptor_map: - items: - $ref: '#/definitions/exchange.SubmissionDescriptor' - type: array - id: - type: string - reason: - description: The reason why the submission was approved or denied. - type: string - status: - description: One of {`pending`, `approved`, `denied`}. - type: string - required: - - definition_id - - descriptor_map - - id - - status - type: object rendering.ColorResource: properties: color: @@ -2411,7 +2411,7 @@ paths: "200": description: OK schema: - $ref: '#/definitions/github.com_tbd54566975_ssi-service_pkg_server_router.GetOperationsResponse' + $ref: '#/definitions/pkg_server_router.Operation' "400": description: Bad request schema: @@ -2440,7 +2440,7 @@ paths: "200": description: OK schema: - $ref: '#/definitions/github.com_tbd54566975_ssi-service_pkg_server_router.Operation' + $ref: '#/definitions/pkg_server_router.Operation' "400": description: Bad request schema: diff --git a/pkg/server/router/operation.go b/pkg/server/router/operation.go index f0e1e645e..596d0b72d 100644 --- a/pkg/server/router/operation.go +++ b/pkg/server/router/operation.go @@ -53,7 +53,7 @@ func (o OperationRouter) GetOperation(ctx context.Context, w http.ResponseWriter return framework.NewRequestError( util.LoggingErrorMsg(err, "failed getting operation"), http.StatusInternalServerError) } - return framework.Respond(ctx, w, routerModel(op), http.StatusOK) + return framework.Respond(ctx, w, routerModel(*op), http.StatusOK) } type GetOperationsRequest struct { @@ -169,10 +169,22 @@ func routerModel(op operation.Operation) Operation { // @Accept json // @Produce json // @Param id path string true "ID" -// @Success 200 {object} GetOperationsResponse "OK" +// @Success 200 {object} Operation "OK" // @Failure 400 {string} string "Bad request" // @Failure 500 {string} string "Internal server error" // @Router /v1/operations [get] func (o OperationRouter) CancelOperation(ctx context.Context, w http.ResponseWriter, r *http.Request) error { - return nil + id := framework.GetParam(ctx, IDParam) + if id == nil { + return framework.NewRequestError( + util.LoggingNewError("get operation request requires id"), http.StatusBadRequest) + } + + op, err := o.service.CancelOperation(operation.CancelOperationRequest{ID: *id}) + + if err != nil { + return framework.NewRequestError( + util.LoggingErrorMsg(err, "failed cancelling operation"), http.StatusInternalServerError) + } + return framework.Respond(ctx, w, routerModel(*op), http.StatusOK) } diff --git a/pkg/server/server_operation_test.go b/pkg/server/server_operation_test.go index 01c9272a2..d96613b85 100644 --- a/pkg/server/server_operation_test.go +++ b/pkg/server/server_operation_test.go @@ -15,6 +15,7 @@ import ( "github.com/stretchr/testify/require" "github.com/tbd54566975/ssi-service/pkg/server/router" "github.com/tbd54566975/ssi-service/pkg/service/operation" + "github.com/tbd54566975/ssi-service/pkg/service/operation/submission" "github.com/tbd54566975/ssi-service/pkg/storage" ) @@ -27,7 +28,7 @@ func TestOperationsAPI(t *testing.T) { holderSigner, holderDID := getSigner(t) definition := createPresentationDefinition(t, pRouter) submissionOp := createSubmission(t, pRouter, definition.PresentationDefinition.ID, VerifiableCredential(), holderDID, holderSigner) - submission := reviewSubmission(t, pRouter, operation.SubmissionID(submissionOp.ID)) + sub := reviewSubmission(t, pRouter, submission.ID(submissionOp.ID)) createdID := submissionOp.ID req := httptest.NewRequest( @@ -43,7 +44,7 @@ func TestOperationsAPI(t *testing.T) { assert.NoError(t, json.NewDecoder(w.Body).Decode(&resp)) assert.True(t, resp.Done) assert.Empty(t, resp.Result.Error) - data, err := json.Marshal(submission) + data, err := json.Marshal(sub) assert.NoError(t, err) var responseAsMap map[string]any assert.NoError(t, json.Unmarshal(data, &responseAsMap)) @@ -220,6 +221,57 @@ func TestOperationsAPI(t *testing.T) { }) }) + t.Run("CancelOperation", func(t *testing.T) { + t.Run("Marks an operation as done", func(t *testing.T) { + s := setupTestDB(t) + pRouter := setupPresentationRouter(t, s) + opRouter := setupOperationsRouter(t, s) + + holderSigner, holderDID := getSigner(t) + definition := createPresentationDefinition(t, pRouter) + submissionOp := createSubmission(t, pRouter, definition.PresentationDefinition.ID, VerifiableCredential(), holderDID, holderSigner) + + createdID := submissionOp.ID + req := httptest.NewRequest( + http.MethodPut, + fmt.Sprintf("https://ssi-service.com/v1/operations/%s", createdID), + nil) + w := httptest.NewRecorder() + + err := opRouter.CancelOperation(newRequestContextWithParams(map[string]string{"id": createdID}), w, req) + + assert.NoError(t, err) + var resp router.Operation + assert.NoError(t, json.NewDecoder(w.Body).Decode(&resp)) + assert.True(t, resp.Done) + assert.Contains(t, resp.Result.Response, "definition_id") + assert.Contains(t, resp.Result.Response, "descriptor_map") + assert.Equal(t, "cancelled", resp.Result.Response.(map[string]any)["status"]) + }) + + t.Run("Returns error when operation is done already", func(t *testing.T) { + s := setupTestDB(t) + pRouter := setupPresentationRouter(t, s) + opRouter := setupOperationsRouter(t, s) + + holderSigner, holderDID := getSigner(t) + definition := createPresentationDefinition(t, pRouter) + submissionOp := createSubmission(t, pRouter, definition.PresentationDefinition.ID, VerifiableCredential(), holderDID, holderSigner) + _ = reviewSubmission(t, pRouter, submission.ID(submissionOp.ID)) + + createdID := submissionOp.ID + req := httptest.NewRequest( + http.MethodPut, + fmt.Sprintf("https://ssi-service.com/v1/operations/%s", createdID), + nil) + w := httptest.NewRecorder() + + err := opRouter.CancelOperation(newRequestContextWithParams(map[string]string{"id": createdID}), w, req) + + assert.Error(t, err) + }) + }) + } func setupTestDB(t *testing.T) storage.ServiceStorage { diff --git a/pkg/server/server_presentation_test.go b/pkg/server/server_presentation_test.go index ebbde08b0..55915cf74 100644 --- a/pkg/server/server_presentation_test.go +++ b/pkg/server/server_presentation_test.go @@ -20,7 +20,7 @@ import ( "github.com/tbd54566975/ssi-service/config" "github.com/tbd54566975/ssi-service/internal/keyaccess" "github.com/tbd54566975/ssi-service/pkg/server/router" - "github.com/tbd54566975/ssi-service/pkg/service/operation" + "github.com/tbd54566975/ssi-service/pkg/service/operation/submission" "github.com/tbd54566975/ssi-service/pkg/service/presentation" "github.com/tbd54566975/ssi-service/pkg/service/presentation/model" "github.com/tbd54566975/ssi-service/pkg/storage" @@ -163,14 +163,14 @@ func TestPresentationAPI(t *testing.T) { "id": "did:web:andresuribe.com", })), holderDID, holderSigner) - req := httptest.NewRequest(http.MethodGet, fmt.Sprintf("https://ssi-service.com/v1/presentations/submissions/%s", operation.SubmissionID(op.ID)), nil) + req := httptest.NewRequest(http.MethodGet, fmt.Sprintf("https://ssi-service.com/v1/presentations/submissions/%s", submission.ID(op.ID)), nil) w := httptest.NewRecorder() - assert.NoError(t, pRouter.GetSubmission(newRequestContextWithParams(map[string]string{"id": operation.SubmissionID(op.ID)}), w, req)) + assert.NoError(t, pRouter.GetSubmission(newRequestContextWithParams(map[string]string{"id": submission.ID(op.ID)}), w, req)) var resp router.GetSubmissionResponse assert.NoError(t, json.NewDecoder(w.Body).Decode(&resp)) - assert.Equal(t, operation.SubmissionID(op.ID), resp.Submission.ID) + assert.Equal(t, submission.ID(op.ID), resp.Submission.ID) assert.Equal(t, definition.PresentationDefinition.ID, resp.Submission.DefinitionID) assert.Equal(t, "pending", resp.Submission.Status) }) @@ -218,7 +218,7 @@ func TestPresentationAPI(t *testing.T) { } value := newRequestValue(t, request) - createdID := operation.SubmissionID(submissionOp.ID) + createdID := submission.ID(submissionOp.ID) req := httptest.NewRequest( http.MethodPut, fmt.Sprintf("https://ssi-service.com/v1/presentations/submissions/%s/review", createdID), @@ -243,7 +243,7 @@ func TestPresentationAPI(t *testing.T) { holderSigner, holderDID := getSigner(t) definition := createPresentationDefinition(t, pRouter) submissionOp := createSubmission(t, pRouter, definition.PresentationDefinition.ID, VerifiableCredential(), holderDID, holderSigner) - createdID := operation.SubmissionID(submissionOp.ID) + createdID := submission.ID(submissionOp.ID) _ = reviewSubmission(t, pRouter, createdID) request := router.ReviewSubmissionRequest{ @@ -323,14 +323,14 @@ func TestPresentationAPI(t *testing.T) { { Status: "pending", PresentationSubmission: &exchange.PresentationSubmission{ - ID: operation.SubmissionID(op.ID), + ID: submission.ID(op.ID), DefinitionID: definition.PresentationDefinition.ID, }, }, { Status: "pending", PresentationSubmission: &exchange.PresentationSubmission{ - ID: operation.SubmissionID(op2.ID), + ID: submission.ID(op2.ID), DefinitionID: definition.PresentationDefinition.ID, }, }, @@ -395,7 +395,7 @@ func TestPresentationAPI(t *testing.T) { { Status: "pending", PresentationSubmission: &exchange.PresentationSubmission{ - ID: operation.SubmissionID(op.ID), + ID: submission.ID(op.ID), DefinitionID: definition.PresentationDefinition.ID, }, }, diff --git a/pkg/service/operation/bolt.go b/pkg/service/operation/bolt.go new file mode 100644 index 000000000..df216f69b --- /dev/null +++ b/pkg/service/operation/bolt.go @@ -0,0 +1,134 @@ +package operation + +import ( + "strings" + + "github.com/goccy/go-json" + "github.com/pkg/errors" + "github.com/sirupsen/logrus" + "github.com/tbd54566975/ssi-service/internal/util" + opstorage "github.com/tbd54566975/ssi-service/pkg/service/operation/storage" + "github.com/tbd54566975/ssi-service/pkg/service/operation/storage/namespace" + "github.com/tbd54566975/ssi-service/pkg/service/operation/submission" + "github.com/tbd54566975/ssi-service/pkg/storage" + "go.einride.tech/aip/filtering" +) + +const ( + cancelledReason = "operation cancelled" +) + +type BoltOperationStorage struct { + db *storage.BoltDB +} + +func NewBoltOperationStorage(db *storage.BoltDB) (*BoltOperationStorage, error) { + if db == nil { + return nil, errors.New("bolt db reference is nil") + } + return &BoltOperationStorage{db: db}, nil + +} + +func (b BoltOperationStorage) CancelOperation(id string) (*opstorage.StoredOperation, error) { + if strings.HasPrefix(id, submission.ParentResource) { + _, opData, err := b.db.UpdateValueAndOperation( + submission.Namespace, submission.ID(id), storage.NewUpdater(map[string]any{ + "status": submission.StatusCancelled, + "reason": cancelledReason, + }), + namespace.FromID(id), id, submission.OperationUpdater{ + UpdaterWithMap: storage.NewUpdater(map[string]any{ + "done": true, + }), + }) + if err != nil { + return nil, errors.Wrap(err, "updating value and op") + } + var op opstorage.StoredOperation + if err = json.Unmarshal(opData, &op); err != nil { + return nil, errors.Wrap(err, "unmarshalling data") + } + return &op, nil + } + return nil, errors.New("unrecognized id structure") +} + +func (b BoltOperationStorage) StoreOperation(op opstorage.StoredOperation) error { + id := op.ID + if id == "" { + return util.LoggingNewError("ID is required for storing operations") + } + jsonBytes, err := json.Marshal(op) + if err != nil { + return util.LoggingErrorMsgf(err, "marshalling operation with id: %s", id) + } + if err = b.db.Write(namespace.FromID(id), id, jsonBytes); err != nil { + return util.LoggingErrorMsg(err, "writing to db") + } + return nil +} + +func (b BoltOperationStorage) GetOperation(id string) (opstorage.StoredOperation, error) { + var stored opstorage.StoredOperation + jsonBytes, err := b.db.Read(namespace.FromID(id), id) + if err != nil { + return stored, util.LoggingErrorMsgf(err, "reading operation with id: %s", id) + } + if len(jsonBytes) == 0 { + return stored, util.LoggingNewErrorf("operation not found with id: %s", id) + } + if err := json.Unmarshal(jsonBytes, &stored); err != nil { + return stored, util.LoggingErrorMsgf(err, "unmarshalling stored operation: %s", id) + } + return stored, nil +} + +func (b BoltOperationStorage) GetOperations(parent string, filter filtering.Filter) ([]opstorage.StoredOperation, error) { + operations, err := b.db.ReadAll(namespace.FromParent(parent)) + if err != nil { + return nil, util.LoggingErrorMsgf(err, "could not get all operations") + } + + shouldInclude, err := storage.NewIncludeFunc(filter) + if err != nil { + return nil, err + } + stored := make([]opstorage.StoredOperation, 0, len(operations)) + for i, manifestBytes := range operations { + var nextOp opstorage.StoredOperation + if err = json.Unmarshal(manifestBytes, &nextOp); err != nil { + logrus.WithError(err).WithField("idx", i).Warnf("Skipping operation") + } + include, err := shouldInclude(nextOp) + // We explicitly ignore evaluation errors and simply include them in the result. + if err != nil || include { + stored = append(stored, nextOp) + } + } + return stored, nil +} + +func (b BoltOperationStorage) DeleteOperation(id string) error { + if err := b.db.Delete(namespace.FromID(id), id); err != nil { + return util.LoggingErrorMsgf(err, "deleting operation: %s", id) + } + return nil +} + +func NewOperationStorage(s storage.ServiceStorage) (opstorage.Storage, error) { + switch s.Type() { + case storage.Bolt: + gotBolt, ok := s.(*storage.BoltDB) + if !ok { + return nil, util.LoggingNewErrorf("trouble instantiating : %s", s.Type()) + } + boltStorage, err := NewBoltOperationStorage(gotBolt) + if err != nil { + return nil, util.LoggingErrorMsg(err, "could not instantiate schema bolt storage") + } + return boltStorage, err + default: + return nil, util.LoggingNewErrorf("unsupported storage type: %s", s.Type()) + } +} diff --git a/pkg/service/operation/model.go b/pkg/service/operation/model.go index fef7c622d..d3e5c7a2c 100644 --- a/pkg/service/operation/model.go +++ b/pkg/service/operation/model.go @@ -1,11 +1,7 @@ package operation import ( - "fmt" - "strings" - "github.com/TBD54566975/ssi-sdk/util" - "github.com/tbd54566975/ssi-service/pkg/service/operation/storage" "go.einride.tech/aip/filtering" ) @@ -20,16 +16,6 @@ type Operation struct { Result Result `json:"result,omitempty"` } -// SubmissionID attempts to parse the submission id from the ID of the operation. This is done by taking the last word -// that results from splitting the id by "/". On failures, the empty string is returned. -func SubmissionID(opID string) string { - i := strings.LastIndex(opID, "/") - if i == -1 { - return "" - } - return opID[(i + 1):] -} - type GetOperationsRequest struct { Parent string `validate:"required"` Filter filtering.Filter @@ -52,7 +38,11 @@ func (r GetOperationRequest) Validate() error { return util.NewValidator().Struct(r) } -// IDFromSubmissionID returns a submission operation ID from the submission ID. -func IDFromSubmissionID(id string) string { - return fmt.Sprintf("%s/%s", storage.SubmissionParentResource, id) +type CancelOperationRequest struct { + ID string `json:"id" validate:"required"` +} + +// Validate does struct validation and returns an error when invalid. +func (r CancelOperationRequest) Validate() error { + return util.NewValidator().Struct(r) } diff --git a/pkg/service/operation/service.go b/pkg/service/operation/service.go index 71ab2439c..433c2e6c2 100644 --- a/pkg/service/operation/service.go +++ b/pkg/service/operation/service.go @@ -11,6 +11,7 @@ import ( "github.com/tbd54566975/ssi-service/internal/util" "github.com/tbd54566975/ssi-service/pkg/service/framework" opstorage "github.com/tbd54566975/ssi-service/pkg/service/operation/storage" + "github.com/tbd54566975/ssi-service/pkg/service/operation/submission" "github.com/tbd54566975/ssi-service/pkg/service/presentation/model" prestorage "github.com/tbd54566975/ssi-service/pkg/service/presentation/storage" "github.com/tbd54566975/ssi-service/pkg/storage" @@ -58,15 +59,15 @@ func (s Service) GetOperations(request GetOperationsRequest) (*GetOperationsResp logrus.WithError(err).WithField("operation_id", op.ID).Error("converting to storage operations to model") continue } - resp.Operations[i] = newOp + resp.Operations[i] = *newOp } return resp, nil } type ServiceModelFunc func(any) any -func serviceModel(op opstorage.StoredOperation) (Operation, error) { - newOp := Operation{ +func serviceModel(op opstorage.StoredOperation) (*Operation, error) { + newOp := &Operation{ ID: op.ID, Done: op.Done, Result: Result{ @@ -76,34 +77,46 @@ func serviceModel(op opstorage.StoredOperation) (Operation, error) { if len(op.Response) > 0 { switch { - case strings.HasPrefix(op.ID, opstorage.SubmissionParentResource): + case strings.HasPrefix(op.ID, submission.ParentResource): var s prestorage.StoredSubmission if err := json.Unmarshal(op.Response, &s); err != nil { - return Operation{}, err + return nil, err } newOp.Result.Response = model.ServiceModel(&s) default: - return newOp, errors.New("unknown response type") + return nil, errors.New("unknown response type") } } return newOp, nil } -func (s Service) GetOperation(request GetOperationRequest) (Operation, error) { +func (s Service) GetOperation(request GetOperationRequest) (*Operation, error) { if err := request.Validate(); err != nil { - return Operation{}, errors.Wrap(err, "invalid request") + return nil, errors.Wrap(err, "invalid request") } storedOp, err := s.storage.GetOperation(request.ID) if err != nil { - return Operation{}, errors.Wrap(err, "fetching from storage") + return nil, errors.Wrap(err, "fetching from storage") } return serviceModel(storedOp) } +func (s Service) CancelOperation(request CancelOperationRequest) (*Operation, error) { + if err := request.Validate(); err != nil { + return nil, errors.Wrap(err, "invalid request") + } + + storedOp, err := s.storage.CancelOperation(request.ID) + if err != nil { + return nil, errors.Wrap(err, "marking as done") + } + return serviceModel(*storedOp) +} + func NewOperationService(s storage.ServiceStorage) (*Service, error) { - opStorage, err := opstorage.NewOperationStorage(s) + opStorage, err := NewOperationStorage(s) if err != nil { return nil, util.LoggingErrorMsg(err, "creating operation storage") } diff --git a/pkg/service/operation/storage/bolt.go b/pkg/service/operation/storage/bolt.go deleted file mode 100644 index 4952bbe66..000000000 --- a/pkg/service/operation/storage/bolt.go +++ /dev/null @@ -1,112 +0,0 @@ -package storage - -import ( - "strings" - - "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" - "go.einride.tech/aip/filtering" -) - -const ( - namespace = "operation_submission" -) - -const SubmissionParentResource = "/presentations/submissions" - -// NamespaceFromID returns a namespace from a given operation ID. An empty string is returned when the namespace cannot -// be determined. -func NamespaceFromID(id string) string { - i := strings.LastIndex(id, "/") - if i == -1 { - return "" - } - return namespaceFromParent(id[:i]) -} - -func namespaceFromParent(parent string) string { - switch parent { - case SubmissionParentResource: - return namespace - default: - return "" - } -} - -type BoltOperationStorage struct { - db *storage.BoltDB -} - -func (b BoltOperationStorage) StoreOperation(op StoredOperation) error { - id := op.ID - if id == "" { - return util.LoggingNewError("ID is required for storing operations") - } - jsonBytes, err := json.Marshal(op) - if err != nil { - return util.LoggingErrorMsgf(err, "marshalling operation with id: %s", id) - } - return b.db.Write(NamespaceFromID(id), id, jsonBytes) -} - -func (b BoltOperationStorage) GetOperation(id string) (StoredOperation, error) { - var stored StoredOperation - jsonBytes, err := b.db.Read(NamespaceFromID(id), id) - if err != nil { - return stored, util.LoggingErrorMsgf(err, "reading operation with id: %s", id) - } - if len(jsonBytes) == 0 { - return stored, util.LoggingNewErrorf("operation not found with id: %s", id) - } - if err := json.Unmarshal(jsonBytes, &stored); err != nil { - return stored, util.LoggingErrorMsgf(err, "unmarshalling stored operation: %s", id) - } - return stored, nil -} - -func (b BoltOperationStorage) GetOperations(parent string, filter filtering.Filter) ([]StoredOperation, error) { - operations, err := b.db.ReadAll(namespaceFromParent(parent)) - if err != nil { - return nil, util.LoggingErrorMsgf(err, "could not get all operations") - } - - shouldInclude, err := storage.NewIncludeFunc(filter) - if err != nil { - return nil, err - } - stored := make([]StoredOperation, 0, len(operations)) - for i, manifestBytes := range operations { - var nextOp StoredOperation - if err = json.Unmarshal(manifestBytes, &nextOp); err != nil { - logrus.WithError(err).WithField("idx", i).Warnf("Skipping operation") - } - include, err := shouldInclude(nextOp) - // We explicitly ignore evaluation errors and simply include them in the result. - if err != nil { - stored = append(stored, nextOp) - continue - } - if include { - stored = append(stored, nextOp) - } - } - return stored, nil -} - -func (b BoltOperationStorage) DeleteOperation(id string) error { - if err := b.db.Delete(NamespaceFromID(id), id); err != nil { - return util.LoggingErrorMsgf(err, "deleting operation: %s", id) - } - return nil -} - -func NewBoltOperationStorage(db *storage.BoltDB) (*BoltOperationStorage, error) { - if db == nil { - return nil, errors.New("bolt db reference is nil") - } - return &BoltOperationStorage{db: db}, nil - -} diff --git a/pkg/service/operation/storage/namespace/namespace.go b/pkg/service/operation/storage/namespace/namespace.go new file mode 100644 index 000000000..6198e9398 --- /dev/null +++ b/pkg/service/operation/storage/namespace/namespace.go @@ -0,0 +1,30 @@ +package namespace + +import ( + "strings" + + "github.com/tbd54566975/ssi-service/pkg/service/operation/submission" +) + +const namespace = "operation_submission" + +// FromID returns a namespace from a given operation ID. An empty string is returned when the namespace cannot +// be determined. +func FromID(id string) string { + i := strings.LastIndex(id, "/") + if i == -1 { + return "" + } + return FromParent(id[:i]) +} + +// FromParent returns a namespace from a given parent resource name like "/presentations/submissions". Empty is returned +// when the parent resource cannot be resolved. +func FromParent(parent string) string { + switch parent { + case submission.ParentResource: + return namespace + default: + return "" + } +} diff --git a/pkg/service/operation/storage/storage.go b/pkg/service/operation/storage/storage.go index e4e9b3c41..d56f4d9e4 100644 --- a/pkg/service/operation/storage/storage.go +++ b/pkg/service/operation/storage/storage.go @@ -1,8 +1,6 @@ package storage import ( - "github.com/tbd54566975/ssi-service/internal/util" - "github.com/tbd54566975/ssi-service/pkg/storage" "go.einride.tech/aip/filtering" ) @@ -35,21 +33,5 @@ type Storage interface { GetOperation(id string) (StoredOperation, error) GetOperations(parent string, filter filtering.Filter) ([]StoredOperation, error) DeleteOperation(id string) error -} - -func NewOperationStorage(s storage.ServiceStorage) (Storage, error) { - switch s.Type() { - case storage.Bolt: - gotBolt, ok := s.(*storage.BoltDB) - if !ok { - return nil, util.LoggingNewErrorf("trouble instantiating : %s", s.Type()) - } - boltStorage, err := NewBoltOperationStorage(gotBolt) - if err != nil { - return nil, util.LoggingErrorMsg(err, "could not instantiate schema bolt storage") - } - return boltStorage, err - default: - return nil, util.LoggingNewErrorf("unsupported storage type: %s", s.Type()) - } + CancelOperation(id string) (*StoredOperation, error) } diff --git a/pkg/service/operation/submission/submission.go b/pkg/service/operation/submission/submission.go new file mode 100644 index 000000000..94adf3561 --- /dev/null +++ b/pkg/service/operation/submission/submission.go @@ -0,0 +1,87 @@ +package submission + +import ( + "fmt" + "strings" + + "github.com/goccy/go-json" + "github.com/pkg/errors" + opstorage "github.com/tbd54566975/ssi-service/pkg/service/operation/storage" + "github.com/tbd54566975/ssi-service/pkg/storage" +) + +// IDFromSubmissionID returns a submission operation ID from the submission ID. +func IDFromSubmissionID(id string) string { + return fmt.Sprintf("%s/%s", ParentResource, id) +} + +// ID attempts to parse the submission id from the ID of the operation. This is done by taking the last word +// that results from splitting the id by "/". On failures, the empty string is returned. +func ID(opID string) string { + i := strings.LastIndex(opID, "/") + if i == -1 { + return "" + } + return opID[(i + 1):] +} + +const ( + // Namespace is the namespace to be used for storing submissions. + Namespace = "presentation_submission" + // ParentResource is the prefix of the submission parent resource. + ParentResource = "/presentations/submissions" +) + +// Status indicates the current state of a submission. +type Status uint8 + +func (s Status) String() string { + switch s { + case StatusPending: + return "pending" + case StatusDenied: + return "denied" + case StatusApproved: + return "approved" + case StatusCancelled: + return "cancelled" + default: + return "unknown" + } +} + +const ( + StatusUnknown Status = iota + StatusPending + StatusCancelled + StatusDenied + StatusApproved +) + +// OperationUpdater is an implementation of the storage.ResponseSettingUpdater. It's provides a way to update the +// operation and submission within a single transaction. +type OperationUpdater struct { + storage.UpdaterWithMap +} + +func (u OperationUpdater) SetUpdatedResponse(response []byte) { + if u.UpdaterWithMap.Values == nil { + return + } + u.UpdaterWithMap.Values["response"] = response +} + +func (u OperationUpdater) Validate(v []byte) error { + var op opstorage.StoredOperation + if err := json.Unmarshal(v, &op); err != nil { + return errors.Wrap(err, "unmarshalling operation") + } + + if op.Done { + return errors.New("operation already marked as done") + } + + return nil +} + +var _ storage.ResponseSettingUpdater = (*OperationUpdater)(nil) diff --git a/pkg/service/operation/model_test.go b/pkg/service/operation/submission/submission_test.go similarity index 89% rename from pkg/service/operation/model_test.go rename to pkg/service/operation/submission/submission_test.go index db8084cd9..14b43b1ef 100644 --- a/pkg/service/operation/model_test.go +++ b/pkg/service/operation/submission/submission_test.go @@ -1,4 +1,4 @@ -package operation +package submission import ( "testing" @@ -30,7 +30,7 @@ func TestSubmissionID(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - assert.Equal(t, tt.want, SubmissionID(tt.opID)) + assert.Equal(t, tt.want, ID(tt.opID)) }) } } diff --git a/pkg/service/presentation/model/model.go b/pkg/service/presentation/model/model.go index 47cada2ef..edbbfa60f 100644 --- a/pkg/service/presentation/model/model.go +++ b/pkg/service/presentation/model/model.go @@ -67,7 +67,7 @@ type ListSubmissionRequest struct { } type Submission struct { - // One of {`pending`, `approved`, `denied`}. + // One of {`pending`, `approved`, `denied`, `cancelled`}. Status string `json:"status" validate:"required"` // The reason why the submission was approved or denied. Reason string `json:"reason"` diff --git a/pkg/service/presentation/service.go b/pkg/service/presentation/service.go index 53605fc29..7e80f2741 100644 --- a/pkg/service/presentation/service.go +++ b/pkg/service/presentation/service.go @@ -16,6 +16,7 @@ import ( "github.com/tbd54566975/ssi-service/pkg/service/framework" "github.com/tbd54566975/ssi-service/pkg/service/operation" opstorage "github.com/tbd54566975/ssi-service/pkg/service/operation/storage" + "github.com/tbd54566975/ssi-service/pkg/service/operation/submission" "github.com/tbd54566975/ssi-service/pkg/service/presentation/model" presentationstorage "github.com/tbd54566975/ssi-service/pkg/service/presentation/storage" "github.com/tbd54566975/ssi-service/pkg/service/schema" @@ -58,7 +59,7 @@ func NewPresentationService(config config.PresentationServiceConfig, s storage.S if err != nil { return nil, util.LoggingErrorMsg(err, "could not instantiate definition storage for the presentation service") } - opsStorage, err := opstorage.NewOperationStorage(s) + opsStorage, err := operation.NewOperationStorage(s) if err != nil { return nil, util.LoggingErrorMsg(err, "could not instantiate storage for the operations") } @@ -177,7 +178,7 @@ func (s Service) CreateSubmission(request model.CreateSubmissionRequest) (*opera } storedSubmission := presentationstorage.StoredSubmission{ - Status: presentationstorage.StatusPending, + Status: submission.StatusPending, Submission: request.Submission, } @@ -186,7 +187,7 @@ func (s Service) CreateSubmission(request model.CreateSubmissionRequest) (*opera return nil, errors.Wrap(err, "could not store presentation") } - opID := operation.IDFromSubmissionID(storedSubmission.Submission.ID) + opID := submission.IDFromSubmissionID(storedSubmission.Submission.ID) storedOp := opstorage.StoredOperation{ ID: opID, Done: false, @@ -235,7 +236,7 @@ func (s Service) ReviewSubmission(request model.ReviewSubmissionRequest) (*model return nil, errors.Wrap(err, "invalid request") } - updatedSubmission, _, err := s.storage.UpdateSubmission(request.ID, request.Approved, request.Reason, operation.IDFromSubmissionID(request.ID)) + updatedSubmission, _, err := s.storage.UpdateSubmission(request.ID, request.Approved, request.Reason, submission.IDFromSubmissionID(request.ID)) if err != nil { return nil, errors.Wrap(err, "updating submission") } diff --git a/pkg/service/presentation/storage/bolt.go b/pkg/service/presentation/storage/bolt.go index 7556aacfb..2d68a0637 100644 --- a/pkg/service/presentation/storage/bolt.go +++ b/pkg/service/presentation/storage/bolt.go @@ -8,58 +8,36 @@ import ( "github.com/sirupsen/logrus" "github.com/tbd54566975/ssi-service/internal/util" opstorage "github.com/tbd54566975/ssi-service/pkg/service/operation/storage" + "github.com/tbd54566975/ssi-service/pkg/service/operation/storage/namespace" + "github.com/tbd54566975/ssi-service/pkg/service/operation/submission" "github.com/tbd54566975/ssi-service/pkg/storage" "go.einride.tech/aip/filtering" ) const ( - namespace = "presentation_definition" - submissionNamespace = "presentation_submission" + presentationDefinitionNamespace = "presentation_definition" ) type BoltPresentationStorage struct { db *storage.BoltDB } -type opUpdater struct { - storage.UpdaterWithMap -} - -func (u opUpdater) SetUpdatedResponse(response []byte) { - u.UpdaterWithMap.Values["response"] = response -} - -func (u opUpdater) Validate(v []byte) error { - var op opstorage.StoredOperation - if err := json.Unmarshal(v, &op); err != nil { - return errors.Wrap(err, "unmarshalling operation") - } - - if op.Done { - return errors.New("operation already marked as done") - } - - return nil -} - -var _ storage.ResponseSettingUpdater = (*opUpdater)(nil) - func (b BoltPresentationStorage) UpdateSubmission(id string, approved bool, reason string, opID string) (StoredSubmission, opstorage.StoredOperation, error) { m := map[string]any{ - "status": StatusDenied, + "status": submission.StatusDenied, "reason": reason, } if approved { - m["status"] = StatusApproved + m["status"] = submission.StatusApproved } submissionData, operationData, err := b.db.UpdateValueAndOperation( - submissionNamespace, + submission.Namespace, id, storage.NewUpdater(m), - opstorage.NamespaceFromID(opID), + namespace.FromID(opID), opID, - opUpdater{ - storage.NewUpdater(map[string]any{ + submission.OperationUpdater{ + UpdaterWithMap: storage.NewUpdater(map[string]any{ "done": true, }), }) @@ -79,7 +57,7 @@ func (b BoltPresentationStorage) UpdateSubmission(id string, approved bool, reas } func (b BoltPresentationStorage) ListSubmissions(filter filtering.Filter) ([]StoredSubmission, error) { - allData, err := b.db.ReadAll(submissionNamespace) + allData, err := b.db.ReadAll(submission.Namespace) if err != nil { return nil, errors.Wrap(err, "reading all data") } @@ -127,11 +105,11 @@ func (b BoltPresentationStorage) StorePresentation(presentation StoredPresentati logrus.WithError(err).Error(errMsg) return errors.Wrapf(err, errMsg) } - return b.db.Write(namespace, id, jsonBytes) + return b.db.Write(presentationDefinitionNamespace, id, jsonBytes) } func (b BoltPresentationStorage) GetPresentation(id string) (*StoredPresentation, error) { - jsonBytes, err := b.db.Read(namespace, id) + jsonBytes, err := b.db.Read(presentationDefinitionNamespace, id) if err != nil { return nil, util.LoggingErrorMsgf(err, "could not get presentation definition: %s", id) } @@ -146,28 +124,28 @@ func (b BoltPresentationStorage) GetPresentation(id string) (*StoredPresentation } func (b BoltPresentationStorage) DeletePresentation(id string) error { - if err := b.db.Delete(namespace, id); err != nil { + if err := b.db.Delete(presentationDefinitionNamespace, id); err != nil { return util.LoggingNewErrorf("could not delete presentation definition: %s", id) } return nil } -func (b BoltPresentationStorage) StoreSubmission(submission StoredSubmission) error { - id := submission.Submission.ID +func (b BoltPresentationStorage) StoreSubmission(s StoredSubmission) error { + id := s.Submission.ID if id == "" { err := errors.New("could not store submission definition without an ID") logrus.WithError(err).Error() return err } - jsonBytes, err := json.Marshal(submission) + jsonBytes, err := json.Marshal(s) if err != nil { return util.LoggingNewErrorf("could not store submission definition: %s", id) } - return b.db.Write(submissionNamespace, id, jsonBytes) + return b.db.Write(submission.Namespace, id, jsonBytes) } func (b BoltPresentationStorage) GetSubmission(id string) (*StoredSubmission, error) { - jsonBytes, err := b.db.Read(submissionNamespace, id) + jsonBytes, err := b.db.Read(submission.Namespace, id) if err != nil { return nil, util.LoggingNewErrorf("could not get submission definition: %s", id) } diff --git a/pkg/service/presentation/storage/storage.go b/pkg/service/presentation/storage/storage.go index 97f4dc173..a8324eddd 100644 --- a/pkg/service/presentation/storage/storage.go +++ b/pkg/service/presentation/storage/storage.go @@ -5,6 +5,7 @@ import ( "github.com/pkg/errors" "github.com/tbd54566975/ssi-service/internal/util" opstorage "github.com/tbd54566975/ssi-service/pkg/service/operation/storage" + "github.com/tbd54566975/ssi-service/pkg/service/operation/submission" "github.com/tbd54566975/ssi-service/pkg/storage" "go.einride.tech/aip/filtering" ) @@ -44,30 +45,8 @@ func NewPresentationStorage(s storage.ServiceStorage) (Storage, error) { } } -type Status uint8 - -func (s Status) String() string { - switch s { - case StatusPending: - return "pending" - case StatusDenied: - return "denied" - case StatusApproved: - return "approved" - default: - return "unknown" - } -} - -const ( - StatusUnknown Status = iota - StatusPending - StatusDenied - StatusApproved -) - type StoredSubmission struct { - Status Status `json:"status"` + Status submission.Status `json:"status"` Submission exchange.PresentationSubmission `json:"submission"` Reason string `json:"reason"` }