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
12 changes: 8 additions & 4 deletions agent/common/issuerservice/issuer.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,8 +49,12 @@ type IssuerCredentialResourceDto struct {
type ApiClient interface {
CreateHolder(ctx context.Context, participantContextID string, did string, holderID string, name string, properties map[string]any) error
DeleteHolder(ctx context.Context, participantContextID string, holderID string) error
// RevokeCredential revokes an issued credential. credentialID is the ID of the IssuerService's credential resource
// (IssuerCredentialResourceDto.ID), not the ID of the verifiable credential itself.
RevokeCredential(ctx context.Context, participantContextID string, credentialID string) error
QueryCredentialsByType(ctx context.Context, participantContextID string, credentialType string) ([]IssuerCredentialResourceDto, error)
// QueryCredentialsByType returns the credentials of the given type that were issued to the holder. The IssuerService
// records the credential subject's ID (the holder's DID) as the holder ID of an issued credential.
QueryCredentialsByType(ctx context.Context, participantContextID string, holderID string, credentialType string) ([]IssuerCredentialResourceDto, error)
}

type HttpApiClient struct {
Expand All @@ -60,8 +64,8 @@ type HttpApiClient struct {
HttpClient *http.Client
}

func (i HttpApiClient) QueryCredentialsByType(ctx context.Context, holderID string, credentialType string) ([]IssuerCredentialResourceDto, error) {
accessToken, err := i.TokenProvider.GetToken(ctx, ScopeApiAdmin, holderID)
func (i HttpApiClient) QueryCredentialsByType(ctx context.Context, participantContextID string, holderID string, credentialType string) ([]IssuerCredentialResourceDto, error) {
accessToken, err := i.TokenProvider.GetToken(ctx, ScopeApiAdmin, participantContextID)
if err != nil {
return nil, fmt.Errorf("failed to get API access token: %w", err)
}
Expand Down Expand Up @@ -179,7 +183,7 @@ func (i HttpApiClient) RevokeCredential(ctx context.Context, participantContextI
if err != nil {
return err
}
url := fmt.Sprintf("%s/v1/participants/%s/credentials/%s/revoke", i.BaseURL, url.PathEscape(participantContextID), url.PathEscape(credentialID))
url := fmt.Sprintf("%s/v1/participants/%s/credentials/%s/revoke", i.BaseURL, url.PathEscape(i.IssuerID), url.PathEscape(credentialID))
req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, nil)
if err != nil {
return err
Expand Down
28 changes: 13 additions & 15 deletions agent/common/issuerservice/issuer_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -107,10 +107,8 @@ func TestHttpApiClient_CreateHolder_ApiReturnsError(t *testing.T) {
}

func TestHttpApiClient_RevokeCredential(t *testing.T) {
template := "/v1/participants/.*/credentials/.*/revoke"
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
matched, _ := regexp.MatchString(template, r.URL.Path)
if matched && r.Method == http.MethodPost {
if r.URL.Path == "/v1/participants/test-issuer/credentials/test-credential-id/revoke" && r.Method == http.MethodPost {
w.WriteHeader(http.StatusOK)
} else {
w.WriteHeader(http.StatusInternalServerError)
Expand All @@ -119,15 +117,15 @@ func TestHttpApiClient_RevokeCredential(t *testing.T) {
defer server.Close()

tp := mocks.NewMockTokenProvider(t)
tp.On("GetToken", mock.Anything, mock.Anything, mock.Anything).Return("test token", nil)
tp.On("GetToken", mock.Anything, ScopeApiAdmin, "test-participant").Return("test token", nil)
client := HttpApiClient{
BaseURL: server.URL,
TokenProvider: tp,
IssuerID: "test-issuer",
HttpClient: &http.Client{},
}

err := client.RevokeCredential(t.Context(), "did:web:test-participant", "test-credential-id")
err := client.RevokeCredential(t.Context(), "test-participant", "test-credential-id")
require.NoError(t, err)
}

Expand Down Expand Up @@ -257,11 +255,11 @@ func TestHttpApiClient_DeleteHolder_AuthError(t *testing.T) {
}

func TestHttpApiClient_QueryCredentialsByType(t *testing.T) {
template := "/v1/participants/.*/credentials/query"
template := "^/v1/participants/test-issuer/credentials/query$"
credentials := []IssuerCredentialResourceDto{
{
ID: "cred-1",
ParticipantContextID: "did:web:test-participant",
ParticipantContextID: "test-issuer",
CredentialFormat: common.CredentialFormat_VCDM10_JWT,
VerifiableCredential: common.VerifiableCredential{
ID: "vc-1",
Expand Down Expand Up @@ -296,19 +294,19 @@ func TestHttpApiClient_QueryCredentialsByType(t *testing.T) {
defer server.Close()

tp := mocks.NewMockTokenProvider(t)
tp.On("GetToken", mock.Anything, mock.Anything, mock.Anything).Return("test token", nil)
tp.On("GetToken", mock.Anything, ScopeApiAdmin, "test-participant").Return("test token", nil)
client := HttpApiClient{
BaseURL: server.URL,
TokenProvider: tp,
IssuerID: "test-issuer",
HttpClient: &http.Client{},
}

result, err := client.QueryCredentialsByType(t.Context(), "did:web:test-participant", "MembershipCredential")
result, err := client.QueryCredentialsByType(t.Context(), "test-participant", "did:web:test-participant", "MembershipCredential")
require.NoError(t, err)
require.Len(t, result, 1)
assert.Equal(t, "cred-1", result[0].ID)
assert.Equal(t, "did:web:test-participant", result[0].ParticipantContextID)
assert.Equal(t, "test-issuer", result[0].ParticipantContextID)
}

func TestHttpApiClient_QueryCredentialsByType_AuthError(t *testing.T) {
Expand All @@ -321,7 +319,7 @@ func TestHttpApiClient_QueryCredentialsByType_AuthError(t *testing.T) {
HttpClient: &http.Client{},
}

result, err := client.QueryCredentialsByType(t.Context(), "did:web:test-participant", "MembershipCredential")
result, err := client.QueryCredentialsByType(t.Context(), "test-participant", "did:web:test-participant", "MembershipCredential")
require.ErrorContains(t, err, "auth error")
require.Nil(t, result)
}
Expand All @@ -347,7 +345,7 @@ func TestHttpApiClient_QueryCredentialsByType_ApiReturnsError(t *testing.T) {
HttpClient: &http.Client{},
}

result, err := client.QueryCredentialsByType(t.Context(), "did:web:test-participant", "MembershipCredential")
result, err := client.QueryCredentialsByType(t.Context(), "test-participant", "did:web:test-participant", "MembershipCredential")
require.ErrorContains(t, err, "failed to query credentials on IssuerService")
require.Nil(t, result)
}
Expand Down Expand Up @@ -375,7 +373,7 @@ func TestHttpApiClient_QueryCredentialsByType_InvalidJson(t *testing.T) {
HttpClient: &http.Client{},
}

result, err := client.QueryCredentialsByType(t.Context(), "did:web:test-participant", "MembershipCredential")
result, err := client.QueryCredentialsByType(t.Context(), "test-participant", "did:web:test-participant", "MembershipCredential")
require.ErrorContains(t, err, "error parsing credentials response")
require.Nil(t, result)
}
Expand All @@ -392,9 +390,9 @@ func TestHttpApiClient_RevokeCredential_EscapesPathSegments(t *testing.T) {
client := HttpApiClient{
BaseURL: server.URL,
TokenProvider: tp,
IssuerID: "test-issuer",
IssuerID: "../other?force=true#",
HttpClient: &http.Client{},
}

require.NoError(t, client.RevokeCredential(t.Context(), "../other?force=true#", "a/b"))
require.NoError(t, client.RevokeCredential(t.Context(), "test-participant", "a/b"))
}
19 changes: 15 additions & 4 deletions agent/orchestration/onboarding/activity/activity.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,12 @@ type credentialRequestData struct {
CredentialRequestURL string `json:"credentialRequest"`
}

// holderData identifies the participant as a holder in the IssuerService, which records the credential subject (the
// participant's DID) as the holder ID of every credential it issues.
type holderData struct {
HolderID string `json:"cfm.participant.id" validate:"required"`
}

// ProcessDeploy processes the deploy action for an onboarding activity by requesting the issuance of verifiable credentials.
// if a credential request is in progress, the deploy action simply checks its status and reschedules itself.
func (p OnboardingActivityProcessor) ProcessDeploy(ctx api.ActivityContext) api.ActivityResult {
Expand Down Expand Up @@ -75,27 +81,32 @@ func (p OnboardingActivityProcessor) ProcessDispose(ctx api.ActivityContext) api
return api.ActivityResult{Result: api.ActivityResultFatalError, Error: fmt.Errorf("error processing Onboarding activity for orchestration %s: %w", ctx.OID(), err)}
}

var holder holderData
if err := ctx.ReadValues(&holder); err != nil {
return api.ActivityResult{Result: api.ActivityResultFatalError, Error: fmt.Errorf("error processing Onboarding activity for orchestration %s: %w", ctx.OID(), err)}
}

participantContextID := credentialRequest.ParticipantContextID
// query credentials by type, using the IdentityAPI
// query the holder's credentials by type, using the IssuerService's Admin API
var revocationErrors []error

for _, spec := range data.CredentialSpecs {
credentialType := spec.Type

credentials, err := p.IssuerServiceApiClient.QueryCredentialsByType(ctx.Context(), participantContextID, credentialType)
credentials, err := p.IssuerServiceApiClient.QueryCredentialsByType(ctx.Context(), participantContextID, holder.HolderID, credentialType)

if err != nil {
return api.ActivityResult{Result: api.ActivityResultFatalError, Error: fmt.Errorf("error querying credentials by type %s for participant context %s: %w", credentialType, participantContextID, err)}
}

if len(credentials) == 0 {
p.Monitor.Infof("Rollback: could not revoke credentials of type '%s': 0 found for participant context '%s'", credentialType, participantContextID)
p.Monitor.Infof("Rollback: could not revoke credentials of type '%s': 0 found for holder '%s'", credentialType, holder.HolderID)
continue
}

// for each credential, send a revocation request
for _, credentialDto := range credentials {
err := p.IssuerServiceApiClient.RevokeCredential(ctx.Context(), participantContextID, credentialDto.VerifiableCredential.ID)
err := p.IssuerServiceApiClient.RevokeCredential(ctx.Context(), participantContextID, credentialDto.ID)
if err != nil {
revocationErrors = append(revocationErrors, err)
}
Expand Down
56 changes: 51 additions & 5 deletions agent/orchestration/onboarding/activity/activity_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -305,22 +305,27 @@ func TestOnboardingActivityProcessor_ProcessDeploy_WhenInvalidStateReceived(t *t
}

func TestOnboardingActivityProcessor_ProcessDispose(t *testing.T) {
queriedHolderIDs := make([]string, 0)
revokedIDs := make([]string, 0)
processor := OnboardingActivityProcessor{
Monitor: system.NoopMonitor{},
IssuerServiceApiClient: MockIssuerServiceApiClient{
expectedCredentials: []issuerservice.IssuerCredentialResourceDto{
{
ID: "",
ParticipantContextID: "test-participant",
ID: "test-credential-id",
ParticipantContextID: "test-issuer",
CredentialFormat: common.CredentialFormat_VCDM20_COSE,
VerifiableCredential: common.VerifiableCredential{},
VerifiableCredential: common.VerifiableCredential{ID: "test-vc-id"},
},
},
queriedHolderIDs: &queriedHolderIDs,
revokedIDs: &revokedIDs,
},
}

var processingData = map[string]any{
"participantContextId": "test-participant",
"cfm.participant.id": "did:web:test-participant",
"cfm.vpa.credentials": []any{
map[string]string{
"id": "id",
Expand All @@ -346,6 +351,40 @@ func TestOnboardingActivityProcessor_ProcessDispose(t *testing.T) {

assert.Equal(t, api.ActivityResultType(api.ActivityResultComplete), result.Result)
assert.NoError(t, result.Error)
assert.Equal(t, []string{"did:web:test-participant"}, queriedHolderIDs, "credentials must be queried by the holder's DID")
assert.Equal(t, []string{"test-credential-id"}, revokedIDs, "the credential resource ID must be revoked, not the VC ID")
}

func TestOnboardingActivityProcessor_ProcessDispose_MissingParticipantID(t *testing.T) {
processor := OnboardingActivityProcessor{
Monitor: system.NoopMonitor{},
IssuerServiceApiClient: MockIssuerServiceApiClient{},
}

var processingData = map[string]any{
"participantContextId": "test-participant",
"cfm.vpa.credentials": []any{
map[string]string{
"id": "id",
"format": "format",
"issuer": "issuer",
"type": "type",
},
},
}

activity := api.Activity{
ID: "test-activity",
Type: "edcv",
Discriminator: api.DisposeDiscriminator,
}

activityContext := api.NewActivityContext(context.Background(), "orch-123", activity, processingData, make(map[string]any))

result := processor.ProcessDispose(activityContext)

assert.Equal(t, api.ActivityResultType(api.ActivityResultFatalError), result.Result)
assert.Error(t, result.Error)
}

func TestOnboardingActivityProcessor_ProcessDispose_RevocationFails(t *testing.T) {
Expand All @@ -368,6 +407,7 @@ func TestOnboardingActivityProcessor_ProcessDispose_RevocationFails(t *testing.T

var processingData = map[string]any{
"participantContextId": "test-participant",
"cfm.participant.id": "did:web:test-participant",
"cfm.vpa.credentials": []any{
map[string]string{
"id": "id",
Expand Down Expand Up @@ -409,6 +449,7 @@ func TestOnboardingActivityProcessor_ProcessDispose_NoCredentials(t *testing.T)

var processingData = map[string]any{
"participantContextId": "test-participant",
"cfm.participant.id": "did:web:test-participant",
"cfm.vpa.credentials": []any{
map[string]string{
"id": "id",
Expand Down Expand Up @@ -457,6 +498,7 @@ func TestOnboardingActivityProcessor_ProcessDispose_FirstTypeWithoutCredentials(

var processingData = map[string]any{
"participantContextId": "test-participant",
"cfm.participant.id": "did:web:test-participant",
"cfm.vpa.credentials": []any{
map[string]string{
"id": "id-1",
Expand Down Expand Up @@ -488,7 +530,7 @@ func TestOnboardingActivityProcessor_ProcessDispose_FirstTypeWithoutCredentials(

assert.Equal(t, api.ActivityResultType(api.ActivityResultComplete), result.Result)
assert.NoError(t, result.Error)
assert.Equal(t, []string{"cred-2"}, revokedIDs, "credentials of later types must still be revoked when an earlier type has none")
assert.Equal(t, []string{"test-credential-id"}, revokedIDs, "credentials of later types must still be revoked when an earlier type has none")
}

type MockIdentityHubClient struct {
Expand Down Expand Up @@ -524,9 +566,13 @@ type MockIssuerServiceApiClient struct {
expectedCredentials []issuerservice.IssuerCredentialResourceDto
credentialsByType map[string][]issuerservice.IssuerCredentialResourceDto
revokedIDs *[]string
queriedHolderIDs *[]string
}

func (m MockIssuerServiceApiClient) QueryCredentialsByType(ctx context.Context, participantContextID string, credentialType string) ([]issuerservice.IssuerCredentialResourceDto, error) {
func (m MockIssuerServiceApiClient) QueryCredentialsByType(ctx context.Context, participantContextID string, holderID string, credentialType string) ([]issuerservice.IssuerCredentialResourceDto, error) {
if m.queriedHolderIDs != nil {
*m.queriedHolderIDs = append(*m.queriedHolderIDs, holderID)
}
if m.credentialsByType != nil {
return m.credentialsByType[credentialType], nil
}
Expand Down
2 changes: 1 addition & 1 deletion agent/orchestration/registration/activity/activity_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -386,7 +386,7 @@ type MockIssuerService struct {
recorded regData
}

func (m *MockIssuerService) QueryCredentialsByType(ctx context.Context, participantContextID string, credentialType string) ([]issuerservice.IssuerCredentialResourceDto, error) {
func (m *MockIssuerService) QueryCredentialsByType(ctx context.Context, participantContextID string, holderID string, credentialType string) ([]issuerservice.IssuerCredentialResourceDto, error) {
return nil, nil
}

Expand Down
Loading