From 9913b5a3522048308665d464cde675f89a22c667 Mon Sep 17 00:00:00 2001 From: Wout Slakhorst Date: Tue, 5 Dec 2023 14:25:16 +0100 Subject: [PATCH] load a VC into a wallet via the internal API (#2643) --- docs/_static/vcr/vcr_v2.yaml | 32 +++++ vcr/api/vcr/v2/api.go | 17 +++ vcr/api/vcr/v2/api_test.go | 40 ++++++ vcr/api/vcr/v2/generated.go | 261 +++++++++++++++++++++++++++++++++++ 4 files changed, 350 insertions(+) diff --git a/docs/_static/vcr/vcr_v2.yaml b/docs/_static/vcr/vcr_v2.yaml index a037c1ff0b..ba6a134c3e 100644 --- a/docs/_static/vcr/vcr_v2.yaml +++ b/docs/_static/vcr/vcr_v2.yaml @@ -416,7 +416,39 @@ paths: $ref: "#/components/schemas/VerifiablePresentation" default: $ref: '../common/error_response.yaml' + /internal/vcr/v2/holder/{did}/vc: + post: + summary: Load a VerifiableCredential into the holders wallet. + description: | + If a VerifiableCredential is not directly issued to the wallet through e.g. OpenID4VCI, this API allows to add it to a wallet. + The DID of the holder has to be provided in the path. + It's assumed that the credentialSubject.id equals the holder DID. + error returns: + * 400 - Invalid credential + * 500 - An error occurred while processing the request + operationId: loadVC + tags: + - credential + parameters: + - name: did + in: path + description: URL encoded DID. + required: true + example: "did:web:example.com:iam:123" + schema: + type: string + requestBody: + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/VerifiableCredential" + responses: + "204": + description: The credential will not be altered in any way, so no need to return it. + default: + $ref: '../common/error_response.yaml' components: schemas: VerifiableCredential: diff --git a/vcr/api/vcr/v2/api.go b/vcr/api/vcr/v2/api.go index d77368d0d6..1240dece68 100644 --- a/vcr/api/vcr/v2/api.go +++ b/vcr/api/vcr/v2/api.go @@ -321,6 +321,23 @@ func (w *Wrapper) VerifyVP(ctx context.Context, request VerifyVPRequestObject) ( return VerifyVP200JSONResponse(result), nil } +func (w *Wrapper) LoadVC(ctx context.Context, request LoadVCRequestObject) (LoadVCResponseObject, error) { + // the actual holder is ignored for now, since we only support a single wallet... + _, err := did.ParseDID(request.Did) + if err != nil { + return nil, core.InvalidInputError("invalid holder did: %w", err) + } + + if request.Body == nil { + return nil, core.InvalidInputError("missing credential in body") + } + err = w.VCR.Wallet().Put(ctx, *request.Body) + if err != nil { + return nil, err + } + return LoadVC204Response{}, nil +} + // TrustIssuer handles API request to start trusting an issuer of a Verifiable Credential. func (w *Wrapper) TrustIssuer(ctx context.Context, request TrustIssuerRequestObject) (TrustIssuerResponseObject, error) { if err := changeTrust(*request.Body, w.VCR.Trust); err != nil { diff --git a/vcr/api/vcr/v2/api_test.go b/vcr/api/vcr/v2/api_test.go index 0a417864ed..73d2cae3b2 100644 --- a/vcr/api/vcr/v2/api_test.go +++ b/vcr/api/vcr/v2/api_test.go @@ -505,6 +505,46 @@ func parsedTimeStr(t time.Time) (time.Time, string) { return parsed, formatted } +func TestWrapper_LoadVC(t *testing.T) { + holderDID := "did:web:example.com:iam:123" + credentialID := "did:web:example.com:iam:456#1" + credentialURI := ssi.MustParseURI(credentialID) + expectedVC := vc.VerifiableCredential{ID: &credentialURI} + + t.Run("test integration with vcr", func(t *testing.T) { + t.Run("successful load", func(t *testing.T) { + testContext := newMockContext(t) + testContext.mockWallet.EXPECT().Put(gomock.Any(), expectedVC).Return(nil) + + response, err := testContext.client.LoadVC(testContext.requestCtx, LoadVCRequestObject{Did: holderDID, Body: &expectedVC}) + + assert.NoError(t, err) + assert.IsType(t, response, LoadVC204Response{}) + }) + + t.Run("vcr returns an error", func(t *testing.T) { + testContext := newMockContext(t) + testContext.mockWallet.EXPECT().Put(gomock.Any(), expectedVC).Return(assert.AnError) + + response, err := testContext.client.LoadVC(testContext.requestCtx, LoadVCRequestObject{Did: holderDID, Body: &expectedVC}) + + assert.Empty(t, response) + assert.EqualError(t, err, assert.AnError.Error()) + }) + }) + + t.Run("param check", func(t *testing.T) { + t.Run("invalid credential id format", func(t *testing.T) { + testContext := newMockContext(t) + + response, err := testContext.client.LoadVC(testContext.requestCtx, LoadVCRequestObject{Did: "%%"}) + + assert.Empty(t, response) + assert.EqualError(t, err, "invalid holder did: invalid DID") + }) + }) +} + func TestWrapper_CreateVP(t *testing.T) { issuerURI := ssi.MustParseURI("did:nuts:123") credentialType := ssi.MustParseURI("ExampleType") diff --git a/vcr/api/vcr/v2/generated.go b/vcr/api/vcr/v2/generated.go index 0e8278bb6a..7d71b4ae0b 100644 --- a/vcr/api/vcr/v2/generated.go +++ b/vcr/api/vcr/v2/generated.go @@ -226,6 +226,9 @@ type SearchIssuedVCsParams struct { // CreateVPJSONRequestBody defines body for CreateVP for application/json ContentType. type CreateVPJSONRequestBody = CreateVPRequest +// LoadVCJSONRequestBody defines body for LoadVC for application/json ContentType. +type LoadVCJSONRequestBody = VerifiableCredential + // IssueVCJSONRequestBody defines body for IssueVC for application/json ContentType. type IssueVCJSONRequestBody = IssueVCRequest @@ -322,6 +325,11 @@ type ClientInterface interface { CreateVP(ctx context.Context, body CreateVPJSONRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error) + // LoadVCWithBody request with any body + LoadVCWithBody(ctx context.Context, did string, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) + + LoadVC(ctx context.Context, did string, body LoadVCJSONRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error) + // IssueVCWithBody request with any body IssueVCWithBody(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) @@ -392,6 +400,30 @@ func (c *Client) CreateVP(ctx context.Context, body CreateVPJSONRequestBody, req return c.Client.Do(req) } +func (c *Client) LoadVCWithBody(ctx context.Context, did string, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) { + req, err := NewLoadVCRequestWithBody(c.Server, did, contentType, body) + if err != nil { + return nil, err + } + req = req.WithContext(ctx) + if err := c.applyEditors(ctx, req, reqEditors); err != nil { + return nil, err + } + return c.Client.Do(req) +} + +func (c *Client) LoadVC(ctx context.Context, did string, body LoadVCJSONRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error) { + req, err := NewLoadVCRequest(c.Server, did, body) + if err != nil { + return nil, err + } + req = req.WithContext(ctx) + if err := c.applyEditors(ctx, req, reqEditors); err != nil { + return nil, err + } + return c.Client.Do(req) +} + func (c *Client) IssueVCWithBody(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) { req, err := NewIssueVCRequestWithBody(c.Server, contentType, body) if err != nil { @@ -636,6 +668,53 @@ func NewCreateVPRequestWithBody(server string, contentType string, body io.Reade return req, nil } +// NewLoadVCRequest calls the generic LoadVC builder with application/json body +func NewLoadVCRequest(server string, did string, body LoadVCJSONRequestBody) (*http.Request, error) { + var bodyReader io.Reader + buf, err := json.Marshal(body) + if err != nil { + return nil, err + } + bodyReader = bytes.NewReader(buf) + return NewLoadVCRequestWithBody(server, did, "application/json", bodyReader) +} + +// NewLoadVCRequestWithBody generates requests for LoadVC with any type of body +func NewLoadVCRequestWithBody(server string, did string, contentType string, body io.Reader) (*http.Request, error) { + var err error + + var pathParam0 string + + pathParam0, err = runtime.StyleParamWithLocation("simple", false, "did", runtime.ParamLocationPath, did) + if err != nil { + return nil, err + } + + serverURL, err := url.Parse(server) + if err != nil { + return nil, err + } + + operationPath := fmt.Sprintf("/internal/vcr/v2/holder/%s/vc", pathParam0) + if operationPath[0] == '/' { + operationPath = "." + operationPath + } + + queryURL, err := serverURL.Parse(operationPath) + if err != nil { + return nil, err + } + + req, err := http.NewRequest("POST", queryURL.String(), body) + if err != nil { + return nil, err + } + + req.Header.Add("Content-Type", contentType) + + return req, nil +} + // NewIssueVCRequest calls the generic IssueVC builder with application/json body func NewIssueVCRequest(server string, body IssueVCJSONRequestBody) (*http.Request, error) { var bodyReader io.Reader @@ -1133,6 +1212,11 @@ type ClientWithResponsesInterface interface { CreateVPWithResponse(ctx context.Context, body CreateVPJSONRequestBody, reqEditors ...RequestEditorFn) (*CreateVPResponse, error) + // LoadVCWithBodyWithResponse request with any body + LoadVCWithBodyWithResponse(ctx context.Context, did string, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*LoadVCResponse, error) + + LoadVCWithResponse(ctx context.Context, did string, body LoadVCJSONRequestBody, reqEditors ...RequestEditorFn) (*LoadVCResponse, error) + // IssueVCWithBodyWithResponse request with any body IssueVCWithBodyWithResponse(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*IssueVCResponse, error) @@ -1211,6 +1295,37 @@ func (r CreateVPResponse) StatusCode() int { return 0 } +type LoadVCResponse struct { + Body []byte + HTTPResponse *http.Response + ApplicationproblemJSONDefault *struct { + // Detail A human-readable explanation specific to this occurrence of the problem. + Detail string `json:"detail"` + + // Status HTTP statuscode + Status float32 `json:"status"` + + // Title A short, human-readable summary of the problem type. + Title string `json:"title"` + } +} + +// Status returns HTTPResponse.Status +func (r LoadVCResponse) Status() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Status + } + return http.StatusText(0) +} + +// StatusCode returns HTTPResponse.StatusCode +func (r LoadVCResponse) StatusCode() int { + if r.HTTPResponse != nil { + return r.HTTPResponse.StatusCode + } + return 0 +} + type IssueVCResponse struct { Body []byte HTTPResponse *http.Response @@ -1578,6 +1693,23 @@ func (c *ClientWithResponses) CreateVPWithResponse(ctx context.Context, body Cre return ParseCreateVPResponse(rsp) } +// LoadVCWithBodyWithResponse request with arbitrary body returning *LoadVCResponse +func (c *ClientWithResponses) LoadVCWithBodyWithResponse(ctx context.Context, did string, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*LoadVCResponse, error) { + rsp, err := c.LoadVCWithBody(ctx, did, contentType, body, reqEditors...) + if err != nil { + return nil, err + } + return ParseLoadVCResponse(rsp) +} + +func (c *ClientWithResponses) LoadVCWithResponse(ctx context.Context, did string, body LoadVCJSONRequestBody, reqEditors ...RequestEditorFn) (*LoadVCResponse, error) { + rsp, err := c.LoadVC(ctx, did, body, reqEditors...) + if err != nil { + return nil, err + } + return ParseLoadVCResponse(rsp) +} + // IssueVCWithBodyWithResponse request with arbitrary body returning *IssueVCResponse func (c *ClientWithResponses) IssueVCWithBodyWithResponse(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*IssueVCResponse, error) { rsp, err := c.IssueVCWithBody(ctx, contentType, body, reqEditors...) @@ -1767,6 +1899,41 @@ func ParseCreateVPResponse(rsp *http.Response) (*CreateVPResponse, error) { return response, nil } +// ParseLoadVCResponse parses an HTTP response from a LoadVCWithResponse call +func ParseLoadVCResponse(rsp *http.Response) (*LoadVCResponse, error) { + bodyBytes, err := io.ReadAll(rsp.Body) + defer func() { _ = rsp.Body.Close() }() + if err != nil { + return nil, err + } + + response := &LoadVCResponse{ + Body: bodyBytes, + HTTPResponse: rsp, + } + + switch { + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && true: + var dest struct { + // Detail A human-readable explanation specific to this occurrence of the problem. + Detail string `json:"detail"` + + // Status HTTP statuscode + Status float32 `json:"status"` + + // Title A short, human-readable summary of the problem type. + Title string `json:"title"` + } + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.ApplicationproblemJSONDefault = &dest + + } + + return response, nil +} + // ParseIssueVCResponse parses an HTTP response from a IssueVCWithResponse call func ParseIssueVCResponse(rsp *http.Response) (*IssueVCResponse, error) { bodyBytes, err := io.ReadAll(rsp.Body) @@ -2220,6 +2387,9 @@ type ServerInterface interface { // Create a new Verifiable Presentation for a set of Verifiable Credentials. // (POST /internal/vcr/v2/holder/vp) CreateVP(ctx echo.Context) error + // Load a VerifiableCredential into the holders wallet. + // (POST /internal/vcr/v2/holder/{did}/vc) + LoadVC(ctx echo.Context, did string) error // Issues a new Verifiable Credential // (POST /internal/vcr/v2/issuer/vc) IssueVC(ctx echo.Context) error @@ -2271,6 +2441,24 @@ func (w *ServerInterfaceWrapper) CreateVP(ctx echo.Context) error { return err } +// LoadVC converts echo context to params. +func (w *ServerInterfaceWrapper) LoadVC(ctx echo.Context) error { + var err error + // ------------- Path parameter "did" ------------- + var did string + + err = runtime.BindStyledParameterWithLocation("simple", false, "did", runtime.ParamLocationPath, ctx.Param("did"), &did) + if err != nil { + return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Invalid format for parameter did: %s", err)) + } + + ctx.Set(JwtBearerAuthScopes, []string{}) + + // Invoke the callback with all the unmarshaled arguments + err = w.Handler.LoadVC(ctx, did) + return err +} + // IssueVC converts echo context to params. func (w *ServerInterfaceWrapper) IssueVC(ctx echo.Context) error { var err error @@ -2472,6 +2660,7 @@ func RegisterHandlersWithBaseURL(router EchoRouter, si ServerInterface, baseURL } router.POST(baseURL+"/internal/vcr/v2/holder/vp", wrapper.CreateVP) + router.POST(baseURL+"/internal/vcr/v2/holder/:did/vc", wrapper.LoadVC) router.POST(baseURL+"/internal/vcr/v2/issuer/vc", wrapper.IssueVC) router.GET(baseURL+"/internal/vcr/v2/issuer/vc/search", wrapper.SearchIssuedVCs) router.DELETE(baseURL+"/internal/vcr/v2/issuer/vc/:id", wrapper.RevokeVC) @@ -2524,6 +2713,44 @@ func (response CreateVPdefaultApplicationProblemPlusJSONResponse) VisitCreateVPR return json.NewEncoder(w).Encode(response.Body) } +type LoadVCRequestObject struct { + Did string `json:"did"` + Body *LoadVCJSONRequestBody +} + +type LoadVCResponseObject interface { + VisitLoadVCResponse(w http.ResponseWriter) error +} + +type LoadVC204Response struct { +} + +func (response LoadVC204Response) VisitLoadVCResponse(w http.ResponseWriter) error { + w.WriteHeader(204) + return nil +} + +type LoadVCdefaultApplicationProblemPlusJSONResponse struct { + Body struct { + // Detail A human-readable explanation specific to this occurrence of the problem. + Detail string `json:"detail"` + + // Status HTTP statuscode + Status float32 `json:"status"` + + // Title A short, human-readable summary of the problem type. + Title string `json:"title"` + } + StatusCode int +} + +func (response LoadVCdefaultApplicationProblemPlusJSONResponse) VisitLoadVCResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/problem+json") + w.WriteHeader(response.StatusCode) + + return json.NewEncoder(w).Encode(response.Body) +} + type IssueVCRequestObject struct { Body *IssueVCJSONRequestBody } @@ -2945,6 +3172,9 @@ type StrictServerInterface interface { // Create a new Verifiable Presentation for a set of Verifiable Credentials. // (POST /internal/vcr/v2/holder/vp) CreateVP(ctx context.Context, request CreateVPRequestObject) (CreateVPResponseObject, error) + // Load a VerifiableCredential into the holders wallet. + // (POST /internal/vcr/v2/holder/{did}/vc) + LoadVC(ctx context.Context, request LoadVCRequestObject) (LoadVCResponseObject, error) // Issues a new Verifiable Credential // (POST /internal/vcr/v2/issuer/vc) IssueVC(ctx context.Context, request IssueVCRequestObject) (IssueVCResponseObject, error) @@ -3021,6 +3251,37 @@ func (sh *strictHandler) CreateVP(ctx echo.Context) error { return nil } +// LoadVC operation middleware +func (sh *strictHandler) LoadVC(ctx echo.Context, did string) error { + var request LoadVCRequestObject + + request.Did = did + + var body LoadVCJSONRequestBody + if err := ctx.Bind(&body); err != nil { + return err + } + request.Body = &body + + handler := func(ctx echo.Context, request interface{}) (interface{}, error) { + return sh.ssi.LoadVC(ctx.Request().Context(), request.(LoadVCRequestObject)) + } + for _, middleware := range sh.middlewares { + handler = middleware(handler, "LoadVC") + } + + response, err := handler(ctx, request) + + if err != nil { + return err + } else if validResponse, ok := response.(LoadVCResponseObject); ok { + return validResponse.VisitLoadVCResponse(ctx.Response()) + } else if response != nil { + return fmt.Errorf("unexpected response type: %T", response) + } + return nil +} + // IssueVC operation middleware func (sh *strictHandler) IssueVC(ctx echo.Context) error { var request IssueVCRequestObject