Skip to content
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
7 changes: 4 additions & 3 deletions discovery/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,10 @@ import (
"context"
"errors"
"fmt"
"slices"
"strings"
"time"

ssi "github.com/nuts-foundation/go-did"
"github.com/nuts-foundation/go-did/did"
"github.com/nuts-foundation/go-did/vc"
Expand All @@ -36,9 +40,6 @@ import (
"github.com/nuts-foundation/nuts-node/vcr/types"
"github.com/nuts-foundation/nuts-node/vdr/didsubject"
"github.com/nuts-foundation/nuts-node/vdr/resolver"
"slices"
"strings"
"time"
)

// clientRegistrationManager is a client component, responsible for managing registrations on a Discovery Service.
Expand Down
37 changes: 36 additions & 1 deletion docs/_static/vcr/vcr_v2.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -491,7 +491,9 @@ paths:
type: string
example: 90BC1AE9-752B-432F-ADC3-DD9F9C61843C
get:
summary: List all Verifiable Credentials in the holder's wallet.
summary: >
List all Verifiable Credentials in the holder's wallet. It will only return non-expired or non-revoked credentials.
If you want to list all credentials regardless of their validity, use the search API.
operationId: getCredentialsInWallet
tags:
- credential
Expand Down Expand Up @@ -569,6 +571,39 @@ paths:
description: Credential has been removed from the wallet.
default:
$ref: '../common/error_response.yaml'
/internal/vcr/v2/holder/{subjectID}/vc/search:
parameters:
- name: subjectID
in: path
description: Subject ID of the wallet owner at this node.
required: true
content:
plain/text:
schema:
type: string
example: 90BC1AE9-752B-432F-ADC3-DD9F9C61843C
get:
summary: "Searches for verifiable credentials in the holder's wallet"
description: >
The SearchVCResult contains a list of matching credentials in the wallet regardless of validity.
This allows applications to build a listing of all credentials in wallets, regardless of its status (expired/revoked).
The entry may contain a revocation if the credential has been revoked with a credential of type CredentialRevocation.

error returns:
* 404 - Subject not found
* 500 - An error occurred while processing the request
operationId: "searchCredentialsInWallet"
tags:
- credential
responses:
"200":
description: A list of matching credentials
content:
application/json:
schema:
$ref: '#/components/schemas/SearchVCResults'
default:
$ref: '../common/error_response.yaml'
components:
schemas:
VerifiableCredential:
Expand Down
29 changes: 26 additions & 3 deletions vcr/api/vcr/v2/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -466,6 +466,29 @@ func (w *Wrapper) GetCredentialsInWallet(ctx context.Context, request GetCredent
return GetCredentialsInWallet200JSONResponse(credentials), nil
}

// SearchCredentialsInWallet handles the API request for searching credentials in the holder's wallet
func (w *Wrapper) SearchCredentialsInWallet(ctx context.Context, request SearchCredentialsInWalletRequestObject) (SearchCredentialsInWalletResponseObject, error) {
dids, err := w.SubjectManager.ListDIDs(ctx, request.SubjectID)
if err != nil {
return nil, err
}

var allCreds []vc.VerifiableCredential
for _, did := range dids {
creds, err := w.VCR.Wallet().SearchCredential(ctx, did)
if err != nil {
return nil, err
}
allCreds = append(allCreds, creds...)
}

searchResults, err := w.vcsWithRevocationsToSearchResults(allCreds)
if err != nil {
return nil, err
}
return SearchCredentialsInWallet200JSONResponse(SearchVCResults{VerifiableCredentials: searchResults}), nil
}

func (w *Wrapper) RemoveCredentialFromWallet(ctx context.Context, request RemoveCredentialFromWalletRequestObject) (RemoveCredentialFromWalletResponseObject, error) {
// get DIDs for holder
dids, err := w.SubjectManager.ListDIDs(ctx, request.SubjectID)
Expand Down Expand Up @@ -532,9 +555,9 @@ func (w *Wrapper) vcsWithRevocationsToSearchResults(foundVCs []vc.VerifiableCred
result := make([]SearchVCResult, len(foundVCs))
for i, resolvedVC := range foundVCs {
var revocation *Revocation
revocation, err := w.VCR.Verifier().GetRevocation(*resolvedVC.ID)
if err != nil && !errors.Is(err, verifier.ErrNotFound) {
return nil, err
revocation, err := w.VCR.Verifier().GetRevocation(resolvedVC)
if err != nil {
return nil, fmt.Errorf("failed to get revocation for credential %s: %w", resolvedVC.ID.String(), err)
}
result[i] = SearchVCResult{VerifiableCredential: resolvedVC, Revocation: revocation}
}
Expand Down
60 changes: 60 additions & 0 deletions vcr/api/vcr/v2/api_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -831,6 +831,66 @@ func TestWrapper_GetCredentialsInWallet(t *testing.T) {
})
}

func TestWrapper_SearchCredentialsInWallet(t *testing.T) {
subjectID := "holder"
vcWithRevocation := testVC
vcWithRevocation.ID = &credentialID
revocationInfo := &Revocation{Reason: "test revocation"}

t.Run("ok - no results", func(t *testing.T) {
testContext := newMockContext(t)
testContext.mockSubjectManager.EXPECT().ListDIDs(gomock.Any(), subjectID).Return([]did.DID{holderDID}, nil)
testContext.mockWallet.EXPECT().SearchCredential(testContext.requestCtx, holderDID).Return([]vc.VerifiableCredential{}, nil)

response, err := testContext.client.SearchCredentialsInWallet(testContext.requestCtx, SearchCredentialsInWalletRequestObject{
SubjectID: subjectID,
})

assert.NoError(t, err)
assert.Equal(t, SearchCredentialsInWallet200JSONResponse(SearchVCResults{VerifiableCredentials: []SearchVCResult{}}), response)
})

t.Run("ok - not revoked", func(t *testing.T) {
testContext := newMockContext(t)
testContext.mockSubjectManager.EXPECT().ListDIDs(gomock.Any(), subjectID).Return([]did.DID{holderDID}, nil)
testContext.mockWallet.EXPECT().SearchCredential(testContext.requestCtx, holderDID).Return([]vc.VerifiableCredential{testVC}, nil)
testContext.mockVerifier.EXPECT().GetRevocation(testVC).Return(nil, nil)

response, err := testContext.client.SearchCredentialsInWallet(testContext.requestCtx, SearchCredentialsInWalletRequestObject{
SubjectID: subjectID,
})

assert.NoError(t, err)
expectedResult := SearchVCResults{VerifiableCredentials: []SearchVCResult{{VerifiableCredential: testVC}}}
assert.Equal(t, SearchCredentialsInWallet200JSONResponse(expectedResult), response)
})

t.Run("ok - revoked", func(t *testing.T) {
testContext := newMockContext(t)
testContext.mockSubjectManager.EXPECT().ListDIDs(gomock.Any(), subjectID).Return([]did.DID{holderDID}, nil)
testContext.mockWallet.EXPECT().SearchCredential(testContext.requestCtx, holderDID).Return([]vc.VerifiableCredential{vcWithRevocation}, nil)
// GetRevocation is then called to get the details
testContext.mockVerifier.EXPECT().GetRevocation(vcWithRevocation).Return(revocationInfo, nil)

response, err := testContext.client.SearchCredentialsInWallet(testContext.requestCtx, SearchCredentialsInWalletRequestObject{
SubjectID: subjectID,
})

assert.NoError(t, err)
expectedResult := SearchVCResults{VerifiableCredentials: []SearchVCResult{{VerifiableCredential: vcWithRevocation, Revocation: revocationInfo}}}
assert.Equal(t, SearchCredentialsInWallet200JSONResponse(expectedResult), response)
})

t.Run("subject not found", func(t *testing.T) {
testContext := newMockContext(t)
testContext.mockSubjectManager.EXPECT().ListDIDs(gomock.Any(), subjectID).Return([]did.DID{}, didsubject.ErrSubjectNotFound)

_, err := testContext.client.SearchCredentialsInWallet(testContext.requestCtx, SearchCredentialsInWalletRequestObject{SubjectID: subjectID})

assert.ErrorIs(t, err, didsubject.ErrSubjectNotFound)
})
}

func TestWrapper_RemoveCredentialFromSubjectWallet(t *testing.T) {
didNuts := did.MustParseDID("did:nuts:123")
didWeb := did.MustParseDID("did:web:example.com")
Expand Down
Loading
Loading