nuts-foundation/nuts-node

View on GitHub
didman/api/v1/api.go

Summary

Maintainability
B
4 hrs
Test Coverage
B
88%
/*
 * Nuts node
 * Copyright (C) 2021 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 <https://www.gnu.org/licenses/>.
 *
 */

package v1

import (
    "context"
    "errors"
    "github.com/labstack/echo/v4"
    "github.com/nuts-foundation/nuts-node/audit"
    "github.com/nuts-foundation/nuts-node/vdr/didnuts"
    "github.com/nuts-foundation/nuts-node/vdr/resolver"
    "net/http"
    "net/url"
    "strings"

    ssi "github.com/nuts-foundation/go-did"
    "github.com/nuts-foundation/go-did/did"
    "github.com/nuts-foundation/nuts-node/core"
    "github.com/nuts-foundation/nuts-node/didman"
)

var _ StrictServerInterface = (*Wrapper)(nil)
var _ core.ErrorStatusCodeResolver = (*Wrapper)(nil)

// Wrapper implements the generated interface from oapi-codegen
type Wrapper struct {
    Didman didman.Didman
}

// ResolveStatusCode maps errors returned by this API to specific HTTP status codes.
func (w *Wrapper) ResolveStatusCode(err error) int {
    switch {
    case errors.Is(err, did.ErrInvalidDID):
        return http.StatusBadRequest
    case errors.Is(err, resolver.ErrNotFound):
        return http.StatusNotFound
    case errors.Is(err, resolver.ErrDIDNotManagedByThisNode):
        return http.StatusBadRequest
    case errors.Is(err, resolver.ErrDeactivated):
        return http.StatusConflict
    case errors.Is(err, resolver.ErrDuplicateService):
        return http.StatusConflict
    case errors.Is(err, didman.ErrServiceInUse):
        return http.StatusConflict
    case errors.Is(err, didnuts.ErrInvalidOptions):
        return http.StatusBadRequest
    case errors.Is(err, resolver.ErrServiceNotFound):
        return http.StatusNotFound
    case errors.As(err, new(didnuts.InvalidServiceError)):
        return http.StatusBadRequest
    case errors.As(err, new(resolver.ServiceQueryError)):
        return http.StatusBadRequest
    case errors.Is(err, resolver.ErrServiceReferenceToDeep):
        return http.StatusNotAcceptable
    case errors.As(err, new(didman.ErrReferencedServiceNotAnEndpoint)):
        return http.StatusNotAcceptable
    default:
        return http.StatusInternalServerError
    }
}

// Routes registers the routes from the open api spec to the echo router.
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, didman.ModuleName)
                ctx.Set(core.StatusCodeResolverContextKey, w)
                return f(ctx, request)
            }
        },
        func(f StrictHandlerFunc, operationID string) StrictHandlerFunc {
            return audit.StrictMiddleware(f, didman.ModuleName, operationID)
        },
    }))
}

func (w *Wrapper) addOrUpdateEndpoint(
    ctx context.Context, requestDID string, properties EndpointProperties,
    operation func(ctx context.Context, id did.DID, serviceType string, endpoint url.URL) (*did.Service, error),
) (*did.Service, error) {
    id, err := did.ParseDID(requestDID)
    if err != nil {
        return nil, err
    }

    if len(strings.TrimSpace(properties.Type)) == 0 {
        return nil, core.InvalidInputError("invalid value for type")
    }

    endpoint, err := url.Parse(properties.Endpoint)
    if err != nil {
        return nil, core.InvalidInputError("invalid value for endpoint: %w", err)
    }
    return operation(ctx, *id, properties.Type, *endpoint)
}

// AddEndpoint handles calls to add a service. It only checks params and sets the correct return status code.
// didman.AddEndpoint does the heavy lifting.
func (w *Wrapper) AddEndpoint(ctx context.Context, request AddEndpointRequestObject) (AddEndpointResponseObject, error) {
    endpoint, err := w.addOrUpdateEndpoint(ctx, request.Did, *request.Body, w.Didman.AddEndpoint)
    if err != nil {
        return nil, err
    }
    return AddEndpoint200JSONResponse(*endpoint), nil
}

