Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

adding new error response object #240

Merged
merged 5 commits into from
Oct 25, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
63 changes: 38 additions & 25 deletions credential/manifest/model.go
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
package manifest

import (
"fmt"
"reflect"
"strings"

errresp "github.com/TBD54566975/ssi-sdk/error"

"github.com/TBD54566975/ssi-sdk/credential/exchange"
"github.com/TBD54566975/ssi-sdk/credential/rendering"
credutil "github.com/TBD54566975/ssi-sdk/credential/util"
Expand Down Expand Up @@ -168,30 +169,34 @@ func IsValidCredentialApplicationForManifest(cm CredentialManifest, applicationA
// parse out the application to its known object model
applicationJSON, ok := applicationAndCredsJSON[CredentialApplicationJSONProperty]
if !ok {
return errors.New("credential_application property not found")
return errresp.NewErrorResponse(errresp.ApplicationError, "credential_application property not found")
}

applicationBytes, err := json.Marshal(applicationJSON)
if err != nil {
return errors.Wrap(err, "failed to marshal credential application")
wrapped := errors.Wrap(err, "failed to marshal credential application")
return errresp.NewErrorResponseWithError(errresp.CriticalError, wrapped)
}
var ca CredentialApplication
if err = json.Unmarshal(applicationBytes, &ca); err != nil {
return errors.Wrap(err, "failed to unmarshal credential application")
wrapped := errors.Wrap(err, "failed to unmarshal credential application")
return errresp.NewErrorResponseWithError(errresp.CriticalError, wrapped)
}

// Basic Validation Checks
if err = cm.IsValid(); err != nil {
return errors.Wrap(err, "credential manifest is not valid")
wrapped := errors.Wrap(err, "credential manifest is not valid")
return errresp.NewErrorResponseWithError(errresp.ApplicationError, wrapped)
}

if err = ca.IsValid(); err != nil {
return errors.Wrap(err, "credential application is not valid")
wrapped := errors.Wrap(err, "credential application is not valid")
return errresp.NewErrorResponseWithError(errresp.ApplicationError, wrapped)
}

// The object MUST contain a manifest_id property. The value of this property MUST be the id of a valid Credential Manifest.
if cm.ID != ca.ManifestID {
return fmt.Errorf("the credential application's manifest id: %s must be equal to the credential manifest's id: %s", cm.ID, ca.ManifestID)
return errresp.NewErrorResponsef(errresp.ApplicationError, "the credential application's manifest id: %s must be equal to the credential manifest's id: %s", ca.ManifestID, cm.ID)
}

// The ca must have a format property if the related Credential Manifest specifies a format property.
Expand All @@ -204,36 +209,38 @@ func IsValidCredentialApplicationForManifest(cm CredentialManifest, applicationA
}

for _, format := range ca.Format.FormatValues() {
if _, ok = cmFormats[format]; !ok {
return errors.New("credential application's format must be a subset of the format property in the credential manifest")
if _, ok := cmFormats[format]; !ok {
return errresp.NewErrorResponse(errresp.ApplicationError, "credential application's format must be a subset of the format property in the credential manifest")
}
}
}

if (cm.PresentationDefinition != nil && len(cm.PresentationDefinition.InputDescriptors) > 0) &&
(ca.PresentationSubmission == nil || len(ca.PresentationSubmission.DescriptorMap) == 0) {
return fmt.Errorf("no descriptors provided for application: %s against manifest: %s", ca.ID, cm.ID)
return errresp.NewErrorResponsef(errresp.ApplicationError, "no descriptors provided for application: %s against manifest: %s", ca.ID, cm.ID)
}

// The Credential Application object MUST contain a presentation_submission property IF the related Credential Manifest contains a presentation_definition.
// Its value MUST be a valid Presentation Submission:
if !cm.PresentationDefinition.IsEmpty() {
if ca.PresentationSubmission.IsEmpty() {
return errors.New("credential application's presentation submission cannot be empty because the credential manifest's presentation definition is not empty")
return errresp.NewErrorResponse(errresp.ApplicationError, "credential application's presentation submission cannot be empty because the credential manifest's presentation definition is not empty")
}

if err = cm.PresentationDefinition.IsValid(); err != nil {
return errors.Wrap(err, "credential manifest's presentation definition is not valid")
wrapped := errors.Wrap(err, "credential manifest's presentation definition is not valid")
return errresp.NewErrorResponseWithError(errresp.ApplicationError, wrapped)
}

if err = ca.PresentationSubmission.IsValid(); err != nil {
return errors.Wrap(err, "credential application's presentation submission is not valid")
wrapped := errors.Wrap(err, "credential application's presentation submission is not valid")
return errresp.NewErrorResponseWithError(errresp.ApplicationError, wrapped)
}

// https://identity.foundation/presentation-exchange/#presentation-submission
// The presentation_submission object MUST contain a definition_id property. The value of this property MUST be the id value of a valid Presentation Definition.
if cm.PresentationDefinition.ID != ca.PresentationSubmission.DefinitionID {
return fmt.Errorf("credential application's presentation submission's definition id: %s does not match the credential manifest's id: %s", cm.PresentationDefinition.ID, ca.PresentationSubmission.DefinitionID)
return errresp.NewErrorResponsef(errresp.ApplicationError, "credential application's presentation submission's definition id: %s does not match the credential manifest's id: %s", ca.PresentationSubmission.DefinitionID, cm.PresentationDefinition.ID)
}

// The descriptor_map object MUST include a format property. The value of this property MUST be a string that matches one of the Claim Format Designation. This denotes the data format of the Claim.
Expand All @@ -244,12 +251,12 @@ func IsValidCredentialApplicationForManifest(cm CredentialManifest, applicationA

for _, submissionDescriptor := range ca.PresentationSubmission.DescriptorMap {
if _, ok := claimFormats[submissionDescriptor.Format]; !ok {
return errors.New("claim format is invalid or not supported")
return errresp.NewErrorResponse(errresp.ApplicationError, "claim format is invalid or not supported")
}

// The descriptor_map object MUST include a path property. The value of this property MUST be a JSONPath string expression.
if _, err := jsonpath.Compile(submissionDescriptor.Path); err != nil {
return fmt.Errorf("invalid json path: %s", submissionDescriptor.Path)
return errresp.NewErrorResponsef(errresp.ApplicationError, "invalid json path: %s", submissionDescriptor.Path)
}
}

Expand All @@ -263,35 +270,38 @@ func IsValidCredentialApplicationForManifest(cm CredentialManifest, applicationA
for _, inputDescriptor := range cm.PresentationDefinition.InputDescriptors {
submissionDescriptor, ok := submissionDescriptorLookup[inputDescriptor.ID]
if !ok {
return fmt.Errorf("unfulfilled input descriptor<%s>; submission not valid", inputDescriptor.ID)
return errresp.NewErrorResponsef(errresp.ApplicationError, "unfulfilled input descriptor<%s>; submission not valid", inputDescriptor.ID)
}

// if the format on the submitted claim does not match the input descriptor, we cannot process
if inputDescriptor.Format != nil && !util.Contains(submissionDescriptor.Format, inputDescriptor.Format.FormatValues()) {
return fmt.Errorf("for input descriptor<%s>, the format of submission descriptor<%s> is not one"+
return errresp.NewErrorResponsef(errresp.ApplicationError, "for input descriptor<%s>, the format of submission descriptor<%s> is not one"+
" of the supported formats: %s", inputDescriptor.ID, submissionDescriptor.Format,
strings.Join(inputDescriptor.Format.FormatValues(), ", "))
}

// TODO(gabe) support nested paths in presentation submissions
// https://github.com/TBD54566975/ssi-sdk/issues/73
if submissionDescriptor.PathNested != nil {
return fmt.Errorf("submission with nested paths not supported: %s", submissionDescriptor.ID)
return errresp.NewErrorResponsef(errresp.ApplicationError, "submission with nested paths not supported: %s", submissionDescriptor.ID)
}

// resolve the claim from the JSON path expression in the submission descriptor
submittedClaim, err := jsonpath.JsonPathLookup(applicationAndCredsJSON, submissionDescriptor.Path)
if err != nil {
return errors.Wrapf(err, "could not resolve claim from submission descriptor<%s> with path: %s", submissionDescriptor.ID, submissionDescriptor.Path)
wrapped := errors.Wrapf(err, "could not resolve claim from submission descriptor<%s> with path: %s", submissionDescriptor.ID, submissionDescriptor.Path)
return errresp.NewErrorResponseWithError(errresp.ApplicationError, wrapped)
}

// convert submitted claim vc to map[string]interface{}
cred, err := credutil.CredentialsFromInterface(submittedClaim)
if err != nil {
return errors.Wrap(err, "failed to extract cred from json")
wrapped := errors.Wrap(err, "failed to extract cred from json")
return errresp.NewErrorResponseWithError(errresp.CriticalError, wrapped)
}
if err = cred.IsValid(); err != nil {
return errors.Wrap(err, "credential is not valid")
wrapped := errors.Wrap(err, "vc is not valid")
return errresp.NewErrorResponseWithError(errresp.ApplicationError, wrapped)
}

// verify the submitted claim complies with the input descriptor
Expand All @@ -306,14 +316,17 @@ func IsValidCredentialApplicationForManifest(cm CredentialManifest, applicationA
credMap := make(map[string]interface{})
claimBytes, err := json.Marshal(cred)
if err != nil {
return errors.Wrap(err, "failed to marshal submitted claim")
wrapped := errors.Wrap(err, "failed to marshal vc")
return errresp.NewErrorResponseWithError(errresp.CriticalError, wrapped)
}
if err = json.Unmarshal(claimBytes, &credMap); err != nil {
return errors.Wrap(err, "problem in unmarshalling credential")
wrapped := errors.Wrap(err, "problem in unmarshalling credential")
return errresp.NewErrorResponseWithError(errresp.CriticalError, wrapped)
}
for _, field := range inputDescriptor.Constraints.Fields {
if err = findMatchingPath(credMap, field.Path); err != nil {
return errors.Wrapf(err, "input descriptor<%s> not fulfilled for field: %s", inputDescriptor.ID, field.ID)
wrapped := errors.Wrapf(err, "input descriptor<%s> not fulfilled for field: %s", inputDescriptor.ID, field.ID)
return errresp.NewErrorResponseWithError(errresp.ApplicationError, wrapped)
}
}
}
Expand Down
7 changes: 3 additions & 4 deletions credential/manifest/model_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,7 @@ func TestCredentialResponse(t *testing.T) {
assert.JSONEq(tt, vector, string(roundTripBytes))
})

t.Run("Credential Response - Denial Vector 1", func(tt *testing.T) {
t.Run("Credential Error - Denial Vector 1", func(tt *testing.T) {
vector, err := getTestVector(ResponseVector2)
assert.NoError(tt, err)

Expand Down Expand Up @@ -258,7 +258,7 @@ func TestIsValidCredentialApplicationForManifest(t *testing.T) {
assert.NoError(tt, err)

err = IsValidCredentialApplicationForManifest(cm, request)
assert.Contains(t, err.Error(), "the credential application's manifest id: WA-DL-CLASS-A must be equal to the credential manifest's id: bad-id")
assert.Contains(t, err.Error(), "the credential application's manifest id: bad-id must be equal to the credential manifest's id: WA-DL-CLASS-A")

// reset
ca.CredentialApplication.ManifestID = cm.ID
Expand Down Expand Up @@ -326,7 +326,7 @@ func TestIsValidCredentialApplicationForManifest(t *testing.T) {
assert.NoError(tt, err)

err = IsValidCredentialApplicationForManifest(cm, request)
assert.Contains(t, err.Error(), "credential application's presentation submission's definition id: 32f54163-7166-48f1-93d8-ff217bdb0653 does not match the credential manifest's id: badid")
assert.Contains(t, err.Error(), "credential application's presentation submission's definition id: badid does not match the credential manifest's id: 32f54163-7166-48f1-93d8-ff217bdb0653")

// reset
cm, ca = getValidTestCredManifestCredApplication(tt)
Expand Down Expand Up @@ -503,7 +503,6 @@ func TestIsValidCredentialApplicationForManifest(t *testing.T) {
err = IsValidCredentialApplicationForManifest(cm, request)
assert.NoError(tt, err)
})

}

func getTestVector(fileName string) (string, error) {
Expand Down
69 changes: 69 additions & 0 deletions error/response.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
package error

import (
"fmt"

"github.com/pkg/errors"
)

type (
Type string
)

const (
ApplicationError Type = "ApplicationError"
CriticalError Type = "CriticalError"
UnknownError Type = "UnknownError"
)

type Response struct {
Valid bool
Err error
ErrorType Type
}

func (r *Response) Error() string {
return fmt.Sprintf("valid %v: err %v, error type: %s", r.Valid, r.Err, r.ErrorType)
}

func NewErrorResponse(errorType Type, errorMessage string) *Response {
return &Response{
Valid: isValid(errorType),
ErrorType: errorType,
Err: errors.New(errorMessage),
}
}

func NewErrorResponsef(errorType Type, format string, a ...interface{}) *Response {
return &Response{
Valid: false,
ErrorType: ApplicationError,
Err: errors.Errorf(format, a...),
}
}

func NewErrorResponseWithError(errorType Type, err error) *Response {
return &Response{
Valid: isValid(errorType),
ErrorType: errorType,
Err: err,
}
}

// GetErrorResponse will get the type of err, if the type is a ErrorResponse it will return that as an ErrorResponse, otherwise it will construct a new default ErrorResponse
func GetErrorResponse(err error) Response {
var errRes *Response

if errors.As(err, &errRes) {
return *errRes
}

return Response{Valid: false, Err: err, ErrorType: UnknownError}
}

func isValid(errorType Type) bool {
if errorType == ApplicationError {
return true
}
return false
}
64 changes: 64 additions & 0 deletions error/response_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
package error

import (
"testing"

"github.com/pkg/errors"
"github.com/stretchr/testify/assert"
)

func TestErrorResponse(t *testing.T) {
t.Run("simple error", func(tt *testing.T) {
err := standardErr()
resp := GetErrorResponse(err)

assert.Equal(tt, resp.Valid, false)
assert.Equal(tt, resp.ErrorType, UnknownError)
assert.Equal(tt, resp.Err, err)
})

t.Run("error response with string", func(tt *testing.T) {
err := errResponse()
resp := GetErrorResponse(err)

assert.Equal(tt, resp.Valid, true)
assert.Equal(tt, resp.ErrorType, ApplicationError)
assert.Equal(tt, resp.Err, err.(*Response).Err)
})

t.Run("error response with string", func(tt *testing.T) {
err := errResponseWithErr()
resp := GetErrorResponse(err)

assert.Equal(tt, resp.Valid, false)
assert.Equal(tt, resp.ErrorType, CriticalError)
assert.Equal(tt, resp.Err, err.(*Response).Err)
})

t.Run("error response with formatted string", func(tt *testing.T) {
err := errResponseWithFormattedMsg()
resp := GetErrorResponse(err)

assert.Equal(tt, resp.Valid, false)
assert.Equal(tt, resp.ErrorType, ApplicationError)
assert.Equal(tt, resp.Err, err.(*Response).Err)
assert.Contains(tt, resp.Error(), "the best number: 5")
})
}

func standardErr() error {
return errors.New("this is a normal error")
}

func errResponse() error {
return NewErrorResponse(ApplicationError, "this is error response with message")
}

func errResponseWithErr() error {
err := errors.New("this is error response with error")
return NewErrorResponseWithError(CriticalError, err)
}

func errResponseWithFormattedMsg() error {
return NewErrorResponsef(CriticalError, "check out this error and the best number: %d", 5)
}