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
42 changes: 22 additions & 20 deletions agent/common/issuerservice/issuer.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,13 +27,15 @@ import (
"github.com/eclipse-cfm/cfm/common/token"
)

// The tokens for the issuer-admin-api are exchanged under the issuer's participant context
// (HttpApiClient.IssuerID), so the token's sub owns every context these calls address and the
// resource scopes below suffice; no issuer-admin-api:admin elevation is needed. The cfm-agents
// ServiceAccount needs a jwtlet mapping on the issuer context granting these scopes, which the
// platform seeds.
const (
// ScopeApiAdmin is required for all issuer-admin-api operations here: the token is bound
// to the tenant's participant context, which the IssuerService does not know (it only has
// the issuer's context), so the caller resolution only passes with admin. Resource-level
// scopes (e.g. issuer-admin-api:credentials:read) are rejected with
// "No participant for 'sub = ...' found".
ScopeApiAdmin = "issuer-admin-api:admin"
ScopeHoldersWrite = "issuer-admin-api:holders:write"
ScopeCredentialsRead = "issuer-admin-api:credentials:read"
ScopeCredentialsWrite = "issuer-admin-api:credentials:write"
)

// IssuerCredentialResourceDto represents a DTO for verifiable credentials that the IssuerService has issued to holders.
Expand All @@ -47,14 +49,14 @@ 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
CreateHolder(ctx context.Context, did string, holderID string, name string, properties map[string]any) error
DeleteHolder(ctx context.Context, 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
RevokeCredential(ctx context.Context, credentialID string) 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)
QueryCredentialsByType(ctx context.Context, holderID string, credentialType string) ([]IssuerCredentialResourceDto, error)
}

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

