Skip to content
This repository has been archived by the owner on Dec 12, 2024. It is now read-only.

Commit

Permalink
ReviewApplication endpoint for fulfilling or denying applications (#240)
Browse files Browse the repository at this point in the history
* Moved the denial/fulfillment from SubmitApplication into ReviewApplication

* typo

* Included Review in e2e tests.
  • Loading branch information
andresuribe87 authored Jan 6, 2023
1 parent 8669899 commit 217bf1d
Show file tree
Hide file tree
Showing 13 changed files with 656 additions and 239 deletions.
541 changes: 356 additions & 185 deletions doc/swagger.yaml

Large diffs are not rendered by default.

39 changes: 38 additions & 1 deletion integration/common.go
Original file line number Diff line number Diff line change
Expand Up @@ -307,7 +307,23 @@ func getJSONElement(jsonString string, jsonPath string) (string, error) {
return "", errors.Wrap(err, "finding element in json string")
}

elementStr := fmt.Sprintf("%v", element)
if element == nil {
return "<nil>", nil
}
var elementStr string
switch element.(type) {
case bool:
elementStr = fmt.Sprintf("%v", element)
case string:
elementStr = fmt.Sprintf("%v", element)
case map[string]any:
data, err := json.Marshal(element)
if err != nil {
return "", err
}
elementStr = compactJSONOutput(string(data))
}

return elementStr, nil
}

Expand All @@ -328,6 +344,7 @@ func get(url string) (string, error) {
return "", fmt.Errorf("status code not in the 200s. body: %s", string(body))
}

logrus.Infof("Received: %s", string(body))
return string(body), err
}

Expand Down Expand Up @@ -389,3 +406,23 @@ func getValidApplicationRequest(credAppJSON string, credentialJWT string) manife
Credentials: creds,
}
}

type reviewApplicationParams struct {
ID string
Approved bool
Reason string
}

func ReviewApplication(params reviewApplicationParams) (string, error) {
trueApplicationJSON, err := resolveTemplate(params, "review-application-input.json")
if err != nil {
return "", err
}

output, err := put(endpoint+version+"manifests/applications/"+params.ID+"/review", trueApplicationJSON)
if err != nil {
return "", errors.Wrapf(err, "application endpoint with output: %s", output)
}

return output, nil
}
33 changes: 28 additions & 5 deletions integration/didweb_integration_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"testing"

"github.com/stretchr/testify/assert"
"github.com/tbd54566975/ssi-service/pkg/service/operation/storage"
)

var didWebContext = NewTestContext("DIDWeb")
Expand Down Expand Up @@ -118,7 +119,7 @@ func TestDidWebCreateCredentialManifestIntegration(t *testing.T) {
assert.NotEmpty(t, manifestID)
}

