From fa63d5825f6849874a40d9dca5a763f6b0dc5eab Mon Sep 17 00:00:00 2001 From: Andres Uribe Gonzalez Date: Wed, 11 Jan 2023 18:31:43 -0500 Subject: [PATCH] Added manual overrides for credential issuance upon application review. --- integration/steelthread_integration_test.go | 5 +++ .../testdata/review-application-input.json | 12 ++++++- pkg/server/router/manifest.go | 11 +++++-- pkg/server/server_manifest_test.go | 33 ++++++++++++++++++- pkg/service/manifest/model/model.go | 15 +++++++++ pkg/service/manifest/response.go | 16 +++++++++ pkg/service/manifest/service.go | 13 ++------ 7 files changed, 89 insertions(+), 16 deletions(-) diff --git a/integration/steelthread_integration_test.go b/integration/steelthread_integration_test.go index b765b3d00..0f9d48d12 100644 --- a/integration/steelthread_integration_test.go +++ b/integration/steelthread_integration_test.go @@ -3,6 +3,7 @@ package integration import ( "testing" + "github.com/TBD54566975/ssi-sdk/credential/util" "github.com/stretchr/testify/assert" "github.com/tbd54566975/ssi-service/pkg/service/operation/storage" ) @@ -177,6 +178,10 @@ func TestSubmitAndReviewApplicationIntegration(t *testing.T) { vc, err := getJSONElement(reviewApplicationOutput, "$.verifiableCredentials[0]") assert.NoError(t, err) assert.NotEmpty(t, vc) + typedVC, err := util.CredentialsFromInterface(vc) + assert.NoError(t, err) + assert.Equal(t, "Mister", typedVC.CredentialSubject["givenName"]) + assert.Equal(t, "Tee", typedVC.CredentialSubject["familyName"]) operationOutput, err := get(endpoint + version + "operations/" + opID) assert.NoError(t, err) diff --git a/integration/testdata/review-application-input.json b/integration/testdata/review-application-input.json index 305d0f8ca..4e2bb7303 100644 --- a/integration/testdata/review-application-input.json +++ b/integration/testdata/review-application-input.json @@ -1,4 +1,14 @@ { "approved": {{.Approved}}, - "reason": "{{.Reason}}" + "reason": "{{.Reason}}", + "credential_overrides": { + "kyc_credential": { + "data": { + "familyName": "Tee", + "givenName": "Mister" + }, + "expiry": null, + "revocable": true + } + } } \ No newline at end of file diff --git a/pkg/server/router/manifest.go b/pkg/server/router/manifest.go index 7674191c3..9dfcda6d9 100644 --- a/pkg/server/router/manifest.go +++ b/pkg/server/router/manifest.go @@ -517,13 +517,18 @@ func (mr ManifestRouter) DeleteResponse(ctx context.Context, w http.ResponseWrit type ReviewApplicationRequest struct { Approved bool `json:"approved"` Reason string `json:"reason"` + + // Overrides to apply to the credentials that will be created. Keys are the ID that corresponds to an + // OutputDescriptor.ID from the manifest. + CredentialOverrides map[string]model.CredentialOverride `json:"credential_overrides,omitempty"` } func (r ReviewApplicationRequest) toServiceRequest(id string) model.ReviewApplicationRequest { return model.ReviewApplicationRequest{ - ID: id, - Approved: r.Approved, - Reason: r.Reason, + ID: id, + Approved: r.Approved, + Reason: r.Reason, + CredentialOverrides: r.CredentialOverrides, } } diff --git a/pkg/server/server_manifest_test.go b/pkg/server/server_manifest_test.go index 99abc6be3..7d738da4f 100644 --- a/pkg/server/server_manifest_test.go +++ b/pkg/server/server_manifest_test.go @@ -30,6 +30,14 @@ import ( "github.com/tbd54566975/ssi-service/pkg/service/schema" ) +func TestFoo(t *testing.T) { + data := []byte(`{"credential_overrides":{"some_key":{}}}`) + var p router.ReviewApplicationRequest + assert.NoError(t, json.Unmarshal(data, &p)) + assert.Equal(t, map[string]manifestsvc.CredentialOverride{ + "some_key": {}, + }, p.CredentialOverrides) +} func TestManifestAPI(t *testing.T) { t.Run("Test Create Manifest", func(tt *testing.T) { bolt := setupTestDB(tt) @@ -581,7 +589,20 @@ func TestManifestAPI(t *testing.T) { assert.Contains(tt, op.ID, "credentials/responses/") // review application - reviewApplicationRequestValue := newRequestValue(tt, router.ReviewApplicationRequest{Approved: true, Reason: "I'm the almighty approver"}) + expireAt := time.Date(2025, 10, 32, 0, 0, 0, 0, time.UTC) + reviewApplicationRequestValue := newRequestValue(tt, router.ReviewApplicationRequest{ + Approved: true, + Reason: "I'm the almighty approver", + CredentialOverrides: map[string]manifestsvc.CredentialOverride{ + "id1": { + Data: map[string]any{ + "looks": "pretty darn handsome", + }, + Expiry: &expireAt, + Revocable: true, + }, + }, + }) applicationID := storage.StatusObjectID(op.ID) req = httptest.NewRequest(http.MethodPut, "https://ssi-service.com/v1/manifests/applications/"+applicationID+"/review", reviewApplicationRequestValue) err = manifestRouter.ReviewApplication(newRequestContextWithParams(map[string]string{"id": applicationID}), w, req) @@ -597,6 +618,16 @@ func TestManifestAPI(t *testing.T) { assert.Len(tt, appResp.Response.Fulfillment.DescriptorMap, 2) assert.Len(tt, appResp.Credentials, 2) assert.Empty(tt, appResp.Response.Denial) + + vc, err := util.CredentialsFromInterface(appResp.Credentials.([]any)[0]) + assert.NoError(tt, err) + assert.Equal(tt, credsdk.CredentialSubject{ + "id": applicantDID.DID.ID, + "looks": "pretty darn handsome", + }, vc.CredentialSubject) + assert.Equal(tt, expireAt.Format(time.RFC3339), vc.ExpirationDate) + assert.NotEmpty(tt, vc.CredentialStatus) + assert.Equal(tt, createdSchema.ID, vc.CredentialSchema.ID) }) t.Run("Test Denied Application", func(tt *testing.T) { diff --git a/pkg/service/manifest/model/model.go b/pkg/service/manifest/model/model.go index bf50625c2..e20da9502 100644 --- a/pkg/service/manifest/model/model.go +++ b/pkg/service/manifest/model/model.go @@ -1,6 +1,8 @@ package model import ( + "time" + "github.com/TBD54566975/ssi-sdk/credential/exchange" manifestsdk "github.com/TBD54566975/ssi-sdk/credential/manifest" cred "github.com/tbd54566975/ssi-service/internal/credential" @@ -93,6 +95,8 @@ type ReviewApplicationRequest struct { ID string `json:"id" validate:"required"` Approved bool `json:"approved"` Reason string `json:"reason"` + + CredentialOverrides map[string]CredentialOverride `json:"credential_overrides,omitempty"` } // Response @@ -121,3 +125,14 @@ func ServiceModel(storedResponse *storage.StoredResponse) SubmitApplicationRespo ResponseJWT: storedResponse.ResponseJWT, } } + +type CredentialOverride struct { + // Data that will be used to determine credential claims. + Data map[string]any `json:"data"` + + // Parameter to determine the expiry of the credential. + Expiry *time.Time `json:"expiry"` + + // Whether the credentials created should be revocable. + Revocable bool `json:"revocable"` +} diff --git a/pkg/service/manifest/response.go b/pkg/service/manifest/response.go index 7aa522db7..b842180e5 100644 --- a/pkg/service/manifest/response.go +++ b/pkg/service/manifest/response.go @@ -18,6 +18,7 @@ import ( "github.com/tbd54566975/ssi-service/pkg/service/credential" "github.com/tbd54566975/ssi-service/pkg/service/issuing" "github.com/tbd54566975/ssi-service/pkg/service/keystore" + "github.com/tbd54566975/ssi-service/pkg/service/manifest/model" ) const ( @@ -56,6 +57,7 @@ func (s Service) buildCredentialResponse( template *issuing.IssuanceTemplate, application manifest.CredentialApplication, applicationJSON map[string]any, + credentialOverrides map[string]model.CredentialOverride, ) (*manifest.CredentialResponse, []cred.Container, error) { // TODO(gabe) need to check if this can be fulfilled and conditionally return success/denial applicationID := application.ID @@ -98,6 +100,7 @@ func (s Service) buildCredentialResponse( return nil, nil, err } } + s.applyRequestData(&credentialRequest, credentialOverrides, od) credentialResponse, err := s.credential.CreateCredential(ctx, credentialRequest) if err != nil { @@ -146,6 +149,19 @@ func (s Service) buildCredentialResponse( return credRes, creds, nil } +func (s Service) applyRequestData(credentialRequest *credential.CreateCredentialRequest, credentialOverrides map[string]model.CredentialOverride, od manifest.OutputDescriptor) { + if credentialOverride, ok := credentialOverrides[od.ID]; ok { + for k, v := range credentialOverride.Data { + credentialRequest.Data[k] = v + } + + if credentialOverride.Expiry != nil { + credentialRequest.Expiry = credentialOverride.Expiry.Format(time.RFC3339) + } + credentialRequest.Revocable = credentialOverride.Revocable + } +} + func (s Service) applyIssuanceTemplate( credentialRequest *credential.CreateCredentialRequest, template *issuing.IssuanceTemplate, diff --git a/pkg/service/manifest/service.go b/pkg/service/manifest/service.go index addd6a9fd..518466153 100644 --- a/pkg/service/manifest/service.go +++ b/pkg/service/manifest/service.go @@ -374,17 +374,7 @@ func (s Service) maybeIssueAutomatically( logrus.Warnf("found multiple issuance templates, using first entry only") } - credResp, creds, err := s.buildCredentialResponse( - ctx, - applicantDID, - manifestID, - gotManifest.Manifest, - true, - "automatic creation via issuance template", - &issuanceTemplate, - request.Application, - request.ApplicationJSON, - ) + credResp, creds, err := s.buildCredentialResponse(ctx, applicantDID, manifestID, gotManifest.Manifest, true, "automatic creation via issuance template", &issuanceTemplate, request.Application, request.ApplicationJSON, nil) if err != nil { return nil, err } @@ -458,6 +448,7 @@ func (s Service) ReviewApplication(ctx context.Context, request model.ReviewAppl nil, application.Application, nil, + request.CredentialOverrides, ) if err != nil { return nil, util.LoggingErrorMsg(err, "could not build credential response")