// UpdateEndpoint handles calls to update a service. It only checks params and sets the correct return status code.
// didman.UpdateEndpoint does the heavy lifting.
func (w *Wrapper) UpdateEndpoint(ctx context.Context, request UpdateEndpointRequestObject) (UpdateEndpointResponseObject, error) {
    if request.Body.Type != "" && request.Body.Type != request.Type {
        return nil, core.InvalidInputError("updating endpoint type is not supported")
    }
    request.Body.Type = request.Type
    endpoint, err := w.addOrUpdateEndpoint(ctx, request.Did, *request.Body, w.Didman.UpdateEndpoint)
    if err != nil {
        return nil, err
    }
    return UpdateEndpoint200JSONResponse(*endpoint), nil
}

// DeleteEndpointsByType handles calls to delete an endpoint. It only checks params and sets the correct return status code.
// didman.DeleteEndpoint does the heavy lifting.
func (w *Wrapper) DeleteEndpointsByType(ctx context.Context, request DeleteEndpointsByTypeRequestObject) (DeleteEndpointsByTypeResponseObject, error) {
    id, err := did.ParseDID(request.Did)
    if err != nil {
        return nil, err
    }

    if len(strings.TrimSpace(request.Type)) == 0 {
        return nil, core.InvalidInputError("invalid endpointType")
    }

    err = w.Didman.DeleteEndpointsByType(ctx, *id, request.Type)
    if err != nil {
        return nil, err
    }
    return DeleteEndpointsByType204Response{}, nil
}

// GetCompoundServices handles calls to get a list of compound services for a provided DID string.
// Its checks params, calls Didman and sets http return values.
func (w *Wrapper) GetCompoundServices(_ context.Context, request GetCompoundServicesRequestObject) (GetCompoundServicesResponseObject, error) {
    id, err := did.ParseDID(request.Did)
    if err != nil {
        return nil, err
    }
    services, err := w.Didman.GetCompoundServices(*id)
    if err != nil {
        return nil, err
    }
    // Service may return nil for empty arrays, map to empty array to avoid returning null from the REST API
    if services == nil {
        services = make([]did.Service, 0)
    }
    r := GetCompoundServices200JSONResponse{}
    for _, service := range services {
        r = append(r, CompoundService(service))
    }
    return r, nil
}

func (w *Wrapper) addOrUpdateCompoundService(
    ctx context.Context, requestDID string, properties CompoundServiceProperties,
    operation func(ctx context.Context, id did.DID, serviceType string, references map[string]ssi.URI) (*did.Service, error),
) (*did.Service, error) {
    id, err := did.ParseDID(requestDID)
    if err != nil {
        return nil, err
    }

    if len(strings.TrimSpace(properties.Type)) == 0 {
        return nil, core.InvalidInputError("invalid value for type")
    }

    // The api accepts a map[string]interface{} which must be converted to a map[string]ssi.URI.
    references := make(map[string]ssi.URI, len(properties.ServiceEndpoint))
    for key, value := range properties.ServiceEndpoint {
        uri, err := interfaceToURI(value)
        if err != nil {
            return nil, core.InvalidInputError("invalid reference for service '%s': %v", key, err)
        }
        references[key] = *uri
    }

    return operation(ctx, *id, properties.Type, references)
}

// AddCompoundService handles calls to add a compound service.
// A CompoundService consists of a type and a map of name -> serviceEndpoint(Ref).
//
// This method checks the params: valid DID and type format
// Converts the request to an CompoundService
// Calls didman.AddCompoundService, which does the heavy lifting.
// Converts the response of AddCompoundService, which is a did.Service back to a CompoundService
// Sets the http status OK and adds the CompoundService to the response
func (w *Wrapper) AddCompoundService(ctx context.Context, request AddCompoundServiceRequestObject) (AddCompoundServiceResponseObject, error) {
    service, err := w.addOrUpdateCompoundService(ctx, request.Did, *request.Body, w.Didman.AddCompoundService)
    if err != nil {
        return nil, err
    }
    return AddCompoundService200JSONResponse(*service), nil
}