func TestDidWebSubmitApplicationIntegration(t *testing.T) {
func TestDidWebSubmitAndReviewApplicationIntegration(t *testing.T) {
if testing.Short() {
t.Skip("skipping integration test")
}
Expand Down Expand Up @@ -150,17 +151,39 @@ func TestDidWebSubmitApplicationIntegration(t *testing.T) {
assert.NoError(t, err)
assert.NotEmpty(t, credAppJWT)

credentialResponseOutput, err := SubmitApplication(applicationParams{
submitApplicationOutput, err := SubmitApplication(applicationParams{
ApplicationJWT: credAppJWT,
})
assert.NoError(t, err)
assert.NotEmpty(t, credentialResponseOutput)
assert.NotEmpty(t, submitApplicationOutput)

crManifestID, err := getJSONElement(credentialResponseOutput, "$.result.response.credential_response.manifest_id")
isDone, err := getJSONElement(submitApplicationOutput, "$.done")
assert.NoError(t, err)
assert.Equal(t, "false", isDone)
opID, err := getJSONElement(submitApplicationOutput, "$.id")
assert.NoError(t, err)

reviewApplicationOutput, err := ReviewApplication(reviewApplicationParams{
ID: storage.StatusObjectID(opID),
Approved: true,
Reason: "oh yeah im testing",
})
assert.NoError(t, err)
crManifestID, err := getJSONElement(reviewApplicationOutput, "$.credential_response.manifest_id")
assert.NoError(t, err)
assert.Equal(t, manifestID, crManifestID)

vc, err := getJSONElement(credentialResponseOutput, "$.result.response.verifiableCredentials[0]")
vc, err := getJSONElement(reviewApplicationOutput, "$.verifiableCredentials[0]")
assert.NoError(t, err)
assert.NotEmpty(t, vc)

operationOutput, err := get(endpoint + version + "operations/" + opID)
assert.NoError(t, err)
isDone, err = getJSONElement(operationOutput, "$.done")
assert.NoError(t, err)
assert.Equal(t, "true", isDone)

opCredentialResponse, err := getJSONElement(operationOutput, "$.result.response")
assert.NoError(t, err)
assert.JSONEq(t, reviewApplicationOutput, opCredentialResponse)
}
34 changes: 29 additions & 5 deletions integration/steelthread_integration_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"testing"

"github.com/stretchr/testify/assert"
"github.com/tbd54566975/ssi-service/pkg/service/operation/storage"
)

var steelThreadContext = NewTestContext("SteelThread")
Expand Down Expand Up @@ -118,7 +119,7 @@ func TestCreateCredentialManifestIntegration(t *testing.T) {
assert.NotEmpty(t, manifestID)
}

func TestSubmitApplicationIntegration(t *testing.T) {
func TestSubmitAndReviewApplicationIntegration(t *testing.T) {
if testing.Short() {
t.Skip("skipping integration test")
}
Expand Down Expand Up @@ -150,17 +151,40 @@ func TestSubmitApplicationIntegration(t *testing.T) {
assert.NoError(t, err)
assert.NotEmpty(t, credAppJWT)

credentialResponseOutput, err := SubmitApplication(applicationParams{
submitApplicationOutput, err := SubmitApplication(applicationParams{
ApplicationJWT: credAppJWT,
})
assert.NoError(t, err)
assert.NotEmpty(t, credentialResponseOutput)
assert.NotEmpty(t, submitApplicationOutput)

crManifestID, err := getJSONElement(credentialResponseOutput, "$.result.response.credential_response.manifest_id")
isDone, err := getJSONElement(submitApplicationOutput, "$.done")
assert.NoError(t, err)
assert.Equal(t, "false", isDone)
opID, err := getJSONElement(submitApplicationOutput, "$.id")
assert.NoError(t, err)

reviewApplicationOutput, err := ReviewApplication(reviewApplicationParams{
ID: storage.StatusObjectID(opID),
Approved: true,
Reason: "oh yeah im testing",
})
assert.NoError(t, err)

crManifestID, err := getJSONElement(reviewApplicationOutput, "$.credential_response.manifest_id")
assert.NoError(t, err)
assert.Equal(t, manifestID, crManifestID)

vc, err := getJSONElement(credentialResponseOutput, "$.result.response.verifiableCredentials[0]")
vc, err := getJSONElement(reviewApplicationOutput, "$.verifiableCredentials[0]")
assert.NoError(t, err)
assert.NotEmpty(t, vc)

operationOutput, err := get(endpoint + version + "operations/" + opID)
assert.NoError(t, err)
isDone, err = getJSONElement(operationOutput, "$.done")
assert.NoError(t, err)
assert.Equal(t, "true", isDone)

opCredentialResponse, err := getJSONElement(operationOutput, "$.result.response")
assert.NoError(t, err)
assert.JSONEq(t, reviewApplicationOutput, opCredentialResponse)
}
4 changes: 4 additions & 0 deletions integration/testdata/review-application-input.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"approved": {{.Approved}},
"reason": "{{.Reason}}"
}
51 changes: 50 additions & 1 deletion pkg/server/router/manifest.go
Original file line number Diff line number Diff line change
Expand Up @@ -283,7 +283,7 @@ func (sar SubmitApplicationRequest) ToServiceRequest() (*model.SubmitApplication
type SubmitApplicationResponse struct {
Response manifestsdk.CredentialResponse `json:"credential_response"`
// this is an interface type to union Data Integrity and JWT style VCs
Credentials []any `json:"verifiableCredentials,omitempty"`
Credentials any `json:"verifiableCredentials,omitempty"`
ResponseJWT keyaccess.JWT `json:"responseJwt,omitempty"`
}

Expand Down Expand Up @@ -513,3 +513,52 @@ func (mr ManifestRouter) DeleteResponse(ctx context.Context, w http.ResponseWrit

return framework.Respond(ctx, w, nil, http.StatusOK)
}

type ReviewApplicationRequest struct {
Approved bool `json:"approved"`
Reason string `json:"reason"`
}

func (r ReviewApplicationRequest) toServiceRequest(id string) model.ReviewApplicationRequest {
return model.ReviewApplicationRequest{
ID: id,
Approved: r.Approved,
Reason: r.Reason,
}
}

// ReviewApplication godoc
// @Summary Reviews an application
// @Description Reviewing an application either fulfills or denies the credential.
// @Tags ApplicationAPI
// @Accept json
// @Produce json
// @Param request body ReviewApplicationRequest true "request body"
// @Success 201 {object} SubmitApplicationResponse "Credential Response"
// @Failure 400 {string} string "Bad request"
// @Failure 500 {string} string "Internal server error"
// @Router /v1/manifests/applications/{id}/review [put]
func (mr ManifestRouter) ReviewApplication(ctx context.Context, w http.ResponseWriter, r *http.Request) error {
id := framework.GetParam(ctx, IDParam)
if id == nil {
return framework.NewRequestError(
util.LoggingNewError("review application request requires id"), http.StatusBadRequest)
}

var request ReviewApplicationRequest
if err := framework.Decode(r, &request); err != nil {
return framework.NewRequestError(
util.LoggingErrorMsg(err, "invalid review application request"), http.StatusBadRequest)
}

applicationResponse, err := mr.service.ReviewApplication(request.toServiceRequest(*id))
if err != nil {
return framework.NewRequestError(
util.LoggingErrorMsg(err, "failed reviewing application"), http.StatusInternalServerError)
}
return framework.Respond(ctx, w, SubmitApplicationResponse{
Response: applicationResponse.Response,
Credentials: applicationResponse.Credentials,
ResponseJWT: applicationResponse.ResponseJWT,
}, http.StatusOK)
}
12 changes: 9 additions & 3 deletions pkg/server/router/manifest_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@ import (
"github.com/google/uuid"
"github.com/mr-tron/base58"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/tbd54566975/ssi-service/pkg/service/manifest/model"
"github.com/tbd54566975/ssi-service/pkg/service/operation/storage"

credmodel "github.com/tbd54566975/ssi-service/internal/credential"
"github.com/tbd54566975/ssi-service/internal/keyaccess"
Expand Down Expand Up @@ -125,8 +125,14 @@ func TestManifestRouter(t *testing.T) {
assert.NoError(tt, err)
createdApplicationResponseOp, err := manifestService.ProcessApplicationSubmission(*sar)
assert.NoError(tt, err)
createdApplicationResponse, ok := createdApplicationResponseOp.Result.Response.(model.SubmitApplicationResponse)
require.True(tt, ok)
assert.False(tt, createdApplicationResponseOp.Done)

createdApplicationResponse, err := manifestService.ReviewApplication(model.ReviewApplicationRequest{
ID: storage.StatusObjectID(createdApplicationResponseOp.ID),
Approved: true,
Reason: "ApprovalMan is here",
})
assert.NoError(tt, err)
assert.NotEmpty(tt, createdManifest)
assert.NotEmpty(tt, createdApplicationResponse.Response.ID)
assert.NotEmpty(tt, createdApplicationResponse.Response.Fulfillment)
Expand Down
1 change: 1 addition & 0 deletions pkg/server/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -245,6 +245,7 @@ func (s *SSIServer) ManifestAPI(service svcframework.Service) (err error) {
s.Handle(http.MethodGet, applicationsHandlerPath, manifestRouter.GetApplications)
s.Handle(http.MethodGet, path.Join(applicationsHandlerPath, "/:id"), manifestRouter.GetApplication)
s.Handle(http.MethodDelete, path.Join(applicationsHandlerPath, "/:id"), manifestRouter.DeleteApplication)
s.Handle(http.MethodPut, path.Join(applicationsHandlerPath, "/:id", "/review"), manifestRouter.ReviewApplication)

s.Handle(http.MethodGet, responsesHandlerPath, manifestRouter.GetResponses)
s.Handle(http.MethodGet, path.Join(responsesHandlerPath, "/:id"), manifestRouter.GetResponse)
Expand Down
29 changes: 21 additions & 8 deletions pkg/server/server_manifest_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,13 @@ import (
"github.com/mr-tron/base58"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
manifestsvc "github.com/tbd54566975/ssi-service/pkg/service/manifest/model"

credmodel "github.com/tbd54566975/ssi-service/internal/credential"
"github.com/tbd54566975/ssi-service/internal/keyaccess"
"github.com/tbd54566975/ssi-service/pkg/server/router"
"github.com/tbd54566975/ssi-service/pkg/service/credential"
"github.com/tbd54566975/ssi-service/pkg/service/did"
manifestsvc "github.com/tbd54566975/ssi-service/pkg/service/manifest/model"
"github.com/tbd54566975/ssi-service/pkg/service/operation/storage"
"github.com/tbd54566975/ssi-service/pkg/service/schema"
)

Expand Down Expand Up @@ -416,10 +416,18 @@ func TestManifestAPI(t *testing.T) {
err = json.NewDecoder(w.Body).Decode(&op)
assert.NoError(tt, err)

var appResp router.SubmitApplicationResponse
respData, err := json.Marshal(op.Result.Response)
assert.False(tt, op.Done)
assert.Contains(tt, op.ID, "credentials/responses/")

// review application
reviewApplicationRequestValue := newRequestValue(tt, router.ReviewApplicationRequest{Approved: true, Reason: "I'm the almighty approver"})
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)
assert.NoError(tt, err)
err = json.Unmarshal(respData, &appResp)

var appResp router.SubmitApplicationResponse
err = json.NewDecoder(w.Body).Decode(&appResp)
assert.NoError(tt, err)

assert.NotEmpty(tt, appResp.Response)
Expand Down Expand Up @@ -685,10 +693,15 @@ func TestManifestAPI(t *testing.T) {
err = json.NewDecoder(w.Body).Decode(&op)
assert.NoError(tt, err)

var appResp router.SubmitApplicationResponse
respData, err := json.Marshal(op.Result.Response)
// review application
reviewApplicationRequestValue := newRequestValue(tt, router.ReviewApplicationRequest{Approved: true, Reason: "I'm the almighty approver"})
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)
assert.NoError(tt, err)
err = json.Unmarshal(respData, &appResp)

var appResp router.SubmitApplicationResponse
err = json.NewDecoder(w.Body).Decode(&appResp)
assert.NoError(tt, err)

assert.NotEmpty(tt, appResp.Response.Fulfillment)
Expand Down
8 changes: 8 additions & 0 deletions pkg/service/manifest/model/model.go
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,14 @@ type DeleteApplicationRequest struct {
ID string `json:"id,omitempty" validate:"required"`
}

// ReviewApplicationRequest is something foobar
type ReviewApplicationRequest struct {
// ID of the application.
ID string `json:"id" validate:"required"`
Approved bool `json:"approved"`
Reason string `json:"reason"`
}

// Response

type GetResponseRequest struct {
Expand Down
13 changes: 10 additions & 3 deletions pkg/service/manifest/response.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"github.com/TBD54566975/ssi-sdk/credential/exchange"
"github.com/TBD54566975/ssi-sdk/credential/manifest"
errresp "github.com/TBD54566975/ssi-sdk/error"
"github.com/pkg/errors"

cred "github.com/tbd54566975/ssi-service/internal/credential"
"github.com/tbd54566975/ssi-service/internal/keyaccess"
Expand Down Expand Up @@ -37,7 +38,7 @@ func (s Service) signCredentialResponseJWT(signingDID string, r CredentialRespon
return responseToken, nil
}

func (s Service) buildCredentialResponse(applicantDID, manifestID, applicationID string, credManifest manifest.CredentialManifest) (*manifest.CredentialResponse, []cred.Container, error) {
func (s Service) buildCredentialResponse(applicantDID, manifestID, applicationID string, credManifest manifest.CredentialManifest, approved bool, reason string) (*manifest.CredentialResponse, []cred.Container, error) {
// TODO(gabe) need to check if this can be fulfilled and conditionally return success/denial
responseBuilder := manifest.NewCredentialResponseBuilder(manifestID)
if err := responseBuilder.SetApplicationID(applicationID); err != nil {
Expand Down Expand Up @@ -80,8 +81,14 @@ func (s Service) buildCredentialResponse(applicantDID, manifestID, applicationID
}

// set the information for the fulfilled credentials in the response
if err := responseBuilder.SetFulfillment(descriptors); err != nil {
return nil, nil, util.LoggingErrorMsg(err, "could not fulfill credential application: could not set fulfillment")
if approved {
if err := responseBuilder.SetFulfillment(descriptors); err != nil {
return nil, nil, util.LoggingErrorMsg(err, "could not fulfill credential application: could not set fulfillment")
}
} else {
if err := responseBuilder.SetDenial(reason); err != nil {
return nil, nil, errors.Wrap(err, "setting denial")
}
}
credRes, err := responseBuilder.Build()
if err != nil {
Expand Down
Loading

0 comments on commit 217bf1d

Please sign in to comment.