func (i HttpApiClient) QueryCredentialsByType(ctx context.Context, participantContextID string, holderID string, credentialType string) ([]IssuerCredentialResourceDto, error) {
accessToken, err := i.TokenProvider.GetToken(ctx, ScopeApiAdmin, participantContextID)
func (i HttpApiClient) QueryCredentialsByType(ctx context.Context, holderID string, credentialType string) ([]IssuerCredentialResourceDto, error) {
accessToken, err := i.TokenProvider.GetToken(ctx, ScopeCredentialsRead, i.IssuerID)
if err != nil {
return nil, fmt.Errorf("failed to get API access token: %w", err)
}
Expand Down Expand Up @@ -113,8 +115,8 @@ func (i HttpApiClient) QueryCredentialsByType(ctx context.Context, participantCo
return credentials, nil
}

func (i HttpApiClient) DeleteHolder(ctx context.Context, participantContextID string, holderID string) error {
accessToken, err := i.TokenProvider.GetToken(ctx, ScopeApiAdmin, participantContextID)
func (i HttpApiClient) DeleteHolder(ctx context.Context, holderID string) error {
accessToken, err := i.TokenProvider.GetToken(ctx, ScopeHoldersWrite, i.IssuerID)
if err != nil {
return fmt.Errorf("failed to get API access token: %w", err)
}
Expand All @@ -137,8 +139,8 @@ func (i HttpApiClient) DeleteHolder(ctx context.Context, participantContextID st
return nil
}

func (i HttpApiClient) CreateHolder(ctx context.Context, participantContextID string, did string, holderID string, name string, properties map[string]any) error {
accessToken, err := i.TokenProvider.GetToken(ctx, ScopeApiAdmin, participantContextID)
func (i HttpApiClient) CreateHolder(ctx context.Context, did string, holderID string, name string, properties map[string]any) error {
accessToken, err := i.TokenProvider.GetToken(ctx, ScopeHoldersWrite, i.IssuerID)
if err != nil {
return fmt.Errorf("failed to get API access token: %w", err)
}
Expand Down Expand Up @@ -178,8 +180,8 @@ func (i HttpApiClient) CreateHolder(ctx context.Context, participantContextID st
return nil
}

func (i HttpApiClient) RevokeCredential(ctx context.Context, participantContextID string, credentialID string) error {
accessToken, err := i.TokenProvider.GetToken(ctx, ScopeApiAdmin, participantContextID)
func (i HttpApiClient) RevokeCredential(ctx context.Context, credentialID string) error {
accessToken, err := i.TokenProvider.GetToken(ctx, ScopeCredentialsWrite, i.IssuerID)
if err != nil {
return err
}
Expand All @@ -192,12 +194,12 @@ func (i HttpApiClient) RevokeCredential(ctx context.Context, participantContextI
req.Header.Set("Authorization", "Bearer "+accessToken)
resp, err := i.HttpClient.Do(req)
if err != nil {
return fmt.Errorf("failed to revoke credential with ID '%s' for participant context '%s': %w", credentialID, participantContextID, err)
return fmt.Errorf("failed to revoke credential with ID '%s' in issuer context '%s': %w", credentialID, i.IssuerID, err)
}
defer httpx.DrainAndClose(resp)

if resp.StatusCode != http.StatusOK {
return fmt.Errorf("failed to revoke credential with ID '%s' for participant context '%s': received status code %d with message [%s]", credentialID, participantContextID, resp.StatusCode, httpx.ReadErrorBody(resp))
return fmt.Errorf("failed to revoke credential with ID '%s' in issuer context '%s': received status code %d with message [%s]", credentialID, i.IssuerID, resp.StatusCode, httpx.ReadErrorBody(resp))
}
return nil
}
36 changes: 18 additions & 18 deletions agent/common/issuerservice/issuer_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -53,15 +53,15 @@ func TestHttpApiClient_CreateHolder(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, ScopeHoldersWrite, "test-issuer").Return("test token", nil)
client := HttpApiClient{
BaseURL: server.URL,
TokenProvider: tp,
IssuerID: "test-issuer",
HttpClient: &http.Client{},
}

err := client.CreateHolder(t.Context(), "test-context-id", "did:web:test-participant", "did:web:test-participant", "test holder", nil)
err := client.CreateHolder(t.Context(), "did:web:test-participant", "did:web:test-participant", "test holder", nil)
require.NoError(t, err)
}

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

err := client.CreateHolder(t.Context(), "test-context-id", "did:web:test-participant", "did:web:test-participant", "test holder", nil)
err := client.CreateHolder(t.Context(), "did:web:test-participant", "did:web:test-participant", "test holder", nil)
require.ErrorContains(t, err, "test error")
}

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

err := client.CreateHolder(t.Context(), "", "did:web:test-participant", "did:web:test-participant", "test holder", nil)
err := client.CreateHolder(t.Context(), "did:web:test-participant", "did:web:test-participant", "test holder", nil)
require.ErrorContains(t, err, "failed to create Holder")
}

Expand All @@ -117,15 +117,15 @@ func TestHttpApiClient_RevokeCredential(t *testing.T) {
defer server.Close()

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

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

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

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

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

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

Expand All @@ -192,15 +192,15 @@ func TestHttpApiClient_DeleteHolder(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, ScopeHoldersWrite, "test-issuer").Return("test token", nil)
client := HttpApiClient{
BaseURL: server.URL,
TokenProvider: tp,
IssuerID: "test-issuer",
HttpClient: &http.Client{},
}

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

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

err := client.DeleteHolder(t.Context(), "", "did:web:test-participant")
err := client.DeleteHolder(t.Context(), "did:web:test-participant")
require.ErrorContains(t, err, "received status code 404")
}

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

err := client.DeleteHolder(t.Context(), "", "did:web:test-participant")
err := client.DeleteHolder(t.Context(), "did:web:test-participant")
require.ErrorContains(t, err, "received status code 401")
}

Expand Down Expand Up @@ -294,15 +294,15 @@ func TestHttpApiClient_QueryCredentialsByType(t *testing.T) {
defer server.Close()

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

result, err := client.QueryCredentialsByType(t.Context(), "test-participant", "did:web:test-participant", "MembershipCredential")
result, err := client.QueryCredentialsByType(t.Context(), "did:web:test-participant", "MembershipCredential")
require.NoError(t, err)
require.Len(t, result, 1)
assert.Equal(t, "cred-1", result[0].ID)
Expand All @@ -319,7 +319,7 @@ func TestHttpApiClient_QueryCredentialsByType_AuthError(t *testing.T) {
HttpClient: &http.Client{},
}

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

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

result, err := client.QueryCredentialsByType(t.Context(), "test-participant", "did:web:test-participant", "MembershipCredential")
result, err := client.QueryCredentialsByType(t.Context(), "did:web:test-participant", "MembershipCredential")
require.ErrorContains(t, err, "error parsing credentials response")
require.Nil(t, result)
}
Expand All @@ -394,5 +394,5 @@ func TestHttpApiClient_RevokeCredential_EscapesPathSegments(t *testing.T) {
HttpClient: &http.Client{},
}

require.NoError(t, client.RevokeCredential(t.Context(), "test-participant", "a/b"))
require.NoError(t, client.RevokeCredential(t.Context(), "a/b"))
}
2 changes: 0 additions & 2 deletions agent/orchestration/jwtletagent/activity/activity.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,6 @@ import (

"github.com/eclipse-cfm/cfm/agent/common/controlplane"
"github.com/eclipse-cfm/cfm/agent/common/identityhub"
"github.com/eclipse-cfm/cfm/agent/common/issuerservice"
"github.com/eclipse-cfm/cfm/agent/common/siglet"
"github.com/eclipse-cfm/cfm/common/model"
"github.com/eclipse-cfm/cfm/common/system"
Expand Down Expand Up @@ -85,7 +84,6 @@ var agentScopes = []string{
identityhub.ScopeApiAdmin,
identityhub.ScopeCredentialsRead,
identityhub.ScopeCredentialsWrite,
issuerservice.ScopeApiAdmin,
siglet.ScopeApiRead,
siglet.ScopeApiWrite,
"read",
Expand Down
4 changes: 2 additions & 2 deletions agent/orchestration/onboarding/activity/activity.go
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ func (p OnboardingActivityProcessor) ProcessDispose(ctx api.ActivityContext) api
for _, spec := range data.CredentialSpecs {
credentialType := spec.Type

credentials, err := p.IssuerServiceApiClient.QueryCredentialsByType(ctx.Context(), participantContextID, holder.HolderID, credentialType)
credentials, err := p.IssuerServiceApiClient.QueryCredentialsByType(ctx.Context(), 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)}
Expand All @@ -106,7 +106,7 @@ func (p OnboardingActivityProcessor) ProcessDispose(ctx api.ActivityContext) api

// for each credential, send a revocation request
for _, credentialDto := range credentials {
err := p.IssuerServiceApiClient.RevokeCredential(ctx.Context(), participantContextID, credentialDto.ID)
err := p.IssuerServiceApiClient.RevokeCredential(ctx.Context(), credentialDto.ID)
if err != nil {
revocationErrors = append(revocationErrors, err)
}
Expand Down
8 changes: 4 additions & 4 deletions agent/orchestration/onboarding/activity/activity_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -569,7 +569,7 @@ type MockIssuerServiceApiClient struct {
queriedHolderIDs *[]string
}

func (m MockIssuerServiceApiClient) QueryCredentialsByType(ctx context.Context, participantContextID string, holderID string, credentialType string) ([]issuerservice.IssuerCredentialResourceDto, error) {
func (m MockIssuerServiceApiClient) QueryCredentialsByType(ctx context.Context, holderID string, credentialType string) ([]issuerservice.IssuerCredentialResourceDto, error) {
if m.queriedHolderIDs != nil {
*m.queriedHolderIDs = append(*m.queriedHolderIDs, holderID)
}
Expand All @@ -580,16 +580,16 @@ func (m MockIssuerServiceApiClient) QueryCredentialsByType(ctx context.Context,

}

func (m MockIssuerServiceApiClient) DeleteHolder(ctx context.Context, participantContextID string, holderID string) error {
func (m MockIssuerServiceApiClient) DeleteHolder(ctx context.Context, holderID string) error {
//TODO implement me
panic("implement me")
}

func (m MockIssuerServiceApiClient) CreateHolder(ctx context.Context, participantContextID string, did string, holderID string, name string, properties map[string]any) error {
func (m MockIssuerServiceApiClient) CreateHolder(ctx context.Context, did string, holderID string, name string, properties map[string]any) error {
return m.expectedError
}

func (m MockIssuerServiceApiClient) RevokeCredential(ctx context.Context, participantContextID string, credentialID string) error {
func (m MockIssuerServiceApiClient) RevokeCredential(ctx context.Context, credentialID string) error {
if m.revokedIDs != nil {
*m.revokedIDs = append(*m.revokedIDs, credentialID)
}
Expand Down
4 changes: 2 additions & 2 deletions agent/orchestration/registration/activity/activity.go
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ func (p RegistrationActivityProcessor) ProcessDeploy(ctx api.ActivityContext) ap
span.RecordError(err)
return api.ActivityResult{Result: api.ActivityResultFatalError, Error: fmt.Errorf("error reading vpa data: %w", err)}
}
if err := p.IssuerService.CreateHolder(ctx.Context(), regData.ParticipantContextId, regData.DID, holderID, regData.HolderName, properties); err != nil {
if err := p.IssuerService.CreateHolder(ctx.Context(), regData.DID, holderID, regData.HolderName, properties); err != nil {
span.RecordError(err)
// todo: inspect error if it is retryable
return api.ActivityResult{Result: api.ActivityResultFatalError, Error: fmt.Errorf("error creating holder in ApiClient: %w", err)}
Expand All @@ -81,7 +81,7 @@ func (p RegistrationActivityProcessor) ProcessDispose(ctx api.ActivityContext) a
return api.ActivityResult{Result: api.ActivityResultFatalError, Error: fmt.Errorf("error processing Registration activity for orchestration %s: %w", ctx.OID(), err)}
}
holderID := regData.DID
if err := p.IssuerService.DeleteHolder(ctx.Context(), regData.ParticipantContextId, holderID); err != nil {
if err := p.IssuerService.DeleteHolder(ctx.Context(), holderID); err != nil {
// todo: inspect error if it is retryable
return api.ActivityResult{Result: api.ActivityResultFatalError, Error: fmt.Errorf("registration rollback: error deleting holder in IssuerService: %w", err)}
}
Expand Down
8 changes: 4 additions & 4 deletions agent/orchestration/registration/activity/activity_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -386,19 +386,19 @@ type MockIssuerService struct {
recorded regData
}

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

func (m *MockIssuerService) DeleteHolder(ctx context.Context, participantContextID string, holderID string) error {
func (m *MockIssuerService) DeleteHolder(ctx context.Context, holderID string) error {
return m.expectedError
}

func (m *MockIssuerService) RevokeCredential(ctx context.Context, participantContextID string, credentialID string) error {
func (m *MockIssuerService) RevokeCredential(ctx context.Context, credentialID string) error {
return nil
}

func (m *MockIssuerService) CreateHolder(ctx context.Context, participantContextID string, did string, holderID string, name string, properties map[string]any) error {
func (m *MockIssuerService) CreateHolder(ctx context.Context, did string, holderID string, name string, properties map[string]any) error {
m.recorded.did = did
m.recorded.holderID = holderID
m.recorded.name = name
Expand Down
Loading