diff --git a/cmd/orb-server/startcmd/start.go b/cmd/orb-server/startcmd/start.go index d9d08dd60..3efa1e6be 100644 --- a/cmd/orb-server/startcmd/start.go +++ b/cmd/orb-server/startcmd/start.go @@ -1221,6 +1221,7 @@ func startOrbServices(parameters *orbParameters) error { WebfingerClient: wfClient, LogEndpointRetriever: logEndpoint, WebResolver: webResolveHandler, + OrbResolver: orbDocResolveHandler, }) if err != nil { return fmt.Errorf("discovery rest: %w", err) diff --git a/pkg/discovery/endpoint/restapi/mocks/orbresolver.gen.go b/pkg/discovery/endpoint/restapi/mocks/orbresolver.gen.go new file mode 100644 index 000000000..6b20ff3fe --- /dev/null +++ b/pkg/discovery/endpoint/restapi/mocks/orbresolver.gen.go @@ -0,0 +1,115 @@ +// Code generated by counterfeiter. DO NOT EDIT. +package mocks + +import ( + "sync" + + "github.com/trustbloc/sidetree-core-go/pkg/document" +) + +type OrbResolver struct { + ResolveDocumentStub func(string, ...document.ResolutionOption) (*document.ResolutionResult, error) + resolveDocumentMutex sync.RWMutex + resolveDocumentArgsForCall []struct { + arg1 string + arg2 []document.ResolutionOption + } + resolveDocumentReturns struct { + result1 *document.ResolutionResult + result2 error + } + resolveDocumentReturnsOnCall map[int]struct { + result1 *document.ResolutionResult + result2 error + } + invocations map[string][][]interface{} + invocationsMutex sync.RWMutex +} + +func (fake *OrbResolver) ResolveDocument(arg1 string, arg2 ...document.ResolutionOption) (*document.ResolutionResult, error) { + fake.resolveDocumentMutex.Lock() + ret, specificReturn := fake.resolveDocumentReturnsOnCall[len(fake.resolveDocumentArgsForCall)] + fake.resolveDocumentArgsForCall = append(fake.resolveDocumentArgsForCall, struct { + arg1 string + arg2 []document.ResolutionOption + }{arg1, arg2}) + fake.recordInvocation("ResolveDocument", []interface{}{arg1, arg2}) + fake.resolveDocumentMutex.Unlock() + if fake.ResolveDocumentStub != nil { + return fake.ResolveDocumentStub(arg1, arg2...) + } + if specificReturn { + return ret.result1, ret.result2 + } + fakeReturns := fake.resolveDocumentReturns + return fakeReturns.result1, fakeReturns.result2 +} + +func (fake *OrbResolver) ResolveDocumentCallCount() int { + fake.resolveDocumentMutex.RLock() + defer fake.resolveDocumentMutex.RUnlock() + return len(fake.resolveDocumentArgsForCall) +} + +func (fake *OrbResolver) ResolveDocumentCalls(stub func(string, ...document.ResolutionOption) (*document.ResolutionResult, error)) { + fake.resolveDocumentMutex.Lock() + defer fake.resolveDocumentMutex.Unlock() + fake.ResolveDocumentStub = stub +} + +func (fake *OrbResolver) ResolveDocumentArgsForCall(i int) (string, []document.ResolutionOption) { + fake.resolveDocumentMutex.RLock() + defer fake.resolveDocumentMutex.RUnlock() + argsForCall := fake.resolveDocumentArgsForCall[i] + return argsForCall.arg1, argsForCall.arg2 +} + +func (fake *OrbResolver) ResolveDocumentReturns(result1 *document.ResolutionResult, result2 error) { + fake.resolveDocumentMutex.Lock() + defer fake.resolveDocumentMutex.Unlock() + fake.ResolveDocumentStub = nil + fake.resolveDocumentReturns = struct { + result1 *document.ResolutionResult + result2 error + }{result1, result2} +} + +func (fake *OrbResolver) ResolveDocumentReturnsOnCall(i int, result1 *document.ResolutionResult, result2 error) { + fake.resolveDocumentMutex.Lock() + defer fake.resolveDocumentMutex.Unlock() + fake.ResolveDocumentStub = nil + if fake.resolveDocumentReturnsOnCall == nil { + fake.resolveDocumentReturnsOnCall = make(map[int]struct { + result1 *document.ResolutionResult + result2 error + }) + } + fake.resolveDocumentReturnsOnCall[i] = struct { + result1 *document.ResolutionResult + result2 error + }{result1, result2} +} + +func (fake *OrbResolver) Invocations() map[string][][]interface{} { + fake.invocationsMutex.RLock() + defer fake.invocationsMutex.RUnlock() + fake.resolveDocumentMutex.RLock() + defer fake.resolveDocumentMutex.RUnlock() + copiedInvocations := map[string][][]interface{}{} + for key, value := range fake.invocations { + copiedInvocations[key] = value + } + return copiedInvocations +} + +func (fake *OrbResolver) recordInvocation(key string, args []interface{}) { + fake.invocationsMutex.Lock() + defer fake.invocationsMutex.Unlock() + if fake.invocations == nil { + fake.invocations = map[string][][]interface{}{} + } + if fake.invocations[key] == nil { + fake.invocations[key] = [][]interface{}{} + } + fake.invocations[key] = append(fake.invocations[key], args) +} diff --git a/pkg/discovery/endpoint/restapi/operations.go b/pkg/discovery/endpoint/restapi/operations.go index c626decb8..88a88b975 100644 --- a/pkg/discovery/endpoint/restapi/operations.go +++ b/pkg/discovery/endpoint/restapi/operations.go @@ -47,6 +47,7 @@ const ( nodeInfoEndpoint = "/.well-known/nodeinfo" orbWebDIDResolverEndpoint = "/1.0/identifiers/did:web:%s:scid:{id}" + orbDIDResolverEndpoint = "/1.0/identifiers/did:orb:{id}" selfRelation = "self" alternateRelation = "alternate" @@ -96,6 +97,10 @@ type webResolver interface { ResolveDocument(id string) (*document.ResolutionResult, error) } +type orbResolver interface { + ResolveDocument(id string, opts ...document.ResolutionOption) (*document.ResolutionResult, error) +} + // New returns discovery operations. func New(c *Config, p *Providers) (*Operation, error) { // If the WebCAS path is empty, it'll cause certain WebFinger queries to be matched incorrectly @@ -128,6 +133,7 @@ func New(c *Config, p *Providers) (*Operation, error) { anchorStore: p.AnchorLinkStore, wfClient: p.WebfingerClient, webResolver: p.WebResolver, + orbResolver: p.OrbResolver, domainWithPort: domainWithPort, }, nil } @@ -144,6 +150,7 @@ type Operation struct { anchorInfoRetriever logEndpointRetriever webResolver + orbResolver pubKeys, httpSignPubKeys []PublicKey resolutionPath string @@ -182,6 +189,7 @@ type Providers struct { WebfingerClient webfingerClient LogEndpointRetriever logEndpointRetriever WebResolver webResolver + OrbResolver orbResolver } // GetRESTHandlers get all controller API handler available for this service. @@ -195,6 +203,7 @@ func (o *Operation) GetRESTHandlers() []common.HTTPHandler { newHTTPHandler(nodeInfoEndpoint, o.nodeInfoHandler), newHTTPHandler(orbWebDIDFileEndpoint, o.orbWebDIDFileHandler), newHTTPHandler(fmt.Sprintf(orbWebDIDResolverEndpoint, o.domainWithPort), o.orbWebDIDResolverHandler), + newHTTPHandler(orbDIDResolverEndpoint, o.orbDIDResolverHandler), } // Only expose a service DID endpoint if the service ID is configured to be a DID. @@ -262,6 +271,27 @@ func (o *Operation) orbWebDIDResolverHandler(rw http.ResponseWriter, r *http.Req writeResponse(rw, result) } +func (o *Operation) orbDIDResolverHandler(rw http.ResponseWriter, r *http.Request) { + id := mux.Vars(r)["id"] + + result, err := o.orbResolver.ResolveDocument("did:orb:" + id) + if err != nil { + if errors.Is(err, orberrors.ErrContentNotFound) { + logger.Debugf("orb resource[%s] not found", id) + + writeErrorResponse(rw, http.StatusNotFound, "resource not found") + } else { + logger.Warnf("error returning orb resource [%s]: %s", id, err) + + writeErrorResponse(rw, http.StatusInternalServerError, "error retrieving resource") + } + + return + } + + writeResponse(rw, result) +} + // webDIDHandler swagger:route Get /.well-known/did.json discovery wellKnownDIDReq // // webDIDHandler. diff --git a/pkg/discovery/endpoint/restapi/operations_test.go b/pkg/discovery/endpoint/restapi/operations_test.go index 6620688e7..096cfe582 100644 --- a/pkg/discovery/endpoint/restapi/operations_test.go +++ b/pkg/discovery/endpoint/restapi/operations_test.go @@ -6,6 +6,7 @@ SPDX-License-Identifier: Apache-2.0 package restapi_test //go:generate counterfeiter -o ./mocks/webresolver.gen.go --fake-name WebResolver . webResolver +//go:generate counterfeiter -o ./mocks/orbresolver.gen.go --fake-name OrbResolver . orbResolver import ( "bytes" @@ -44,6 +45,7 @@ const ( orbWebDIDFileEndpoint = "/scid/{id}/did.json" orbWebDIDResolverEndpoint = "/1.0/identifiers/did:web:base:scid:{id}" + orbDIDResolverEndpoint = "/1.0/identifiers/did:orb:{id}" suffix = "suffix" ) @@ -133,7 +135,7 @@ func TestGetRESTHandlers(t *testing.T) { &restapi.Providers{}, ) require.NoError(t, err) - require.Equal(t, 8, len(c.GetRESTHandlers())) + require.Equal(t, 9, len(c.GetRESTHandlers())) }) t.Run("HTTP service ID Success", func(t *testing.T) { @@ -145,7 +147,7 @@ func TestGetRESTHandlers(t *testing.T) { c, err := restapi.New(cfg, &restapi.Providers{}) require.NoError(t, err) - require.Equal(t, 9, len(c.GetRESTHandlers()), + require.Equal(t, 10, len(c.GetRESTHandlers()), "Expecting 9 handlers, including the service did handler") }) } @@ -865,6 +867,84 @@ func TestOrbWebDIDResolver(t *testing.T) { }) } +func TestOrbDIDResolver(t *testing.T) { + t.Run("success", func(t *testing.T) { + didDoc := make(document.Document) + + or := &endpointmocks.OrbResolver{} + or.ResolveDocumentReturns(&document.ResolutionResult{Document: didDoc}, nil) + + c, err := restapi.New(&restapi.Config{ + OperationPath: "/op", + ResolutionPath: "/resolve", + WebCASPath: "/cas", + ServiceEndpointURL: testutil.MustParseURL("http://base/services/orb"), + }, + &restapi.Providers{OrbResolver: or}) + require.NoError(t, err) + + handler := getHandler(t, c, orbDIDResolverEndpoint) + + urlVars := make(map[string]string) + urlVars["id"] = "uAAA:" + suffix + + rr := serveHTTP(t, handler.Handler(), http.MethodGet, orbDIDResolverEndpoint, + nil, urlVars, false) + + require.Equal(t, http.StatusOK, rr.Code) + }) + + t.Run("error - resource not found", func(t *testing.T) { + or := &endpointmocks.OrbResolver{} + or.ResolveDocumentReturns(nil, orberrors.ErrContentNotFound) + + c, err := restapi.New(&restapi.Config{ + OperationPath: "/op", + ResolutionPath: "/resolve", + WebCASPath: "/cas", + ServiceEndpointURL: testutil.MustParseURL("http://base/services/orb"), + }, + &restapi.Providers{OrbResolver: or}) + require.NoError(t, err) + + handler := getHandler(t, c, orbDIDResolverEndpoint) + + urlVars := make(map[string]string) + urlVars["id"] = "uAAA:" + suffix + + rr := serveHTTP(t, handler.Handler(), http.MethodGet, orbDIDResolverEndpoint, + nil, urlVars, false) + + require.Equal(t, http.StatusNotFound, rr.Code) + require.Contains(t, rr.Body.String(), "resource not found") + }) + + t.Run("error - internal server error", func(t *testing.T) { + or := &endpointmocks.OrbResolver{} + or.ResolveDocumentReturns(nil, fmt.Errorf("internal error")) + + c, err := restapi.New(&restapi.Config{ + OperationPath: "/op", + ResolutionPath: "/resolve", + WebCASPath: "/cas", + ServiceEndpointURL: testutil.MustParseURL("http://base/services/orb"), + }, + &restapi.Providers{OrbResolver: or}) + require.NoError(t, err) + + handler := getHandler(t, c, orbDIDResolverEndpoint) + + urlVars := make(map[string]string) + urlVars["id"] = suffix + + rr := serveHTTP(t, handler.Handler(), http.MethodGet, orbDIDResolverEndpoint, + nil, urlVars, false) + + require.Equal(t, http.StatusInternalServerError, rr.Code) + require.Contains(t, rr.Body.String(), "error retrieving resource") + }) +} + func TestOrbWebDIDFile(t *testing.T) { t.Run("success", func(t *testing.T) { didDoc := make(document.Document) diff --git a/test/bdd/did_orb_steps.go b/test/bdd/did_orb_steps.go index adbf853a2..61535ed1f 100644 --- a/test/bdd/did_orb_steps.go +++ b/test/bdd/did_orb_steps.go @@ -366,11 +366,6 @@ func (d *DIDOrbSteps) clientVerifiesWebDocumentFromOrbDocument(didWebVar, didOrb } return diddoctransformer.VerifyWebDocumentFromOrbDocument(&didWebResolutionResult, &didOrbResolutionResult) - if err != nil { - return err - } - - return nil } func (d *DIDOrbSteps) clientFailsToVerifyResolvedDocument() error { diff --git a/test/bdd/features/did-sidetree.feature b/test/bdd/features/did-sidetree.feature index fa9809ad2..a94a37954 100644 --- a/test/bdd/features/did-sidetree.feature +++ b/test/bdd/features/did-sidetree.feature @@ -501,11 +501,25 @@ Feature: When client sends request to "https://orb.domain3.com/sidetree/v1/identifiers" to resolve DID document with interim did Then check success response contains "canonicalId" + Then the response is saved to variable "orbResponse" + And the JSON path "didDocumentMetadata.equivalentId.#" of the response has 3 items + And the JSON path "didDocumentMetadata.equivalentId.0" of the response is saved to variable "eqid_0" + And the JSON path "didDocumentMetadata.equivalentId.1" of the response is saved to variable "eqid_1" # test published did without corresponding did:web in also known as When an HTTP GET is sent to "https://orb.domain3.com/scid/${didSuffix}/did.json" When an HTTP GET is sent to "https://orb.domain3.com/1.0/identifiers/did:web:orb.domain3.com:scid:${didSuffix}" + Then the response is saved to variable "webResponse" + + Then client verifies that web document from variable "webResponse" is produced from orb document from variable "orbResponse" + + # test unpublished ID + When an HTTP GET is sent to "https://orb.domain3.com/1.0/identifiers/did:orb:uAAA:${didSuffix}" + + # test equivalent IDs (canonical, HL) + When an HTTP GET is sent to "https://orb.domain3.com/1.0/identifiers/${eqid_0}" + When an HTTP GET is sent to "https://orb.domain3.com/1.0/identifiers/${eqid_1}" When client sends request to "https://orb.domain3.com/sidetree/v1/operations" to add also known as URI "did:web:orb.domain3.com:scid:${didSuffix}" to DID document Then check for request success