diff --git a/vcr/pe/presentation_definition.go b/vcr/pe/presentation_definition.go index 19705eadbe..e6e72bd9d9 100644 --- a/vcr/pe/presentation_definition.go +++ b/vcr/pe/presentation_definition.go @@ -73,6 +73,32 @@ func (presentationDefinition PresentationDefinition) Match(vcs []vc.VerifiableCr return selectedVCs, descriptorMaps, nil } +// ResolveConstraintsFields returns a map where each of the InputDescriptor constraints field is mapped, +// to the corresponding value from the Verifiable Credentials that map to the InputDescriptor. +// The credentialMap is a map with the InputDescriptor.Id as key and the VerifiableCredential as value. +// Constraints that contain no ID are ignored. +func (presentationDefinition PresentationDefinition) ResolveConstraintsFields(credentialMap map[string]vc.VerifiableCredential) (map[string]interface{}, error) { + result := make(map[string]interface{}) + for inputDescriptorID, cred := range credentialMap { + // Find the input descriptor + var inputDescriptor InputDescriptor + for _, curr := range presentationDefinition.InputDescriptors { + if curr.Id == inputDescriptorID { + inputDescriptor = *curr + break + } + } + if inputDescriptor.Constraints == nil { + continue + } + _, values, _ := matchConstraint(inputDescriptor.Constraints, cred) + for key, value := range values { + result[key] = value + } + } + return result, nil +} + func (presentationDefinition PresentationDefinition) matchConstraints(vcs []vc.VerifiableCredential) ([]Candidate, error) { var candidates []Candidate @@ -275,7 +301,8 @@ func matchCredential(descriptor InputDescriptor, credential vc.VerifiableCredent // for each constraint in descriptor.constraints: // a vc must match the constraint if descriptor.Constraints != nil { - return matchConstraint(descriptor.Constraints, credential) + matches, _, err := matchConstraint(descriptor.Constraints, credential) + return matches, err } return true, nil } @@ -284,7 +311,8 @@ func matchCredential(descriptor InputDescriptor, credential vc.VerifiableCredent // All Fields need to match according to the Field rules. // IsHolder, SameSubject, SubjectIsIssuer, Statuses are not supported for now. // LimitDisclosure is not supported for now. -func matchConstraint(constraint *Constraints, credential vc.VerifiableCredential) (bool, error) { +// If the constraint matches, it returns true and a map containing constraint field IDs and matched values. +func matchConstraint(constraint *Constraints, credential vc.VerifiableCredential) (bool, map[string]interface{}, error) { // jsonpath works on interfaces, so convert the VC to an interface var credentialAsMap map[string]interface{} var err error @@ -298,26 +326,31 @@ func matchConstraint(constraint *Constraints, credential vc.VerifiableCredential credentialAsMap, err = remarshalToMap(credential) } if err != nil { - return false, err + return false, nil, err } // for each field in constraint.fields: // a vc must match the field + values := make(map[string]interface{}) for _, field := range constraint.Fields { - match, err := matchField(field, credentialAsMap) + match, value, err := matchField(field, credentialAsMap) if err != nil { - return false, err + return false, nil, err } if !match { - return false, nil + return false, nil, nil + } + if field.Id != nil { + values[*field.Id] = value } } - return true, nil + return true, values, nil } // matchField matches the field against the VC. +// If the field matches, it returns true and the matched value. The matched value can be nil if the field is optional. // All fields need to match unless optional is set to true and no values are found for all the paths. -func matchField(field Field, credential map[string]interface{}) (bool, error) { +func matchField(field Field, credential map[string]interface{}) (bool, interface{}, error) { // for each path in field.paths: // a vc must match one of the path var optionalInvalid int @@ -325,23 +358,23 @@ func matchField(field Field, credential map[string]interface{}) (bool, error) { // if path is not found continue value, err := getValueAtPath(path, credential) if err != nil { - return false, err + return false, nil, err } if value == nil { continue } if field.Filter == nil { - return true, nil + return true, value, nil } // if filter at path matches return true match, err := matchFilter(*field.Filter, value) if err != nil { - return false, err + return false, nil, err } if match { - return true, nil + return true, value, nil } // if filter at path does not match continue and set optionalInvalid optionalInvalid++ @@ -349,9 +382,9 @@ func matchField(field Field, credential map[string]interface{}) (bool, error) { // no matches, check optional. Optional is only valid if all paths returned no results // not if a filter did not match if field.Optional != nil && *field.Optional && optionalInvalid == 0 { - return true, nil + return true, nil, nil } - return false, nil + return false, nil, nil } // getValueAtPath uses the JSON path expression to get the value from the VC diff --git a/vcr/pe/presentation_definition_test.go b/vcr/pe/presentation_definition_test.go index eef3cb75db..4172c32ec6 100644 --- a/vcr/pe/presentation_definition_test.go +++ b/vcr/pe/presentation_definition_test.go @@ -24,6 +24,7 @@ import ( "crypto/rand" "embed" "encoding/json" + "fmt" "github.com/nuts-foundation/nuts-node/vcr/credential" "testing" @@ -443,39 +444,54 @@ func Test_matchConstraint(t *testing.T) { testCredential := vc.VerifiableCredential{} _ = json.Unmarshal([]byte(testCredentialString), &testCredential) + credSubjectFieldID := "credential_subject_field" typeVal := "VerifiableCredential" - f1True := Field{Path: []string{"$.credentialSubject.field"}} + f1True := Field{Id: &credSubjectFieldID, Path: []string{"$.credentialSubject.field"}} + f1TrueWithoutID := Field{Path: []string{"$.credentialSubject.field"}} f2True := Field{Path: []string{"$.type"}, Filter: &Filter{Type: "string", Const: &typeVal}} f3False := Field{Path: []string{"$.credentialSubject.field"}, Filter: &Filter{Type: "string", Const: &typeVal}} + fieldMap := map[string]interface{}{credSubjectFieldID: "value"} t.Run("single constraint match", func(t *testing.T) { - match, err := matchConstraint(&Constraints{Fields: []Field{f1True}}, testCredential) + match, value, err := matchConstraint(&Constraints{Fields: []Field{f1True}}, testCredential) require.NoError(t, err) + assert.Equal(t, fieldMap, value) + assert.True(t, match) + }) + t.Run("field match without ID is not included in values map", func(t *testing.T) { + match, values, err := matchConstraint(&Constraints{Fields: []Field{f1TrueWithoutID}}, testCredential) + + require.NoError(t, err) + assert.Empty(t, values) assert.True(t, match) }) t.Run("single constraint mismatch", func(t *testing.T) { - match, err := matchConstraint(&Constraints{Fields: []Field{f3False}}, testCredential) + match, values, err := matchConstraint(&Constraints{Fields: []Field{f3False}}, testCredential) require.NoError(t, err) + assert.Nil(t, values) assert.False(t, match) }) t.Run("multi constraint match", func(t *testing.T) { - match, err := matchConstraint(&Constraints{Fields: []Field{f1True, f2True}}, testCredential) + match, values, err := matchConstraint(&Constraints{Fields: []Field{f1True, f2True}}, testCredential) require.NoError(t, err) + assert.Equal(t, fieldMap, values) assert.True(t, match) }) t.Run("multi constraint, single mismatch", func(t *testing.T) { - match, err := matchConstraint(&Constraints{Fields: []Field{f1True, f3False}}, testCredential) + match, values, err := matchConstraint(&Constraints{Fields: []Field{f1True, f3False}}, testCredential) require.NoError(t, err) + assert.Nil(t, values) assert.False(t, match) }) t.Run("error", func(t *testing.T) { - match, err := matchConstraint(&Constraints{Fields: []Field{{Path: []string{"$$"}}}}, testCredential) + match, values, err := matchConstraint(&Constraints{Fields: []Field{{Path: []string{"$$"}}}}, testCredential) require.Error(t, err) + assert.Nil(t, values) assert.False(t, match) }) } @@ -486,50 +502,57 @@ func Test_matchField(t *testing.T) { testCredentialMap, _ := remarshalToMap(testCredential) t.Run("single path match", func(t *testing.T) { - match, err := matchField(Field{Path: []string{"$.credentialSubject.field"}}, testCredentialMap) + match, value, err := matchField(Field{Path: []string{"$.credentialSubject.field"}}, testCredentialMap) require.NoError(t, err) + assert.Equal(t, "value", value) assert.True(t, match) }) t.Run("multi path match", func(t *testing.T) { - match, err := matchField(Field{Path: []string{"$.other", "$.credentialSubject.field"}}, testCredentialMap) + match, value, err := matchField(Field{Path: []string{"$.other", "$.credentialSubject.field"}}, testCredentialMap) require.NoError(t, err) + assert.Equal(t, "value", value) assert.True(t, match) }) t.Run("no match", func(t *testing.T) { - match, err := matchField(Field{Path: []string{"$.foo", "$.bar"}}, testCredentialMap) + match, value, err := matchField(Field{Path: []string{"$.foo", "$.bar"}}, testCredentialMap) require.NoError(t, err) + assert.Nil(t, value) assert.False(t, match) }) t.Run("no match, but optional", func(t *testing.T) { trueVal := true - match, err := matchField(Field{Path: []string{"$.foo", "$.bar"}, Optional: &trueVal}, testCredentialMap) + match, value, err := matchField(Field{Path: []string{"$.foo", "$.bar"}, Optional: &trueVal}, testCredentialMap) require.NoError(t, err) + assert.Nil(t, value) assert.True(t, match) }) t.Run("invalid match and optional", func(t *testing.T) { trueVal := true stringVal := "bar" - match, err := matchField(Field{Path: []string{"$.credentialSubject.field", "$.foo"}, Optional: &trueVal, Filter: &Filter{Const: &stringVal}}, testCredentialMap) + match, value, err := matchField(Field{Path: []string{"$.credentialSubject.field", "$.foo"}, Optional: &trueVal, Filter: &Filter{Const: &stringVal}}, testCredentialMap) require.NoError(t, err) + assert.Nil(t, value) assert.False(t, match) }) t.Run("valid match with Filter", func(t *testing.T) { stringVal := "value" - match, err := matchField(Field{Path: []string{"$.credentialSubject.field"}, Filter: &Filter{Type: "string", Const: &stringVal}}, testCredentialMap) + match, value, err := matchField(Field{Path: []string{"$.credentialSubject.field"}, Filter: &Filter{Type: "string", Const: &stringVal}}, testCredentialMap) require.NoError(t, err) + assert.Equal(t, stringVal, value) assert.True(t, match) }) t.Run("match on type", func(t *testing.T) { stringVal := "VerifiableCredential" - match, err := matchField(Field{Path: []string{"$.type"}, Filter: &Filter{Type: "string", Const: &stringVal}}, testCredentialMap) + match, value, err := matchField(Field{Path: []string{"$.type"}, Filter: &Filter{Type: "string", Const: &stringVal}}, testCredentialMap) require.NoError(t, err) + assert.Equal(t, stringVal, value) assert.True(t, match) }) t.Run("match on type array", func(t *testing.T) { @@ -542,24 +565,27 @@ func Test_matchField(t *testing.T) { }` _ = json.Unmarshal([]byte(testCredentialString), &testCredentialMap) stringVal := "VerifiableCredential" - match, err := matchField(Field{Path: []string{"$.type"}, Filter: &Filter{Type: "string", Const: &stringVal}}, testCredentialMap) + match, value, err := matchField(Field{Path: []string{"$.type"}, Filter: &Filter{Type: "string", Const: &stringVal}}, testCredentialMap) require.NoError(t, err) + assert.Equal(t, []interface{}{"VerifiableCredential"}, value) assert.True(t, match) }) t.Run("errors", func(t *testing.T) { t.Run("invalid path", func(t *testing.T) { - match, err := matchField(Field{Path: []string{"$$"}}, testCredentialMap) + match, value, err := matchField(Field{Path: []string{"$$"}}, testCredentialMap) require.Error(t, err) + assert.Nil(t, value) assert.False(t, match) }) t.Run("invalid pattern", func(t *testing.T) { pattern := "[" - match, err := matchField(Field{Path: []string{"$.credentialSubject.field"}, Filter: &Filter{Type: "string", Pattern: &pattern}}, testCredentialMap) + match, value, err := matchField(Field{Path: []string{"$.credentialSubject.field"}, Filter: &Filter{Type: "string", Pattern: &pattern}}, testCredentialMap) require.Error(t, err) + assert.Nil(t, value) assert.False(t, match) }) }) @@ -657,3 +683,45 @@ func credentialToJSONLD(credential vc.VerifiableCredential) vc.VerifiableCredent } return result } + +func TestPresentationDefinition_ResolveConstraintsFields(t *testing.T) { + type fields struct { + Format *PresentationDefinitionClaimFormatDesignations + Frame *Frame + Id string + InputDescriptors []*InputDescriptor + Name string + Purpose *string + SubmissionRequirements []*SubmissionRequirement + } + type args struct { + credentialMap map[string]vc.VerifiableCredential + } + tests := []struct { + name string + fields fields + args args + want map[string]interface{} + wantErr assert.ErrorAssertionFunc + }{ + // TODO: Add test cases. + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + presentationDefinition := PresentationDefinition{ + Format: tt.fields.Format, + Frame: tt.fields.Frame, + Id: tt.fields.Id, + InputDescriptors: tt.fields.InputDescriptors, + Name: tt.fields.Name, + Purpose: tt.fields.Purpose, + SubmissionRequirements: tt.fields.SubmissionRequirements, + } + got, err := presentationDefinition.ResolveConstraintsFields(tt.args.credentialMap) + if !tt.wantErr(t, err, fmt.Sprintf("ResolveConstraintsFields(%v)", tt.args.credentialMap)) { + return + } + assert.Equalf(t, tt.want, got, "ResolveConstraintsFields(%v)", tt.args.credentialMap) + }) + } +}