// UpdateCompoundService handles calls to update a compound service.
func (w *Wrapper) UpdateCompoundService(ctx context.Context, request UpdateCompoundServiceRequestObject) (UpdateCompoundServiceResponseObject, error) {
    if request.Body.Type != "" && request.Body.Type != request.Type {
        return nil, core.InvalidInputError("updating compound service type is not supported")
    }
    request.Body.Type = request.Type
    service, err := w.addOrUpdateCompoundService(ctx, request.Did, *request.Body, w.Didman.UpdateCompoundService)
    if err != nil {
        return nil, err
    }
    return UpdateCompoundService200JSONResponse(*service), nil
}

// GetCompoundServiceEndpoint handles calls to read a specific endpoint of a compound service.
func (w *Wrapper) GetCompoundServiceEndpoint(_ context.Context, request GetCompoundServiceEndpointRequestObject) (GetCompoundServiceEndpointResponseObject, error) {
    id, err := did.ParseDID(request.Did)
    if err != nil {
        return nil, err
    }
    resolve := true
    if request.Params.Resolve != nil {
        resolve = *request.Params.Resolve
    }
    endpoint, err := w.Didman.GetCompoundServiceEndpoint(*id, request.CompoundServiceType, request.EndpointType, resolve)
    if err != nil {
        return nil, err
    }

    // By default application/json, text/plain if explicitly requested
    var accept string
    if request.Params.Accept != nil {
        accept = *request.Params.Accept
    }
    switch accept {
    case "text/plain":
        return GetCompoundServiceEndpoint200TextResponse(endpoint), nil
    default:
        return GetCompoundServiceEndpoint200JSONResponse{Endpoint: endpoint}, nil
    }
}

func interfaceToURI(input interface{}) (*ssi.URI, error) {
    str, ok := input.(string)
    if !ok {
        return nil, errors.New("not a string")
    }
    return ssi.ParseURI(str)
}

// DeleteService handles calls to delete a service. It only checks params and sets the correct return status code.
// didman.DeleteService does the heavy lifting.
func (w *Wrapper) DeleteService(ctx context.Context, request DeleteServiceRequestObject) (DeleteServiceResponseObject, error) {
    id, err := ssi.ParseURI(request.Id)
    if err != nil {
        return nil, core.InvalidInputError("failed to parse URI: %w", err)
    }

    if err = w.Didman.DeleteService(ctx, *id); err != nil {
        return nil, err
    }

    return DeleteService204Response{}, nil
}

// UpdateContactInformation handles requests for updating contact information for a specific DID.
// It parses the did path param and unmarshals the request body and passes them to didman.UpdateContactInformation.
func (w *Wrapper) UpdateContactInformation(ctx context.Context, request UpdateContactInformationRequestObject) (UpdateContactInformationResponseObject, error) {
    id, err := did.ParseDID(request.Did)
    if err != nil {
        return nil, err
    }
    newContactInfo, err := w.Didman.UpdateContactInformation(ctx, *id, *request.Body)
    if err != nil {
        return nil, err
    }

    return UpdateContactInformation200JSONResponse(*newContactInfo), nil
}

// GetContactInformation handles requests for contact information for a specific DID.
// It parses the did path param and passes it to didman.GetContactInformation.
func (w *Wrapper) GetContactInformation(_ context.Context, request GetContactInformationRequestObject) (GetContactInformationResponseObject, error) {
    id, err := did.ParseDID(request.Did)
    if err != nil {
        return nil, err
    }

    contactInfo, err := w.Didman.GetContactInformation(*id)
    if err != nil {
        return nil, err
    }
    if contactInfo == nil {
        return nil, core.NotFoundError("contact information for DID not found")
    }

    return GetContactInformation200JSONResponse(*contactInfo), nil
}

// SearchOrganizations handles requests for searching organizations, meaning it looks for (valid) Verifiable Credentials
// that map to the "organization" concept and where its subject resolves to an active DID Document.
// It optionally filters only on organizations which DID documents contain a service with the specified type.
func (w *Wrapper) SearchOrganizations(ctx context.Context, request SearchOrganizationsRequestObject) (SearchOrganizationsResponseObject, error) {
    results, err := w.Didman.SearchOrganizations(ctx, request.Params.Query, request.Params.DidServiceType)
    if err != nil {
        return nil, err
    }
    // Service may return nil for empty arrays, map to empty array to avoid returning null from the REST API
    if results == nil {
        results = make([]OrganizationSearchResult, 0)
    }
    return SearchOrganizations200JSONResponse(results), nil
}