diff --git a/auth/oauth/openid.go b/auth/oauth/openid.go index f657cc0f5a..852ecae90c 100644 --- a/auth/oauth/openid.go +++ b/auth/oauth/openid.go @@ -1,3 +1,21 @@ +/* + * Copyright (C) 2023 Nuts community + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + package oauth // algValuesSupported contains a list of supported cipher suites for jwt_vc_json & jwt_vp_json presentation formats diff --git a/cmd/root.go b/cmd/root.go index c9d1d9eda2..e33ce818fc 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -44,6 +44,7 @@ import ( didmanAPI "github.com/nuts-foundation/nuts-node/didman/api/v1" didmanCmd "github.com/nuts-foundation/nuts-node/didman/cmd" "github.com/nuts-foundation/nuts-node/discovery" + discoveryAPI "github.com/nuts-foundation/nuts-node/discovery/api/v1" discoveryCmd "github.com/nuts-foundation/nuts-node/discovery/cmd" "github.com/nuts-foundation/nuts-node/events" eventsCmd "github.com/nuts-foundation/nuts-node/events/cmd" @@ -221,6 +222,7 @@ func CreateSystem(shutdownCallback context.CancelFunc) *core.System { system.RegisterRoutes(authIAMAPI.New(authInstance, credentialInstance, vdrInstance, storageInstance)) system.RegisterRoutes(&authMeansAPI.Wrapper{Auth: authInstance}) system.RegisterRoutes(&didmanAPI.Wrapper{Didman: didmanInstance}) + system.RegisterRoutes(&discoveryAPI.Wrapper{Server: discoveryInstance}) // Register engines // without dependencies diff --git a/codegen/configs/discovery_v1.yaml b/codegen/configs/discovery_v1.yaml new file mode 100644 index 0000000000..ffa5d4adbb --- /dev/null +++ b/codegen/configs/discovery_v1.yaml @@ -0,0 +1,10 @@ +package: v1 +generate: + echo-server: true + client: true + models: true + strict-server: true +output-options: + skip-prune: true + exclude-schemas: + - VerifiablePresentation diff --git a/discovery/api/v1/generated.go b/discovery/api/v1/generated.go new file mode 100644 index 0000000000..34076c2ab2 --- /dev/null +++ b/discovery/api/v1/generated.go @@ -0,0 +1,1041 @@ +// Package v1 provides primitives to interact with the openapi HTTP API. +// +// Code generated by github.com/deepmap/oapi-codegen/v2 version v2.0.0 DO NOT EDIT. +package v1 + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "io" + "net/http" + "net/url" + "strings" + + "github.com/labstack/echo/v4" + "github.com/oapi-codegen/runtime" + strictecho "github.com/oapi-codegen/runtime/strictmiddleware/echo" +) + +const ( + JwtBearerAuthScopes = "jwtBearerAuth.Scopes" +) + +// SearchResult defines model for SearchResult. +type SearchResult struct { + // Fields Input descriptor IDs and their mapped values that from the Verifiable Credential. + Fields map[string]string `json:"fields"` + + // Id The ID of the Verifiable Presentation. + Id string `json:"id"` + + // Vp Verifiable Presentation + Vp VerifiablePresentation `json:"vp"` +} + +// GetPresentationsParams defines parameters for GetPresentations. +type GetPresentationsParams struct { + Tag *string `form:"tag,omitempty" json:"tag,omitempty"` +} + +// SearchPresentationsParams defines parameters for SearchPresentations. +type SearchPresentationsParams struct { + Query map[string]string `form:"query" json:"query"` +} + +// RegisterPresentationJSONRequestBody defines body for RegisterPresentation for application/json ContentType. +type RegisterPresentationJSONRequestBody = VerifiablePresentation + +// RequestEditorFn is the function signature for the RequestEditor callback function +type RequestEditorFn func(ctx context.Context, req *http.Request) error + +// Doer performs HTTP requests. +// +// The standard http.Client implements this interface. +type HttpRequestDoer interface { + Do(req *http.Request) (*http.Response, error) +} + +// Client which conforms to the OpenAPI3 specification for this service. +type Client struct { + // The endpoint of the server conforming to this interface, with scheme, + // https://api.deepmap.com for example. This can contain a path relative + // to the server, such as https://api.deepmap.com/dev-test, and all the + // paths in the swagger spec will be appended to the server. + Server string + + // Doer for performing requests, typically a *http.Client with any + // customized settings, such as certificate chains. + Client HttpRequestDoer + + // A list of callbacks for modifying requests which are generated before sending over + // the network. + RequestEditors []RequestEditorFn +} + +// ClientOption allows setting custom parameters during construction +type ClientOption func(*Client) error + +// Creates a new Client, with reasonable defaults +func NewClient(server string, opts ...ClientOption) (*Client, error) { + // create a client with sane default values + client := Client{ + Server: server, + } + // mutate client and add all optional params + for _, o := range opts { + if err := o(&client); err != nil { + return nil, err + } + } + // ensure the server URL always has a trailing slash + if !strings.HasSuffix(client.Server, "/") { + client.Server += "/" + } + // create httpClient, if not already present + if client.Client == nil { + client.Client = &http.Client{} + } + return &client, nil +} + +// WithHTTPClient allows overriding the default Doer, which is +// automatically created using http.Client. This is useful for tests. +func WithHTTPClient(doer HttpRequestDoer) ClientOption { + return func(c *Client) error { + c.Client = doer + return nil + } +} + +// WithRequestEditorFn allows setting up a callback function, which will be +// called right before sending the request. This can be used to mutate the request. +func WithRequestEditorFn(fn RequestEditorFn) ClientOption { + return func(c *Client) error { + c.RequestEditors = append(c.RequestEditors, fn) + return nil + } +} + +// The interface specification for the client above. +type ClientInterface interface { + // GetPresentations request + GetPresentations(ctx context.Context, serviceID string, params *GetPresentationsParams, reqEditors ...RequestEditorFn) (*http.Response, error) + + // RegisterPresentationWithBody request with any body + RegisterPresentationWithBody(ctx context.Context, serviceID string, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) + + RegisterPresentation(ctx context.Context, serviceID string, body RegisterPresentationJSONRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error) + + // SearchPresentations request + SearchPresentations(ctx context.Context, serviceID string, params *SearchPresentationsParams, reqEditors ...RequestEditorFn) (*http.Response, error) +} + +func (c *Client) GetPresentations(ctx context.Context, serviceID string, params *GetPresentationsParams, reqEditors ...RequestEditorFn) (*http.Response, error) { + req, err := NewGetPresentationsRequest(c.Server, serviceID, params) + 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) RegisterPresentationWithBody(ctx context.Context, serviceID string, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) { + req, err := NewRegisterPresentationRequestWithBody(c.Server, serviceID, 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) RegisterPresentation(ctx context.Context, serviceID string, body RegisterPresentationJSONRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error) { + req, err := NewRegisterPresentationRequest(c.Server, serviceID, 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) SearchPresentations(ctx context.Context, serviceID string, params *SearchPresentationsParams, reqEditors ...RequestEditorFn) (*http.Response, error) { + req, err := NewSearchPresentationsRequest(c.Server, serviceID, params) + 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) +} + +// NewGetPresentationsRequest generates requests for GetPresentations +func NewGetPresentationsRequest(server string, serviceID string, params *GetPresentationsParams) (*http.Request, error) { + var err error + + var pathParam0 string + + pathParam0, err = runtime.StyleParamWithLocation("simple", false, "serviceID", runtime.ParamLocationPath, serviceID) + if err != nil { + return nil, err + } + + serverURL, err := url.Parse(server) + if err != nil { + return nil, err + } + + operationPath := fmt.Sprintf("/discovery/%s", pathParam0) + if operationPath[0] == '/' { + operationPath = "." + operationPath + } + + queryURL, err := serverURL.Parse(operationPath) + if err != nil { + return nil, err + } + + if params != nil { + queryValues := queryURL.Query() + + if params.Tag != nil { + + if queryFrag, err := runtime.StyleParamWithLocation("form", true, "tag", runtime.ParamLocationQuery, *params.Tag); err != nil { + return nil, err + } else if parsed, err := url.ParseQuery(queryFrag); err != nil { + return nil, err + } else { + for k, v := range parsed { + for _, v2 := range v { + queryValues.Add(k, v2) + } + } + } + + } + + queryURL.RawQuery = queryValues.Encode() + } + + req, err := http.NewRequest("GET", queryURL.String(), nil) + if err != nil { + return nil, err + } + + return req, nil +} + +// NewRegisterPresentationRequest calls the generic RegisterPresentation builder with application/json body +func NewRegisterPresentationRequest(server string, serviceID string, body RegisterPresentationJSONRequestBody) (*http.Request, error) { + var bodyReader io.Reader + buf, err := json.Marshal(body) + if err != nil { + return nil, err + } + bodyReader = bytes.NewReader(buf) + return NewRegisterPresentationRequestWithBody(server, serviceID, "application/json", bodyReader) +} + +// NewRegisterPresentationRequestWithBody generates requests for RegisterPresentation with any type of body +func NewRegisterPresentationRequestWithBody(server string, serviceID string, contentType string, body io.Reader) (*http.Request, error) { + var err error + + var pathParam0 string + + pathParam0, err = runtime.StyleParamWithLocation("simple", false, "serviceID", runtime.ParamLocationPath, serviceID) + if err != nil { + return nil, err + } + + serverURL, err := url.Parse(server) + if err != nil { + return nil, err + } + + operationPath := fmt.Sprintf("/discovery/%s", 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 +} + +// NewSearchPresentationsRequest generates requests for SearchPresentations +func NewSearchPresentationsRequest(server string, serviceID string, params *SearchPresentationsParams) (*http.Request, error) { + var err error + + var pathParam0 string + + pathParam0, err = runtime.StyleParamWithLocation("simple", false, "serviceID", runtime.ParamLocationPath, serviceID) + if err != nil { + return nil, err + } + + serverURL, err := url.Parse(server) + if err != nil { + return nil, err + } + + operationPath := fmt.Sprintf("/discovery/%s/search", pathParam0) + if operationPath[0] == '/' { + operationPath = "." + operationPath + } + + queryURL, err := serverURL.Parse(operationPath) + if err != nil { + return nil, err + } + + if params != nil { + queryValues := queryURL.Query() + + if queryFrag, err := runtime.StyleParamWithLocation("form", true, "query", runtime.ParamLocationQuery, params.Query); err != nil { + return nil, err + } else if parsed, err := url.ParseQuery(queryFrag); err != nil { + return nil, err + } else { + for k, v := range parsed { + for _, v2 := range v { + queryValues.Add(k, v2) + } + } + } + + queryURL.RawQuery = queryValues.Encode() + } + + req, err := http.NewRequest("GET", queryURL.String(), nil) + if err != nil { + return nil, err + } + + return req, nil +} + +func (c *Client) applyEditors(ctx context.Context, req *http.Request, additionalEditors []RequestEditorFn) error { + for _, r := range c.RequestEditors { + if err := r(ctx, req); err != nil { + return err + } + } + for _, r := range additionalEditors { + if err := r(ctx, req); err != nil { + return err + } + } + return nil +} + +// ClientWithResponses builds on ClientInterface to offer response payloads +type ClientWithResponses struct { + ClientInterface +} + +// NewClientWithResponses creates a new ClientWithResponses, which wraps +// Client with return type handling +func NewClientWithResponses(server string, opts ...ClientOption) (*ClientWithResponses, error) { + client, err := NewClient(server, opts...) + if err != nil { + return nil, err + } + return &ClientWithResponses{client}, nil +} + +// WithBaseURL overrides the baseURL. +func WithBaseURL(baseURL string) ClientOption { + return func(c *Client) error { + newBaseURL, err := url.Parse(baseURL) + if err != nil { + return err + } + c.Server = newBaseURL.String() + return nil + } +} + +// ClientWithResponsesInterface is the interface specification for the client with responses above. +type ClientWithResponsesInterface interface { + // GetPresentationsWithResponse request + GetPresentationsWithResponse(ctx context.Context, serviceID string, params *GetPresentationsParams, reqEditors ...RequestEditorFn) (*GetPresentationsResponse, error) + + // RegisterPresentationWithBodyWithResponse request with any body + RegisterPresentationWithBodyWithResponse(ctx context.Context, serviceID string, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*RegisterPresentationResponse, error) + + RegisterPresentationWithResponse(ctx context.Context, serviceID string, body RegisterPresentationJSONRequestBody, reqEditors ...RequestEditorFn) (*RegisterPresentationResponse, error) + + // SearchPresentationsWithResponse request + SearchPresentationsWithResponse(ctx context.Context, serviceID string, params *SearchPresentationsParams, reqEditors ...RequestEditorFn) (*SearchPresentationsResponse, error) +} + +type GetPresentationsResponse struct { + Body []byte + HTTPResponse *http.Response + JSON200 *struct { + Entries []VerifiablePresentation `json:"entries"` + Tag string `json:"tag"` + } + 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 GetPresentationsResponse) Status() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Status + } + return http.StatusText(0) +} + +// StatusCode returns HTTPResponse.StatusCode +func (r GetPresentationsResponse) StatusCode() int { + if r.HTTPResponse != nil { + return r.HTTPResponse.StatusCode + } + return 0 +} + +type RegisterPresentationResponse struct { + Body []byte + HTTPResponse *http.Response + ApplicationproblemJSON400 *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"` + } + 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 RegisterPresentationResponse) Status() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Status + } + return http.StatusText(0) +} + +// StatusCode returns HTTPResponse.StatusCode +func (r RegisterPresentationResponse) StatusCode() int { + if r.HTTPResponse != nil { + return r.HTTPResponse.StatusCode + } + return 0 +} + +type SearchPresentationsResponse struct { + Body []byte + HTTPResponse *http.Response + JSON200 *[]SearchResult + 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 SearchPresentationsResponse) Status() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Status + } + return http.StatusText(0) +} + +// StatusCode returns HTTPResponse.StatusCode +func (r SearchPresentationsResponse) StatusCode() int { + if r.HTTPResponse != nil { + return r.HTTPResponse.StatusCode + } + return 0 +} + +// GetPresentationsWithResponse request returning *GetPresentationsResponse +func (c *ClientWithResponses) GetPresentationsWithResponse(ctx context.Context, serviceID string, params *GetPresentationsParams, reqEditors ...RequestEditorFn) (*GetPresentationsResponse, error) { + rsp, err := c.GetPresentations(ctx, serviceID, params, reqEditors...) + if err != nil { + return nil, err + } + return ParseGetPresentationsResponse(rsp) +} + +// RegisterPresentationWithBodyWithResponse request with arbitrary body returning *RegisterPresentationResponse +func (c *ClientWithResponses) RegisterPresentationWithBodyWithResponse(ctx context.Context, serviceID string, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*RegisterPresentationResponse, error) { + rsp, err := c.RegisterPresentationWithBody(ctx, serviceID, contentType, body, reqEditors...) + if err != nil { + return nil, err + } + return ParseRegisterPresentationResponse(rsp) +} + +func (c *ClientWithResponses) RegisterPresentationWithResponse(ctx context.Context, serviceID string, body RegisterPresentationJSONRequestBody, reqEditors ...RequestEditorFn) (*RegisterPresentationResponse, error) { + rsp, err := c.RegisterPresentation(ctx, serviceID, body, reqEditors...) + if err != nil { + return nil, err + } + return ParseRegisterPresentationResponse(rsp) +} + +// SearchPresentationsWithResponse request returning *SearchPresentationsResponse +func (c *ClientWithResponses) SearchPresentationsWithResponse(ctx context.Context, serviceID string, params *SearchPresentationsParams, reqEditors ...RequestEditorFn) (*SearchPresentationsResponse, error) { + rsp, err := c.SearchPresentations(ctx, serviceID, params, reqEditors...) + if err != nil { + return nil, err + } + return ParseSearchPresentationsResponse(rsp) +} + +// ParseGetPresentationsResponse parses an HTTP response from a GetPresentationsWithResponse call +func ParseGetPresentationsResponse(rsp *http.Response) (*GetPresentationsResponse, error) { + bodyBytes, err := io.ReadAll(rsp.Body) + defer func() { _ = rsp.Body.Close() }() + if err != nil { + return nil, err + } + + response := &GetPresentationsResponse{ + Body: bodyBytes, + HTTPResponse: rsp, + } + + switch { + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 200: + var dest struct { + Entries []VerifiablePresentation `json:"entries"` + Tag string `json:"tag"` + } + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON200 = &dest + + 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 +} + +// ParseRegisterPresentationResponse parses an HTTP response from a RegisterPresentationWithResponse call +func ParseRegisterPresentationResponse(rsp *http.Response) (*RegisterPresentationResponse, error) { + bodyBytes, err := io.ReadAll(rsp.Body) + defer func() { _ = rsp.Body.Close() }() + if err != nil { + return nil, err + } + + response := &RegisterPresentationResponse{ + Body: bodyBytes, + HTTPResponse: rsp, + } + + switch { + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 400: + 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.ApplicationproblemJSON400 = &dest + + 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 +} + +// ParseSearchPresentationsResponse parses an HTTP response from a SearchPresentationsWithResponse call +func ParseSearchPresentationsResponse(rsp *http.Response) (*SearchPresentationsResponse, error) { + bodyBytes, err := io.ReadAll(rsp.Body) + defer func() { _ = rsp.Body.Close() }() + if err != nil { + return nil, err + } + + response := &SearchPresentationsResponse{ + Body: bodyBytes, + HTTPResponse: rsp, + } + + switch { + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 200: + var dest []SearchResult + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON200 = &dest + + 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 +} + +// ServerInterface represents all server handlers. +type ServerInterface interface { + // Retrieves the presentations of a discovery service. + // (GET /discovery/{serviceID}) + GetPresentations(ctx echo.Context, serviceID string, params GetPresentationsParams) error + // Register a presentation on the discovery service. + // (POST /discovery/{serviceID}) + RegisterPresentation(ctx echo.Context, serviceID string) error + // Searches for presentations registered on the discovery service. + // (GET /discovery/{serviceID}/search) + SearchPresentations(ctx echo.Context, serviceID string, params SearchPresentationsParams) error +} + +// ServerInterfaceWrapper converts echo contexts to parameters. +type ServerInterfaceWrapper struct { + Handler ServerInterface +} + +// GetPresentations converts echo context to params. +func (w *ServerInterfaceWrapper) GetPresentations(ctx echo.Context) error { + var err error + // ------------- Path parameter "serviceID" ------------- + var serviceID string + + err = runtime.BindStyledParameterWithLocation("simple", false, "serviceID", runtime.ParamLocationPath, ctx.Param("serviceID"), &serviceID) + if err != nil { + return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Invalid format for parameter serviceID: %s", err)) + } + + ctx.Set(JwtBearerAuthScopes, []string{}) + + // Parameter object where we will unmarshal all parameters from the context + var params GetPresentationsParams + // ------------- Optional query parameter "tag" ------------- + + err = runtime.BindQueryParameter("form", true, false, "tag", ctx.QueryParams(), ¶ms.Tag) + if err != nil { + return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Invalid format for parameter tag: %s", err)) + } + + // Invoke the callback with all the unmarshaled arguments + err = w.Handler.GetPresentations(ctx, serviceID, params) + return err +} + +// RegisterPresentation converts echo context to params. +func (w *ServerInterfaceWrapper) RegisterPresentation(ctx echo.Context) error { + var err error + // ------------- Path parameter "serviceID" ------------- + var serviceID string + + err = runtime.BindStyledParameterWithLocation("simple", false, "serviceID", runtime.ParamLocationPath, ctx.Param("serviceID"), &serviceID) + if err != nil { + return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Invalid format for parameter serviceID: %s", err)) + } + + ctx.Set(JwtBearerAuthScopes, []string{}) + + // Invoke the callback with all the unmarshaled arguments + err = w.Handler.RegisterPresentation(ctx, serviceID) + return err +} + +// SearchPresentations converts echo context to params. +func (w *ServerInterfaceWrapper) SearchPresentations(ctx echo.Context) error { + var err error + // ------------- Path parameter "serviceID" ------------- + var serviceID string + + err = runtime.BindStyledParameterWithLocation("simple", false, "serviceID", runtime.ParamLocationPath, ctx.Param("serviceID"), &serviceID) + if err != nil { + return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Invalid format for parameter serviceID: %s", err)) + } + + ctx.Set(JwtBearerAuthScopes, []string{}) + + // Parameter object where we will unmarshal all parameters from the context + var params SearchPresentationsParams + // ------------- Required query parameter "query" ------------- + + err = runtime.BindQueryParameter("form", true, true, "query", ctx.QueryParams(), ¶ms.Query) + if err != nil { + return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Invalid format for parameter query: %s", err)) + } + + // Invoke the callback with all the unmarshaled arguments + err = w.Handler.SearchPresentations(ctx, serviceID, params) + return err +} + +// This is a simple interface which specifies echo.Route addition functions which +// are present on both echo.Echo and echo.Group, since we want to allow using +// either of them for path registration +type EchoRouter interface { + CONNECT(path string, h echo.HandlerFunc, m ...echo.MiddlewareFunc) *echo.Route + DELETE(path string, h echo.HandlerFunc, m ...echo.MiddlewareFunc) *echo.Route + GET(path string, h echo.HandlerFunc, m ...echo.MiddlewareFunc) *echo.Route + HEAD(path string, h echo.HandlerFunc, m ...echo.MiddlewareFunc) *echo.Route + OPTIONS(path string, h echo.HandlerFunc, m ...echo.MiddlewareFunc) *echo.Route + PATCH(path string, h echo.HandlerFunc, m ...echo.MiddlewareFunc) *echo.Route + POST(path string, h echo.HandlerFunc, m ...echo.MiddlewareFunc) *echo.Route + PUT(path string, h echo.HandlerFunc, m ...echo.MiddlewareFunc) *echo.Route + TRACE(path string, h echo.HandlerFunc, m ...echo.MiddlewareFunc) *echo.Route +} + +// RegisterHandlers adds each server route to the EchoRouter. +func RegisterHandlers(router EchoRouter, si ServerInterface) { + RegisterHandlersWithBaseURL(router, si, "") +} + +// Registers handlers, and prepends BaseURL to the paths, so that the paths +// can be served under a prefix. +func RegisterHandlersWithBaseURL(router EchoRouter, si ServerInterface, baseURL string) { + + wrapper := ServerInterfaceWrapper{ + Handler: si, + } + + router.GET(baseURL+"/discovery/:serviceID", wrapper.GetPresentations) + router.POST(baseURL+"/discovery/:serviceID", wrapper.RegisterPresentation) + router.GET(baseURL+"/discovery/:serviceID/search", wrapper.SearchPresentations) + +} + +type GetPresentationsRequestObject struct { + ServiceID string `json:"serviceID"` + Params GetPresentationsParams +} + +type GetPresentationsResponseObject interface { + VisitGetPresentationsResponse(w http.ResponseWriter) error +} + +type GetPresentations200JSONResponse struct { + Entries []VerifiablePresentation `json:"entries"` + Tag string `json:"tag"` +} + +func (response GetPresentations200JSONResponse) VisitGetPresentationsResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(200) + + return json.NewEncoder(w).Encode(response) +} + +type GetPresentationsdefaultApplicationProblemPlusJSONResponse 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 GetPresentationsdefaultApplicationProblemPlusJSONResponse) VisitGetPresentationsResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/problem+json") + w.WriteHeader(response.StatusCode) + + return json.NewEncoder(w).Encode(response.Body) +} + +type RegisterPresentationRequestObject struct { + ServiceID string `json:"serviceID"` + Body *RegisterPresentationJSONRequestBody +} + +type RegisterPresentationResponseObject interface { + VisitRegisterPresentationResponse(w http.ResponseWriter) error +} + +type RegisterPresentation201Response struct { +} + +func (response RegisterPresentation201Response) VisitRegisterPresentationResponse(w http.ResponseWriter) error { + w.WriteHeader(201) + return nil +} + +type RegisterPresentation400ApplicationProblemPlusJSONResponse 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"` +} + +func (response RegisterPresentation400ApplicationProblemPlusJSONResponse) VisitRegisterPresentationResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/problem+json") + w.WriteHeader(400) + + return json.NewEncoder(w).Encode(response) +} + +type RegisterPresentationdefaultApplicationProblemPlusJSONResponse 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 RegisterPresentationdefaultApplicationProblemPlusJSONResponse) VisitRegisterPresentationResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/problem+json") + w.WriteHeader(response.StatusCode) + + return json.NewEncoder(w).Encode(response.Body) +} + +type SearchPresentationsRequestObject struct { + ServiceID string `json:"serviceID"` + Params SearchPresentationsParams +} + +type SearchPresentationsResponseObject interface { + VisitSearchPresentationsResponse(w http.ResponseWriter) error +} + +type SearchPresentations200JSONResponse []SearchResult + +func (response SearchPresentations200JSONResponse) VisitSearchPresentationsResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(200) + + return json.NewEncoder(w).Encode(response) +} + +type SearchPresentationsdefaultApplicationProblemPlusJSONResponse 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 SearchPresentationsdefaultApplicationProblemPlusJSONResponse) VisitSearchPresentationsResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/problem+json") + w.WriteHeader(response.StatusCode) + + return json.NewEncoder(w).Encode(response.Body) +} + +// StrictServerInterface represents all server handlers. +type StrictServerInterface interface { + // Retrieves the presentations of a discovery service. + // (GET /discovery/{serviceID}) + GetPresentations(ctx context.Context, request GetPresentationsRequestObject) (GetPresentationsResponseObject, error) + // Register a presentation on the discovery service. + // (POST /discovery/{serviceID}) + RegisterPresentation(ctx context.Context, request RegisterPresentationRequestObject) (RegisterPresentationResponseObject, error) + // Searches for presentations registered on the discovery service. + // (GET /discovery/{serviceID}/search) + SearchPresentations(ctx context.Context, request SearchPresentationsRequestObject) (SearchPresentationsResponseObject, error) +} + +type StrictHandlerFunc = strictecho.StrictEchoHandlerFunc +type StrictMiddlewareFunc = strictecho.StrictEchoMiddlewareFunc + +func NewStrictHandler(ssi StrictServerInterface, middlewares []StrictMiddlewareFunc) ServerInterface { + return &strictHandler{ssi: ssi, middlewares: middlewares} +} + +type strictHandler struct { + ssi StrictServerInterface + middlewares []StrictMiddlewareFunc +} + +// GetPresentations operation middleware +func (sh *strictHandler) GetPresentations(ctx echo.Context, serviceID string, params GetPresentationsParams) error { + var request GetPresentationsRequestObject + + request.ServiceID = serviceID + request.Params = params + + handler := func(ctx echo.Context, request interface{}) (interface{}, error) { + return sh.ssi.GetPresentations(ctx.Request().Context(), request.(GetPresentationsRequestObject)) + } + for _, middleware := range sh.middlewares { + handler = middleware(handler, "GetPresentations") + } + + response, err := handler(ctx, request) + + if err != nil { + return err + } else if validResponse, ok := response.(GetPresentationsResponseObject); ok { + return validResponse.VisitGetPresentationsResponse(ctx.Response()) + } else if response != nil { + return fmt.Errorf("unexpected response type: %T", response) + } + return nil +} + +// RegisterPresentation operation middleware +func (sh *strictHandler) RegisterPresentation(ctx echo.Context, serviceID string) error { + var request RegisterPresentationRequestObject + + request.ServiceID = serviceID + + var body RegisterPresentationJSONRequestBody + if err := ctx.Bind(&body); err != nil { + return err + } + request.Body = &body + + handler := func(ctx echo.Context, request interface{}) (interface{}, error) { + return sh.ssi.RegisterPresentation(ctx.Request().Context(), request.(RegisterPresentationRequestObject)) + } + for _, middleware := range sh.middlewares { + handler = middleware(handler, "RegisterPresentation") + } + + response, err := handler(ctx, request) + + if err != nil { + return err + } else if validResponse, ok := response.(RegisterPresentationResponseObject); ok { + return validResponse.VisitRegisterPresentationResponse(ctx.Response()) + } else if response != nil { + return fmt.Errorf("unexpected response type: %T", response) + } + return nil +} + +// SearchPresentations operation middleware +func (sh *strictHandler) SearchPresentations(ctx echo.Context, serviceID string, params SearchPresentationsParams) error { + var request SearchPresentationsRequestObject + + request.ServiceID = serviceID + request.Params = params + + handler := func(ctx echo.Context, request interface{}) (interface{}, error) { + return sh.ssi.SearchPresentations(ctx.Request().Context(), request.(SearchPresentationsRequestObject)) + } + for _, middleware := range sh.middlewares { + handler = middleware(handler, "SearchPresentations") + } + + response, err := handler(ctx, request) + + if err != nil { + return err + } else if validResponse, ok := response.(SearchPresentationsResponseObject); ok { + return validResponse.VisitSearchPresentationsResponse(ctx.Response()) + } else if response != nil { + return fmt.Errorf("unexpected response type: %T", response) + } + return nil +} diff --git a/discovery/api/v1/types.go b/discovery/api/v1/types.go new file mode 100644 index 0000000000..7a9ff02004 --- /dev/null +++ b/discovery/api/v1/types.go @@ -0,0 +1,24 @@ +/* + * Copyright (C) 2023 Nuts community + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package v1 + +import "github.com/nuts-foundation/go-did/vc" + +// VerifiablePresentation is a type alias for the VerifiablePresentation from the go-did library. +type VerifiablePresentation = vc.VerifiablePresentation diff --git a/discovery/api/v1/wapper.go b/discovery/api/v1/wapper.go new file mode 100644 index 0000000000..c7c9e94976 --- /dev/null +++ b/discovery/api/v1/wapper.go @@ -0,0 +1,101 @@ +/* + * Copyright (C) 2023 Nuts community + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package v1 + +import ( + "context" + "errors" + "github.com/labstack/echo/v4" + "github.com/nuts-foundation/nuts-node/core" + "github.com/nuts-foundation/nuts-node/discovery" + "net/http" +) + +var _ StrictServerInterface = (*Wrapper)(nil) +var _ core.ErrorStatusCodeResolver = (*Wrapper)(nil) + +type Wrapper struct { + Server discovery.Server + Client discovery.Client +} + +func (w *Wrapper) ResolveStatusCode(err error) int { + // todo + switch { + case errors.Is(err, discovery.ErrServerModeDisabled): + return http.StatusBadRequest + case errors.Is(err, discovery.ErrInvalidPresentation): + return http.StatusBadRequest + default: + return http.StatusInternalServerError + } +} + +func (w *Wrapper) Routes(router core.EchoRouter) { + RegisterHandlers(router, NewStrictHandler(w, []StrictMiddlewareFunc{ + func(f StrictHandlerFunc, operationID string) StrictHandlerFunc { + return func(ctx echo.Context, request interface{}) (response interface{}, err error) { + ctx.Set(core.OperationIDContextKey, operationID) + ctx.Set(core.ModuleNameContextKey, discovery.ModuleName) + ctx.Set(core.StatusCodeResolverContextKey, w) + return f(ctx, request) + } + }, + })) +} + +func (w *Wrapper) GetPresentations(_ context.Context, request GetPresentationsRequestObject) (GetPresentationsResponseObject, error) { + var tag *discovery.Tag + if request.Params.Tag != nil { + tag = new(discovery.Tag) + *tag = discovery.Tag(*request.Params.Tag) + } + presentations, newTag, err := w.Server.Get(request.ServiceID, tag) + if err != nil { + return nil, err + } + return GetPresentations200JSONResponse{ + Entries: presentations, + Tag: string(*newTag), + }, nil +} + +func (w *Wrapper) RegisterPresentation(_ context.Context, request RegisterPresentationRequestObject) (RegisterPresentationResponseObject, error) { + err := w.Server.Add(request.ServiceID, *request.Body) + if err != nil { + return nil, err + } + return RegisterPresentation201Response{}, nil +} + +func (w *Wrapper) SearchPresentations(_ context.Context, request SearchPresentationsRequestObject) (SearchPresentationsResponseObject, error) { + searchResults, err := w.Client.Search(request.ServiceID, request.Params.Query) + if err != nil { + return nil, err + } + var result []SearchResult + for _, searchResult := range searchResults { + result = append(result, SearchResult{ + Vp: searchResult.VP, + Id: searchResult.VP.ID.String(), + Fields: searchResult.Fields, + }) + } + return SearchPresentations200JSONResponse(result), nil +} diff --git a/discovery/api/v1/wapper_test.go b/discovery/api/v1/wapper_test.go new file mode 100644 index 0000000000..6c70b0707e --- /dev/null +++ b/discovery/api/v1/wapper_test.go @@ -0,0 +1,158 @@ +/* + * Copyright (C) 2023 Nuts community + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package v1 + +import ( + ssi "github.com/nuts-foundation/go-did" + "github.com/nuts-foundation/go-did/vc" + "github.com/nuts-foundation/nuts-node/discovery" + "github.com/nuts-foundation/nuts-node/vcr/credential" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "go.uber.org/mock/gomock" + "testing" +) + +const serviceID = "wonderland" + +func TestWrapper_GetPresentations(t *testing.T) { + t.Run("no tag", func(t *testing.T) { + latestTag := discovery.Tag("latest") + test := newMockContext(t) + presentations := []vc.VerifiablePresentation{} + test.server.EXPECT().Get(serviceID, nil).Return(presentations, &latestTag, nil) + + response, err := test.wrapper.GetPresentations(nil, GetPresentationsRequestObject{ServiceID: serviceID}) + + require.NoError(t, err) + require.IsType(t, GetPresentations200JSONResponse{}, response) + assert.Equal(t, latestTag, discovery.Tag(response.(GetPresentations200JSONResponse).Tag)) + assert.Equal(t, presentations, response.(GetPresentations200JSONResponse).Entries) + }) + t.Run("with tag", func(t *testing.T) { + givenTag := discovery.Tag("given") + latestTag := discovery.Tag("latest") + test := newMockContext(t) + presentations := []vc.VerifiablePresentation{} + test.server.EXPECT().Get(serviceID, &givenTag).Return(presentations, &latestTag, nil) + + response, err := test.wrapper.GetPresentations(nil, GetPresentationsRequestObject{ + ServiceID: serviceID, + Params: GetPresentationsParams{ + Tag: (*string)(&givenTag), + }, + }) + + require.NoError(t, err) + require.IsType(t, GetPresentations200JSONResponse{}, response) + assert.Equal(t, latestTag, discovery.Tag(response.(GetPresentations200JSONResponse).Tag)) + assert.Equal(t, presentations, response.(GetPresentations200JSONResponse).Entries) + }) +} + +func TestWrapper_RegisterPresentation(t *testing.T) { + t.Run("ok", func(t *testing.T) { + test := newMockContext(t) + presentation := vc.VerifiablePresentation{} + test.server.EXPECT().Add(serviceID, presentation).Return(nil) + + response, err := test.wrapper.RegisterPresentation(nil, RegisterPresentationRequestObject{ + ServiceID: serviceID, + Body: &presentation, + }) + + assert.NoError(t, err) + assert.IsType(t, RegisterPresentation201Response{}, response) + }) + t.Run("error", func(t *testing.T) { + test := newMockContext(t) + presentation := vc.VerifiablePresentation{} + test.server.EXPECT().Add(serviceID, presentation).Return(discovery.ErrInvalidPresentation) + + _, err := test.wrapper.RegisterPresentation(nil, RegisterPresentationRequestObject{ + ServiceID: serviceID, + Body: &presentation, + }) + + assert.ErrorIs(t, err, discovery.ErrInvalidPresentation) + }) +} + +func TestWrapper_SearchPresentations(t *testing.T) { + query := map[string]string{ + "foo": "bar", + } + id, _ := ssi.ParseURI("did:nuts:foo#1") + vp := vc.VerifiablePresentation{ + ID: id, + VerifiableCredential: []vc.VerifiableCredential{credential.ValidNutsOrganizationCredential(t)}, + } + t.Run("ok", func(t *testing.T) { + test := newMockContext(t) + results := []discovery.SearchResult{ + { + VP: vp, + Fields: nil, + }, + } + test.client.EXPECT().Search(serviceID, query).Return(results, nil) + + response, err := test.wrapper.SearchPresentations(nil, SearchPresentationsRequestObject{ + ServiceID: serviceID, + Params: SearchPresentationsParams{Query: query}, + }) + + assert.NoError(t, err) + assert.IsType(t, SearchPresentations200JSONResponse{}, response) + actual := response.(SearchPresentations200JSONResponse) + require.Len(t, actual, 1) + assert.Equal(t, vp, actual[0].Vp) + assert.Equal(t, vp.ID.String(), actual[0].Id) + }) + t.Run("error", func(t *testing.T) { + test := newMockContext(t) + test.client.EXPECT().Search(serviceID, query).Return(nil, discovery.ErrServiceNotFound) + + _, err := test.wrapper.SearchPresentations(nil, SearchPresentationsRequestObject{ + ServiceID: serviceID, + Params: SearchPresentationsParams{Query: query}, + }) + + assert.ErrorIs(t, err, discovery.ErrServiceNotFound) + }) +} + +type mockContext struct { + ctrl *gomock.Controller + server *discovery.MockServer + client *discovery.MockClient + wrapper Wrapper +} + +func newMockContext(t *testing.T) mockContext { + ctrl := gomock.NewController(t) + server := discovery.NewMockServer(ctrl) + client := discovery.NewMockClient(ctrl) + return mockContext{ + ctrl: ctrl, + server: server, + client: client, + wrapper: Wrapper{Server: server, Client: client}, + } +} diff --git a/discovery/interface.go b/discovery/interface.go index d308eb91a3..4167a99cc7 100644 --- a/discovery/interface.go +++ b/discovery/interface.go @@ -26,8 +26,6 @@ import ( "strings" ) -// Tag is value that references a point in the list. -// It is used by clients to request new entries since their last query. // It is opaque for clients: they should not try to interpret it. // The server who issued the tag can interpret it as Lamport timestamp. type Tag string @@ -86,7 +84,7 @@ var ErrPresentationAlreadyExists = errors.New("presentation already exists") // Server defines the API for Discovery Servers. type Server interface { // Add registers a presentation on the given Discovery Service. - // If the presentation is not valid or it does not conform to the Service ServiceDefinition, it returns an error. + // If the presentation is not valid, or it does not conform to the Service ServiceDefinition, it returns an error. Add(serviceID string, presentation vc.VerifiablePresentation) error // Get retrieves the presentations for the given service, starting at the given timestamp. Get(serviceID string, startAt *Tag) ([]vc.VerifiablePresentation, *Tag, error) @@ -94,5 +92,10 @@ type Server interface { // Client defines the API for Discovery Clients. type Client interface { - Search(serviceID string, query map[string]string) ([]vc.VerifiablePresentation, error) + Search(serviceID string, query map[string]string) ([]SearchResult, error) +} + +type SearchResult struct { + VP vc.VerifiablePresentation + Fields map[string]string } diff --git a/discovery/mock.go b/discovery/mock.go index ade7b84915..9b1320c756 100644 --- a/discovery/mock.go +++ b/discovery/mock.go @@ -92,10 +92,10 @@ func (m *MockClient) EXPECT() *MockClientMockRecorder { } // Search mocks base method. -func (m *MockClient) Search(serviceID string, query map[string]string) ([]vc.VerifiablePresentation, error) { +func (m *MockClient) Search(serviceID string, query map[string]string) ([]SearchResult, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "Search", serviceID, query) - ret0, _ := ret[0].([]vc.VerifiablePresentation) + ret0, _ := ret[0].([]SearchResult) ret1, _ := ret[1].(error) return ret0, ret1 } diff --git a/discovery/module.go b/discovery/module.go index 3685e7025c..d17747d0e1 100644 --- a/discovery/module.go +++ b/discovery/module.go @@ -35,12 +35,29 @@ import ( const ModuleName = "Discovery" +// ErrServerModeDisabled is returned when a client invokes a Discovery Server (Add or Get) operation on the node, +// for a Discovery Service which it doesn't serve. var ErrServerModeDisabled = errors.New("node is not a discovery server for this service") +// ErrInvalidPresentation is returned when a client tries to register a Verifiable Presentation that is invalid. +var ErrInvalidPresentation = errors.New("presentation is invalid for registration") + +var ( + errUnsupportedPresentationFormat = errors.New("only JWT presentations are supported") + errPresentationWithoutID = errors.New("presentation does not have an ID") + errPresentationWithoutExpiration = errors.New("presentation does not have an expiration") + errPresentationValidityExceedsCredentials = errors.New("presentation is valid longer than the credential(s) it contains") + errPresentationDoesNotFulfillDefinition = errors.New("presentation does not fulfill Presentation ServiceDefinition") + errRetractionReferencesUnknownPresentation = errors.New("retraction presentation refers to a non-existing presentation") + errRetractionContainsCredentials = errors.New("retraction presentation must not contain credentials") + errInvalidRetractionJTIClaim = errors.New("invalid/missing 'retract_jti' claim for retraction presentation") +) + var _ core.Injectable = &Module{} var _ core.Runnable = &Module{} var _ core.Configurable = &Module{} var _ Server = &Module{} +var _ Client = &Module{} var retractionPresentationType = ssi.MustParseURI("RetractedVerifiablePresentation") @@ -107,6 +124,8 @@ func (m *Module) Config() interface{} { return &m.config } +// Add registers a presentation on the given Discovery Service. +// See interface.go for more information. func (m *Module) Add(serviceID string, presentation vc.VerifiablePresentation) error { // First, simple sanity checks definition, isServer := m.serverDefinitions[serviceID] @@ -114,10 +133,10 @@ func (m *Module) Add(serviceID string, presentation vc.VerifiablePresentation) e return ErrServerModeDisabled } if presentation.Format() != vc.JWTPresentationProofFormat { - return errors.New("only JWT presentations are supported") + return errors.Join(ErrInvalidPresentation, errUnsupportedPresentationFormat) } if presentation.ID == nil { - return errors.New("presentation does not have an ID") + return errors.Join(ErrInvalidPresentation, errPresentationWithoutID) } // Make sure the presentation is intended for this service if err := validateAudience(definition, presentation.JWT().Audience()); err != nil { @@ -125,11 +144,11 @@ func (m *Module) Add(serviceID string, presentation vc.VerifiablePresentation) e } expiration := presentation.JWT().Expiration() if expiration.IsZero() { - return errors.New("presentation does not have an expiration") + return errors.Join(ErrInvalidPresentation, errPresentationWithoutExpiration) } // VPs should not be valid for too long, as that would prevent the server from pruning them. if int(expiration.Sub(time.Now()).Seconds()) > definition.PresentationMaxValidity { - return fmt.Errorf("presentation is valid for too long (max %s)", time.Duration(definition.PresentationMaxValidity)*time.Second) + return errors.Join(ErrInvalidPresentation, fmt.Errorf("presentation is valid for too long (max %s)", time.Duration(definition.PresentationMaxValidity)*time.Second)) } // Check if the presentation already exists credentialSubjectID, err := credential.PresentationSigner(presentation) @@ -141,7 +160,7 @@ func (m *Module) Add(serviceID string, presentation vc.VerifiablePresentation) e return err } if exists { - return ErrPresentationAlreadyExists + return errors.Join(ErrInvalidPresentation, ErrPresentationAlreadyExists) } // Depending on the presentation type, we need to validate different properties before storing it. if presentation.IsType(retractionPresentationType) { @@ -150,12 +169,12 @@ func (m *Module) Add(serviceID string, presentation vc.VerifiablePresentation) e err = m.validateRegistration(definition, presentation) } if err != nil { - return err + return errors.Join(ErrInvalidPresentation, err) } // Check signature of presentation and contained credential(s) _, err = m.vcrInstance.Verifier().VerifyVP(presentation, true, true, nil) if err != nil { - return fmt.Errorf("presentation verification failed: %w", err) + return errors.Join(ErrInvalidPresentation, fmt.Errorf("presentation verification failed: %w", err)) } return m.store.add(definition.ID, presentation, nil) } @@ -165,7 +184,7 @@ func (m *Module) validateRegistration(definition ServiceDefinition, presentation expiration := presentation.JWT().Expiration() for _, cred := range presentation.VerifiableCredential { if cred.ExpirationDate != nil && expiration.After(*cred.ExpirationDate) { - return fmt.Errorf("presentation is valid longer than the credential(s) it contains") + return errPresentationValidityExceedsCredentials } } // VP must fulfill the PEX Presentation ServiceDefinition @@ -175,7 +194,7 @@ func (m *Module) validateRegistration(definition ServiceDefinition, presentation return err } if len(creds) != len(presentation.VerifiableCredential) { - return errors.New("presentation does not fulfill Presentation ServiceDefinition") + return errPresentationDoesNotFulfillDefinition } return nil } @@ -184,29 +203,28 @@ func (m *Module) validateRetraction(serviceID string, presentation vc.Verifiable // Presentation might be a retraction (deletion of an earlier credentialRecord) must contain no credentials, and refer to the VP being retracted by ID. // If those conditions aren't met, we don't need to register the retraction. if len(presentation.VerifiableCredential) > 0 { - return errors.New("retraction presentation must not contain credentials") + return errRetractionContainsCredentials } // Check that the retraction refers to an existing presentation. // If not, it might've already been removed due to expiry or superseded by a newer presentation. - var retractJTIString string - if retractJTIRaw, ok := presentation.JWT().Get("retract_jti"); !ok { - return errors.New("retraction presentation does not contain 'retract_jti' claim") - } else { - if retractJTIString, ok = retractJTIRaw.(string); !ok { - return errors.New("retraction presentation 'retract_jti' claim is not a string") - } + retractJTIRaw, _ := presentation.JWT().Get("retract_jti") + retractJTI, ok := retractJTIRaw.(string) + if !ok { + return errInvalidRetractionJTIClaim } signerDID, _ := credential.PresentationSigner(presentation) // checked before - exists, err := m.store.exists(serviceID, signerDID.String(), retractJTIString) + exists, err := m.store.exists(serviceID, signerDID.String(), retractJTI) if err != nil { return err } if !exists { - return errors.New("retraction presentation refers to a non-existing presentation") + return errRetractionReferencesUnknownPresentation } return nil } +// Get retrieves the presentations for the given service, starting at the given tag. +// See interface.go for more information. func (m *Module) Get(serviceID string, tag *Tag) ([]vc.VerifiablePresentation, *Tag, error) { if _, exists := m.serverDefinitions[serviceID]; !exists { return nil, nil, ErrServerModeDisabled @@ -241,6 +259,25 @@ func loadDefinitions(directory string) (map[string]ServiceDefinition, error) { return result, nil } +func (m *Module) Search(serviceID string, query map[string]string) ([]SearchResult, error) { + if _, exists := m.serverDefinitions[serviceID]; !exists { + return nil, ErrServerModeDisabled + } + matchingVPs, err := m.store.search(serviceID, query) + if err != nil { + return nil, err + } + var result []SearchResult + for _, matchingVP := range matchingVPs { + result = append(result, SearchResult{ + VP: matchingVP, + // TODO: TokenIntrospection is also missing map[InputDescriptorId]CredentialValue ? + //Fields: matchingVP.Fields, + }) + } + return result, nil +} + // validateAudience checks if the given audience of the presentation matches the service ID. func validateAudience(service ServiceDefinition, audience []string) error { for _, audienceID := range audience { diff --git a/discovery/module_test.go b/discovery/module_test.go index 6c32c28e6b..0367bfc464 100644 --- a/discovery/module_test.go +++ b/discovery/module_test.go @@ -56,7 +56,7 @@ func Test_Module_Add(t *testing.T) { presentationVerifier.EXPECT().VerifyVP(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return(nil, errors.New("failed")) err := m.Add(testServiceID, vpAlice) - require.EqualError(t, err, "presentation verification failed: failed") + require.EqualError(t, err, "presentation is invalid for registration\npresentation verification failed: failed") _, tag, err := m.Get(testServiceID, nil) require.NoError(t, err) @@ -70,7 +70,7 @@ func Test_Module_Add(t *testing.T) { err := m.Add(testServiceID, vpAlice) assert.NoError(t, err) err = m.Add(testServiceID, vpAlice) - assert.EqualError(t, err, "presentation already exists") + assert.ErrorIs(t, err, ErrPresentationAlreadyExists) }) t.Run("valid for too long", func(t *testing.T) { m, _ := setupModule(t, storageEngine) @@ -80,7 +80,7 @@ func Test_Module_Add(t *testing.T) { m.serverDefinitions[testServiceID] = def err := m.Add(testServiceID, vpAlice) - assert.EqualError(t, err, "presentation is valid for too long (max 1s)") + assert.EqualError(t, err, "presentation is invalid for registration\npresentation is valid for too long (max 1s)") }) t.Run("no expiration", func(t *testing.T) { m, _ := setupModule(t, storageEngine) @@ -88,7 +88,7 @@ func Test_Module_Add(t *testing.T) { claims[jwt.AudienceKey] = []string{testServiceID} delete(claims, "exp") })) - assert.EqualError(t, err, "presentation does not have an expiration") + assert.ErrorIs(t, err, errPresentationWithoutExpiration) }) t.Run("presentation does not contain an ID", func(t *testing.T) { m, _ := setupModule(t, storageEngine) @@ -98,12 +98,12 @@ func Test_Module_Add(t *testing.T) { delete(claims, "jti") }, vcAlice) err := m.Add(testServiceID, vpWithoutID) - assert.EqualError(t, err, "presentation does not have an ID") + assert.ErrorIs(t, err, errPresentationWithoutID) }) t.Run("not a JWT", func(t *testing.T) { m, _ := setupModule(t, storageEngine) err := m.Add(testServiceID, vc.VerifiablePresentation{}) - assert.EqualError(t, err, "only JWT presentations are supported") + assert.ErrorIs(t, err, errUnsupportedPresentationFormat) }) t.Run("registration", func(t *testing.T) { @@ -129,7 +129,7 @@ func Test_Module_Add(t *testing.T) { claims[jwt.AudienceKey] = []string{testServiceID} }, vcAlice) err := m.Add(testServiceID, vpAlice) - assert.EqualError(t, err, "presentation is valid longer than the credential(s) it contains") + assert.ErrorIs(t, err, errPresentationValidityExceedsCredentials) }) t.Run("not conform to Presentation Definition", func(t *testing.T) { m, _ := setupModule(t, storageEngine) @@ -163,7 +163,7 @@ func Test_Module_Add(t *testing.T) { t.Run("non-existent presentation", func(t *testing.T) { m, _ := setupModule(t, storageEngine) err := m.Add(testServiceID, vpAliceRetract) - assert.EqualError(t, err, "retraction presentation refers to a non-existing presentation") + assert.ErrorIs(t, err, errRetractionReferencesUnknownPresentation) }) t.Run("must not contain credentials", func(t *testing.T) { m, _ := setupModule(t, storageEngine) @@ -172,7 +172,7 @@ func Test_Module_Add(t *testing.T) { claims[jwt.AudienceKey] = []string{testServiceID} }, vcAlice) err := m.Add(testServiceID, vp) - assert.EqualError(t, err, "retraction presentation must not contain credentials") + assert.ErrorIs(t, err, errRetractionContainsCredentials) }) t.Run("missing 'retract_jti' claim", func(t *testing.T) { m, _ := setupModule(t, storageEngine) @@ -181,7 +181,7 @@ func Test_Module_Add(t *testing.T) { claims[jwt.AudienceKey] = []string{testServiceID} }) err := m.Add(testServiceID, vp) - assert.EqualError(t, err, "retraction presentation does not contain 'retract_jti' claim") + assert.ErrorIs(t, err, errInvalidRetractionJTIClaim) }) t.Run("'retract_jti' claim in not a string", func(t *testing.T) { m, _ := setupModule(t, storageEngine) @@ -191,7 +191,7 @@ func Test_Module_Add(t *testing.T) { claims[jwt.AudienceKey] = []string{testServiceID} }) err := m.Add(testServiceID, vp) - assert.EqualError(t, err, "retraction presentation 'retract_jti' claim is not a string") + assert.ErrorIs(t, err, errInvalidRetractionJTIClaim) }) }) } @@ -276,3 +276,27 @@ func TestModule_Configure(t *testing.T) { assert.ErrorContains(t, err, "unable to read definitions directory 'test/non_existent'") }) } + +func TestModule_Search(t *testing.T) { + storageEngine := storage.NewTestStorageEngine(t) + require.NoError(t, storageEngine.Start()) + t.Run("ok", func(t *testing.T) { + m, _ := setupModule(t, storageEngine) + require.NoError(t, m.store.add(testServiceID, vpAlice, nil)) + results, err := m.Search(testServiceID, map[string]string{ + "credentialSubject.id": aliceDID.String(), + }) + assert.NoError(t, err) + assert.Equal(t, []SearchResult{ + { + VP: vpAlice, + Fields: nil, + }, + }, results) + }) + t.Run("not a client for this service ID", func(t *testing.T) { + m, _ := setupModule(t, storageEngine) + _, err := m.Search("other", nil) + assert.ErrorIs(t, err, ErrServerModeDisabled) + }) +} diff --git a/docs/_static/discovery/v1.yaml b/docs/_static/discovery/v1.yaml index 84be706c9e..76eeecf636 100644 --- a/docs/_static/discovery/v1.yaml +++ b/docs/_static/discovery/v1.yaml @@ -92,6 +92,7 @@ paths: # See https://stackoverflow.com/questions/49582559/how-to-document-dynamic-query-parameter-names-in-openapi-swagger - in: query name: query + required: true schema: type: object additionalProperties: @@ -131,23 +132,31 @@ paths: schema: type: array items: - type: object - required: - - id - - credential - properties: - id: - type: string - description: The ID of the Verifiable Presentation. - credential: - type: object - description: The Verifiable Credential that matched the query. + $ref: "#/components/schemas/SearchResult" default: $ref: "../common/error_response.yaml" components: schemas: VerifiablePresentation: $ref: "../common/ssi_types.yaml#/components/schemas/VerifiablePresentation" + SearchResult: + type: object + required: + - id + - vp + - fields + properties: + id: + type: string + description: The ID of the Verifiable Presentation. + vp: + $ref: "#/components/schemas/VerifiablePresentation" + fields: + type: object + description: Input descriptor IDs and their mapped values that from the Verifiable Credential. + additionalProperties: + type: string + description: The value of the field that matched the query. securitySchemes: jwtBearerAuth: type: http diff --git a/docs/pages/integrating/api.rst b/docs/pages/integrating/api.rst index c59379ba43..a8d7f84d18 100644 --- a/docs/pages/integrating/api.rst +++ b/docs/pages/integrating/api.rst @@ -7,6 +7,7 @@ Below you can discover the Nuts Node APIs and download their OpenAPI specificati - Common: `SSI types <../../_static/common/ssi_types.yaml>`_, `Default Error <../../_static/common/error_response.yaml>`_ - `DID Manager <../../_static/didman/v1.yaml>`_ +- `Discovery Service <../../_static/discovery/v1.yaml>`_ - `Crypto <../../_static/crypto/v1.yaml>`_ - `Verifiable Credential Registry (v2) <../../_static/vcr/vcr_v2.yaml>`_ - `Verifiable Data Registry <../../_static/vdr/v1.yaml>`_ @@ -30,6 +31,7 @@ Below you can discover the Nuts Node APIs and download their OpenAPI specificati "dom_id": "#swagger-ui", urls: [ {url: "../../_static/didman/v1.yaml", name: "DID Manager"}, + {url: "../../_static/discovery/v1.yaml", name: "Discovery Service"}, {url: "../../_static/crypto/v1.yaml", name: "Crypto"}, {url: "../../_static/vcr/vcr_v2.yaml", name: "Verifiable Credential Registry (v2)"}, {url: "../../_static/vdr/v1.yaml", name: "Verifiable Data Registry"}, diff --git a/makefile b/makefile index 4932e9302a..1365b36384 100644 --- a/makefile +++ b/makefile @@ -70,6 +70,7 @@ gen-api: oapi-codegen --config codegen/configs/auth_employeeid.yaml auth/services/selfsigned/web/spec.yaml | gofmt > auth/services/selfsigned/web/generated.go oapi-codegen --config codegen/configs/auth_iam.yaml docs/_static/auth/iam.yaml | gofmt > auth/api/iam/generated.go oapi-codegen --config codegen/configs/didman_v1.yaml docs/_static/didman/v1.yaml | gofmt > didman/api/v1/generated.go + oapi-codegen --config codegen/configs/discovery_v1.yaml docs/_static/discovery/v1.yaml | gofmt > discovery/api/v1/generated.go oapi-codegen --config codegen/configs/crypto_store_client.yaml https://raw.githubusercontent.com/nuts-foundation/secret-store-api/main/nuts-storage-api-v1.yaml | gofmt > crypto/storage/external/generated.go oapi-codegen --config codegen/configs/policy_client_v1.yaml docs/_static/policy/v1.yaml | gofmt > policy/api/v1/client/generated.go diff --git a/vdr/management/management_mock.go b/vdr/management/management_mock.go index 6ae0dbd41f..f3d5f10cc2 100644 --- a/vdr/management/management_mock.go +++ b/vdr/management/management_mock.go @@ -14,138 +14,9 @@ import ( did "github.com/nuts-foundation/go-did/did" crypto "github.com/nuts-foundation/nuts-node/crypto" - resolver "github.com/nuts-foundation/nuts-node/vdr/resolver" gomock "go.uber.org/mock/gomock" ) -// MockManager is a mock of Manager interface. -type MockManager struct { - ctrl *gomock.Controller - recorder *MockManagerMockRecorder -} - -// MockManagerMockRecorder is the mock recorder for MockManager. -type MockManagerMockRecorder struct { - mock *MockManager -} - -// NewMockManager creates a new mock instance. -func NewMockManager(ctrl *gomock.Controller) *MockManager { - mock := &MockManager{ctrl: ctrl} - mock.recorder = &MockManagerMockRecorder{mock} - return mock -} - -// EXPECT returns an object that allows the caller to indicate expected use. -func (m *MockManager) EXPECT() *MockManagerMockRecorder { - return m.recorder -} - -// AddVerificationMethod mocks base method. -func (m *MockManager) AddVerificationMethod(ctx context.Context, id did.DID, keyUsage DIDKeyFlags) (*did.VerificationMethod, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "AddVerificationMethod", ctx, id, keyUsage) - ret0, _ := ret[0].(*did.VerificationMethod) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// AddVerificationMethod indicates an expected call of AddVerificationMethod. -func (mr *MockManagerMockRecorder) AddVerificationMethod(ctx, id, keyUsage any) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AddVerificationMethod", reflect.TypeOf((*MockManager)(nil).AddVerificationMethod), ctx, id, keyUsage) -} - -// Create mocks base method. -func (m *MockManager) Create(ctx context.Context, method string, options DIDCreationOptions) (*did.Document, crypto.Key, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "Create", ctx, method, options) - ret0, _ := ret[0].(*did.Document) - ret1, _ := ret[1].(crypto.Key) - ret2, _ := ret[2].(error) - return ret0, ret1, ret2 -} - -// Create indicates an expected call of Create. -func (mr *MockManagerMockRecorder) Create(ctx, method, options any) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Create", reflect.TypeOf((*MockManager)(nil).Create), ctx, method, options) -} - -// Deactivate mocks base method. -func (m *MockManager) Deactivate(ctx context.Context, id did.DID) error { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "Deactivate", ctx, id) - ret0, _ := ret[0].(error) - return ret0 -} - -// Deactivate indicates an expected call of Deactivate. -func (mr *MockManagerMockRecorder) Deactivate(ctx, id any) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Deactivate", reflect.TypeOf((*MockManager)(nil).Deactivate), ctx, id) -} - -// IsOwner mocks base method. -func (m *MockManager) IsOwner(arg0 context.Context, arg1 did.DID) (bool, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "IsOwner", arg0, arg1) - ret0, _ := ret[0].(bool) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// IsOwner indicates an expected call of IsOwner. -func (mr *MockManagerMockRecorder) IsOwner(arg0, arg1 any) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "IsOwner", reflect.TypeOf((*MockManager)(nil).IsOwner), arg0, arg1) -} - -// ListOwned mocks base method. -func (m *MockManager) ListOwned(ctx context.Context) ([]did.DID, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "ListOwned", ctx) - ret0, _ := ret[0].([]did.DID) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// ListOwned indicates an expected call of ListOwned. -func (mr *MockManagerMockRecorder) ListOwned(ctx any) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListOwned", reflect.TypeOf((*MockManager)(nil).ListOwned), ctx) -} - -// RemoveVerificationMethod mocks base method. -func (m *MockManager) RemoveVerificationMethod(ctx context.Context, id did.DID, keyID did.DIDURL) error { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "RemoveVerificationMethod", ctx, id, keyID) - ret0, _ := ret[0].(error) - return ret0 -} - -// RemoveVerificationMethod indicates an expected call of RemoveVerificationMethod. -func (mr *MockManagerMockRecorder) RemoveVerificationMethod(ctx, id, keyID any) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RemoveVerificationMethod", reflect.TypeOf((*MockManager)(nil).RemoveVerificationMethod), ctx, id, keyID) -} - -// Resolve mocks base method. -func (m *MockManager) Resolve(id did.DID, metadata *resolver.ResolveMetadata) (*did.Document, *resolver.DocumentMetadata, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "Resolve", id, metadata) - ret0, _ := ret[0].(*did.Document) - ret1, _ := ret[1].(*resolver.DocumentMetadata) - ret2, _ := ret[2].(error) - return ret0, ret1, ret2 -} - -// Resolve indicates an expected call of Resolve. -func (mr *MockManagerMockRecorder) Resolve(id, metadata any) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Resolve", reflect.TypeOf((*MockManager)(nil).Resolve), id, metadata) -} - // MockDocCreator is a mock of DocCreator interface. type MockDocCreator struct { ctrl *gomock.Controller @@ -170,9 +41,9 @@ func (m *MockDocCreator) EXPECT() *MockDocCreatorMockRecorder { } // Create mocks base method. -func (m *MockDocCreator) Create(ctx context.Context, method string, options DIDCreationOptions) (*did.Document, crypto.Key, error) { +func (m *MockDocCreator) Create(ctx context.Context, options DIDCreationOptions) (*did.Document, crypto.Key, error) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "Create", ctx, method, options) + ret := m.ctrl.Call(m, "Create", ctx, options) ret0, _ := ret[0].(*did.Document) ret1, _ := ret[1].(crypto.Key) ret2, _ := ret[2].(error) @@ -180,9 +51,9 @@ func (m *MockDocCreator) Create(ctx context.Context, method string, options DIDC } // Create indicates an expected call of Create. -func (mr *MockDocCreatorMockRecorder) Create(ctx, method, options any) *gomock.Call { +func (mr *MockDocCreatorMockRecorder) Create(ctx, options any) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Create", reflect.TypeOf((*MockDocCreator)(nil).Create), ctx, method, options) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Create", reflect.TypeOf((*MockDocCreator)(nil).Create), ctx, options) } // MockDocUpdater is a mock of DocUpdater interface.