diff --git a/cmd/root.go b/cmd/root.go index 4254a43350..154a429323 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -23,13 +23,9 @@ import ( "context" "errors" "fmt" - "github.com/nuts-foundation/nuts-node/discovery" - "github.com/nuts-foundation/nuts-node/vdr/resolver" - - "github.com/nuts-foundation/nuts-node/golden_hammer" - goldenHammerCmd "github.com/nuts-foundation/nuts-node/golden_hammer/cmd" - "github.com/nuts-foundation/nuts-node/vdr/didnuts" - "github.com/nuts-foundation/nuts-node/vdr/didnuts/didstore" + "github.com/sirupsen/logrus" + "github.com/spf13/cobra" + "github.com/spf13/pflag" "io" "os" "runtime/pprof" @@ -47,9 +43,12 @@ import ( "github.com/nuts-foundation/nuts-node/didman" 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" 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" + "github.com/nuts-foundation/nuts-node/golden_hammer" + goldenHammerCmd "github.com/nuts-foundation/nuts-node/golden_hammer/cmd" httpEngine "github.com/nuts-foundation/nuts-node/http" httpCmd "github.com/nuts-foundation/nuts-node/http/cmd" "github.com/nuts-foundation/nuts-node/jsonld" @@ -65,10 +64,11 @@ import ( vcrCmd "github.com/nuts-foundation/nuts-node/vcr/cmd" "github.com/nuts-foundation/nuts-node/vdr" vdrAPI "github.com/nuts-foundation/nuts-node/vdr/api/v1" + vdrAPIv2 "github.com/nuts-foundation/nuts-node/vdr/api/v2" vdrCmd "github.com/nuts-foundation/nuts-node/vdr/cmd" - "github.com/sirupsen/logrus" - "github.com/spf13/cobra" - "github.com/spf13/pflag" + "github.com/nuts-foundation/nuts-node/vdr/didnuts" + "github.com/nuts-foundation/nuts-node/vdr/didnuts/didstore" + "github.com/nuts-foundation/nuts-node/vdr/resolver" ) var stdOutWriter io.Writer = os.Stdout @@ -209,6 +209,7 @@ func CreateSystem(shutdownCallback context.CancelFunc) *core.System { Updater: vdrInstance, Resolver: vdrInstance.Resolver(), }}) + system.RegisterRoutes(&vdrAPIv2.Wrapper{VDR: vdrInstance, Storage: storageInstance, Crypto: cryptoInstance}) system.RegisterRoutes(&vcrAPI.Wrapper{VCR: credentialInstance, ContextManager: jsonld}) system.RegisterRoutes(&openid4vciAPI.Wrapper{ VCR: credentialInstance, diff --git a/docs/_static/vdr/v2.yaml b/docs/_static/vdr/v2.yaml index 63fdb1aea1..15c73c032b 100644 --- a/docs/_static/vdr/v2.yaml +++ b/docs/_static/vdr/v2.yaml @@ -95,13 +95,12 @@ paths: * 400 - Returned in case of malformed DID or service * 404 - Corresponding DID document could not be found * 500 - An error occurred while processing the request - parameters: - - name: service - in: body - description: Service to be added to the DID document. - required: true - schema: - $ref: '#/components/schemas/Service' + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/Service' operationId: addService tags: - DID @@ -111,7 +110,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/VerificationMethod' + $ref: '#/components/schemas/Service' default: $ref: '../common/error_response.yaml' /internal/vdr/v2/did/{did}/service/{id}: diff --git a/e2e-tests/oauth-flow/rfc021/docker-compose.yml b/e2e-tests/oauth-flow/rfc021/docker-compose.yml index 4f62fcffad..be39404578 100644 --- a/e2e-tests/oauth-flow/rfc021/docker-compose.yml +++ b/e2e-tests/oauth-flow/rfc021/docker-compose.yml @@ -1,7 +1,7 @@ version: "3.7" services: nodeA-backend: - image: "${IMAGE_NODE_A:-nutsfoundation/nuts-node:master}" + image: "${IMAGE_NODE_A:-nutsfoundation/nuts-node:latest}" ports: - "11323:1323" environment: @@ -24,8 +24,8 @@ services: - "../../tls-certs/nodeA-certificate.pem:/etc/nginx/ssl/key.pem:ro" - "../../tls-certs/truststore.pem:/etc/nginx/ssl/truststore.pem:ro" - "./node-A/html:/etc/nginx/html:ro" - nodeB: - image: "${IMAGE_NODE_B:-nutsfoundation/nuts-node:master}" + nodeB-backend: + image: "${IMAGE_NODE_B:-nutsfoundation/nuts-node:latest}" ports: - "21323:1323" environment: @@ -38,3 +38,13 @@ services: - "../../tls-certs/truststore.pem:/etc/ssl/certs/truststore.pem:ro" healthcheck: interval: 1s # Make test run quicker by checking health status more often + nodeB: + image: nginx:1.25.1 + ports: + - "20443:443" + volumes: + - "./node-B/nginx.conf:/etc/nginx/nginx.conf:ro" + - "../../tls-certs/nodeB-certificate.pem:/etc/nginx/ssl/server.pem:ro" + - "../../tls-certs/nodeB-certificate.pem:/etc/nginx/ssl/key.pem:ro" + - "../../tls-certs/truststore.pem:/etc/nginx/ssl/truststore.pem:ro" + - "./node-B/html:/etc/nginx/html:ro" diff --git a/e2e-tests/oauth-flow/rfc021/node-B/nuts.yaml b/e2e-tests/oauth-flow/rfc021/node-B/nuts.yaml index 48297172e5..a1d33171fa 100644 --- a/e2e-tests/oauth-flow/rfc021/node-B/nuts.yaml +++ b/e2e-tests/oauth-flow/rfc021/node-B/nuts.yaml @@ -17,4 +17,3 @@ tls: truststorefile: /opt/nuts/truststore.pem certfile: /opt/nuts/certificate-and-key.pem certkeyfile: /opt/nuts/certificate-and-key.pem - diff --git a/e2e-tests/oauth-flow/rfc021/run-test.sh b/e2e-tests/oauth-flow/rfc021/run-test.sh index 9c07d7e052..4e0604161d 100755 --- a/e2e-tests/oauth-flow/rfc021/run-test.sh +++ b/e2e-tests/oauth-flow/rfc021/run-test.sh @@ -24,12 +24,12 @@ VENDOR_A_DID=$(echo $VENDOR_A_DIDDOC | jq -r .id) echo Vendor A DID: $VENDOR_A_DID # Register Vendor B -VENDOR_B_DIDDOC=$(docker compose exec nodeB nuts vdr create-did --v2) +VENDOR_B_DIDDOC=$(docker compose exec nodeB-backend nuts vdr create-did --v2) VENDOR_B_DID=$(echo $VENDOR_B_DIDDOC | jq -r .id) echo Vendor B DID: $VENDOR_B_DID # Issue NutsOrganizationCredential for Vendor B -REQUEST="{\"type\":\"NutsOrganizationCredential\",\"issuer\":\"${VENDOR_B_DID}\", \"credentialSubject\": {\"id\":\"${VENDOR_B_DID}\", \"organization\":{\"name\":\"Caresoft B.V.\", \"city\":\"Caretown\"}},\"visibility\": \"public\"}" +REQUEST="{\"type\":\"NutsOrganizationCredential\",\"issuer\":\"${VENDOR_B_DID}\", \"credentialSubject\": {\"id\":\"${VENDOR_B_DID}\", \"organization\":{\"name\":\"Caresoft B.V.\", \"city\":\"Caretown\"}},\"visibility\": \"private\"}" RESPONSE=$(echo $REQUEST | curl -X POST --data-binary @- http://localhost:21323/internal/vcr/v2/issuer/vc -H "Content-Type:application/json") if echo $RESPONSE | grep -q "VerifiableCredential"; then echo "VC issued" diff --git a/vdr/api/v2/api.go b/vdr/api/v2/api.go index 97e7df0835..7624edb885 100644 --- a/vdr/api/v2/api.go +++ b/vdr/api/v2/api.go @@ -21,10 +21,15 @@ package v2 import ( "context" + "github.com/labstack/echo/v4" + "github.com/nuts-foundation/go-did/did" + "github.com/nuts-foundation/nuts-node/audit" "github.com/nuts-foundation/nuts-node/core" "github.com/nuts-foundation/nuts-node/vdr" "github.com/nuts-foundation/nuts-node/vdr/didweb" "github.com/nuts-foundation/nuts-node/vdr/management" + "github.com/nuts-foundation/nuts-node/vdr/resolver" + "net/http" ) var _ StrictServerInterface = (*Wrapper)(nil) @@ -35,9 +40,30 @@ type Wrapper struct { VDR vdr.VDR } -func (w Wrapper) ResolveStatusCode(err error) int { - //TODO implement me - panic("implement me") +// ResolveStatusCode maps errors returned by this API to specific HTTP status codes. +func (a *Wrapper) ResolveStatusCode(err error) int { + return core.ResolveStatusCode(err, map[error]int{ + resolver.ErrNotFound: http.StatusNotFound, + resolver.ErrDIDNotManagedByThisNode: http.StatusForbidden, + resolver.ErrDuplicateService: http.StatusBadRequest, + did.ErrInvalidDID: http.StatusBadRequest, + }) +} + +func (a *Wrapper) Routes(router core.EchoRouter) { + RegisterHandlers(router, NewStrictHandler(a, []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, vdr.ModuleName) + ctx.Set(core.StatusCodeResolverContextKey, a) + return f(ctx, request) + } + }, + func(f StrictHandlerFunc, operationID string) StrictHandlerFunc { + return audit.StrictMiddleware(f, vdr.ModuleName, operationID) + }, + })) } func (w Wrapper) CreateDID(ctx context.Context, _ CreateDIDRequestObject) (CreateDIDResponseObject, error) { diff --git a/vdr/api/v2/generated.go b/vdr/api/v2/generated.go index ca2abd7af4..1bd71ea316 100644 --- a/vdr/api/v2/generated.go +++ b/vdr/api/v2/generated.go @@ -4,6 +4,7 @@ package v2 import ( + "bytes" "context" "encoding/json" "fmt" @@ -30,6 +31,9 @@ type DIDResolutionResult struct { DocumentMetadata DIDDocumentMetadata `json:"documentMetadata"` } +// AddServiceJSONRequestBody defines body for AddService for application/json ContentType. +type AddServiceJSONRequestBody = Service + // RequestEditorFn is the function signature for the RequestEditor callback function type RequestEditorFn func(ctx context.Context, req *http.Request) error @@ -112,8 +116,10 @@ type ClientInterface interface { // ResolveDID request ResolveDID(ctx context.Context, did string, reqEditors ...RequestEditorFn) (*http.Response, error) - // AddService request - AddService(ctx context.Context, did string, reqEditors ...RequestEditorFn) (*http.Response, error) + // AddServiceWithBody request with any body + AddServiceWithBody(ctx context.Context, did string, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) + + AddService(ctx context.Context, did string, body AddServiceJSONRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error) // DeleteService request DeleteService(ctx context.Context, did string, id string, reqEditors ...RequestEditorFn) (*http.Response, error) @@ -164,8 +170,20 @@ func (c *Client) ResolveDID(ctx context.Context, did string, reqEditors ...Reque return c.Client.Do(req) } -func (c *Client) AddService(ctx context.Context, did string, reqEditors ...RequestEditorFn) (*http.Response, error) { - req, err := NewAddServiceRequest(c.Server, did) +func (c *Client) AddServiceWithBody(ctx context.Context, did string, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) { + req, err := NewAddServiceRequestWithBody(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) AddService(ctx context.Context, did string, body AddServiceJSONRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error) { + req, err := NewAddServiceRequest(c.Server, did, body) if err != nil { return nil, err } @@ -319,8 +337,19 @@ func NewResolveDIDRequest(server string, did string) (*http.Request, error) { return req, nil } -// NewAddServiceRequest generates requests for AddService -func NewAddServiceRequest(server string, did string) (*http.Request, error) { +// NewAddServiceRequest calls the generic AddService builder with application/json body +func NewAddServiceRequest(server string, did string, body AddServiceJSONRequestBody) (*http.Request, error) { + var bodyReader io.Reader + buf, err := json.Marshal(body) + if err != nil { + return nil, err + } + bodyReader = bytes.NewReader(buf) + return NewAddServiceRequestWithBody(server, did, "application/json", bodyReader) +} + +// NewAddServiceRequestWithBody generates requests for AddService with any type of body +func NewAddServiceRequestWithBody(server string, did string, contentType string, body io.Reader) (*http.Request, error) { var err error var pathParam0 string @@ -345,11 +374,13 @@ func NewAddServiceRequest(server string, did string) (*http.Request, error) { return nil, err } - req, err := http.NewRequest("POST", queryURL.String(), nil) + req, err := http.NewRequest("POST", queryURL.String(), body) if err != nil { return nil, err } + req.Header.Add("Content-Type", contentType) + return req, nil } @@ -562,8 +593,10 @@ type ClientWithResponsesInterface interface { // ResolveDIDWithResponse request ResolveDIDWithResponse(ctx context.Context, did string, reqEditors ...RequestEditorFn) (*ResolveDIDResponse, error) - // AddServiceWithResponse request - AddServiceWithResponse(ctx context.Context, did string, reqEditors ...RequestEditorFn) (*AddServiceResponse, error) + // AddServiceWithBodyWithResponse request with any body + AddServiceWithBodyWithResponse(ctx context.Context, did string, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*AddServiceResponse, error) + + AddServiceWithResponse(ctx context.Context, did string, body AddServiceJSONRequestBody, reqEditors ...RequestEditorFn) (*AddServiceResponse, error) // DeleteServiceWithResponse request DeleteServiceWithResponse(ctx context.Context, did string, id string, reqEditors ...RequestEditorFn) (*DeleteServiceResponse, error) @@ -676,7 +709,7 @@ func (r ResolveDIDResponse) StatusCode() int { type AddServiceResponse struct { Body []byte HTTPResponse *http.Response - JSON200 *VerificationMethod + JSON200 *Service ApplicationproblemJSONDefault *struct { // Detail A human-readable explanation specific to this occurrence of the problem. Detail string `json:"detail"` @@ -858,9 +891,17 @@ func (c *ClientWithResponses) ResolveDIDWithResponse(ctx context.Context, did st return ParseResolveDIDResponse(rsp) } -// AddServiceWithResponse request returning *AddServiceResponse -func (c *ClientWithResponses) AddServiceWithResponse(ctx context.Context, did string, reqEditors ...RequestEditorFn) (*AddServiceResponse, error) { - rsp, err := c.AddService(ctx, did, reqEditors...) +// AddServiceWithBodyWithResponse request with arbitrary body returning *AddServiceResponse +func (c *ClientWithResponses) AddServiceWithBodyWithResponse(ctx context.Context, did string, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*AddServiceResponse, error) { + rsp, err := c.AddServiceWithBody(ctx, did, contentType, body, reqEditors...) + if err != nil { + return nil, err + } + return ParseAddServiceResponse(rsp) +} + +func (c *ClientWithResponses) AddServiceWithResponse(ctx context.Context, did string, body AddServiceJSONRequestBody, reqEditors ...RequestEditorFn) (*AddServiceResponse, error) { + rsp, err := c.AddService(ctx, did, body, reqEditors...) if err != nil { return nil, err } @@ -1037,7 +1078,7 @@ func ParseAddServiceResponse(rsp *http.Response) (*AddServiceResponse, error) { switch { case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 200: - var dest VerificationMethod + var dest Service if err := json.Unmarshal(bodyBytes, &dest); err != nil { return nil, err } @@ -1564,14 +1605,15 @@ func (response ResolveDIDdefaultApplicationProblemPlusJSONResponse) VisitResolve } type AddServiceRequestObject struct { - Did string `json:"did"` + Did string `json:"did"` + Body *AddServiceJSONRequestBody } type AddServiceResponseObject interface { VisitAddServiceResponse(w http.ResponseWriter) error } -type AddService200JSONResponse VerificationMethod +type AddService200JSONResponse Service func (response AddService200JSONResponse) VisitAddServiceResponse(w http.ResponseWriter) error { w.Header().Set("Content-Type", "application/json") @@ -1873,6 +1915,12 @@ func (sh *strictHandler) AddService(ctx echo.Context, did string) error { request.Did = did + var body AddServiceJSONRequestBody + if err := ctx.Bind(&body); err != nil { + return err + } + request.Body = &body + handler := func(ctx echo.Context, request interface{}) (interface{}, error) { return sh.ssi.AddService(ctx.Request().Context(), request.(AddServiceRequestObject)) } diff --git a/vdr/interface.go b/vdr/interface.go index 8dcf143103..eaa39305dc 100644 --- a/vdr/interface.go +++ b/vdr/interface.go @@ -33,13 +33,10 @@ type VDR interface { // Create creates a new DID document according to the given DID method and returns it. Create(ctx context.Context, method string, options management.DIDCreationOptions) (*did.Document, crypto.Key, error) - // ResolveManaged resolves a DID document that is managed by the local node. ResolveManaged(id did.DID) (*did.Document, error) - // Resolver returns the resolver for getting the DID document for a DID. Resolver() resolver.DIDResolver - // ConflictedDocuments returns the DID Document and metadata of all documents with a conflict. ConflictedDocuments() ([]did.Document, []resolver.DocumentMetadata, error) }