Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: Return the canonical DID in a WebFinger query #782

Merged
merged 1 commit into from
Sep 21, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
80 changes: 80 additions & 0 deletions pkg/discovery/endpoint/restapi/anchorinfo.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
/*
Copyright SecureKey Technologies Inc. All Rights Reserved.

SPDX-License-Identifier: Apache-2.0
*/

package restapi

import (
"fmt"

"github.com/trustbloc/orb/pkg/resolver/resource/registry"
)

// AnchorInfo contains information about an anchor credential.
type AnchorInfo struct {
AnchorOrigin string
AnchorURI string
CanonicalReference string
}

// AnchorInfoRetriever retrieves anchor information about a DID.
type AnchorInfoRetriever struct {
resourceRegistry *registry.Registry
}

// NewAnchorInfoRetriever returns a new AnchorInfoRetriever.
func NewAnchorInfoRetriever(r *registry.Registry) *AnchorInfoRetriever {
return &AnchorInfoRetriever{resourceRegistry: r}
}

// GetAnchorInfo returns anchor information about the given DID.
func (r *AnchorInfoRetriever) GetAnchorInfo(did string) (*AnchorInfo, error) {
// TODO (#537): Show IPFS alternates if configured.
metadata, err := r.resourceRegistry.GetResourceInfo(did)
if err != nil {
return nil, fmt.Errorf("get info for DID [%s]: %w", did, err)
}

info := &AnchorInfo{}

info.AnchorOrigin, err = r.getProperty(registry.AnchorOriginProperty, metadata, true)
if err != nil {
return nil, fmt.Errorf("get anchor origin for DID [%s]: %w", did, err)
}

info.AnchorURI, err = r.getProperty(registry.AnchorURIProperty, metadata, true)
if err != nil {
return nil, fmt.Errorf("get anchor URI for DID [%s]: %w", did, err)
}

info.CanonicalReference, err = r.getProperty(registry.CanonicalReferenceProperty, metadata, false)
if err != nil {
return nil, fmt.Errorf("get canonical ID for DID [%s]: %w", did, err)
}

return info, nil
}

func (r *AnchorInfoRetriever) getProperty(property string, metadata registry.Metadata, required bool) (string, error) {
rawValue, ok := metadata[property]
if !ok {
if !required {
return "", nil
}

return "", fmt.Errorf("property required [%s]", property)
}

value, ok := rawValue.(string)
if !ok {
return "", fmt.Errorf("could not assert property as a string [%s]", property)
}

if value == "" && required {
return "", fmt.Errorf("property required [%s]", property)
}

return value, nil
}
113 changes: 113 additions & 0 deletions pkg/discovery/endpoint/restapi/anchorinfo_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
/*
Copyright SecureKey Technologies Inc. All Rights Reserved.

SPDX-License-Identifier: Apache-2.0
*/

package restapi_test

import (
"errors"
"testing"

"github.com/stretchr/testify/require"

"github.com/trustbloc/orb/pkg/discovery/endpoint/restapi"
"github.com/trustbloc/orb/pkg/resolver/resource/registry"
)

func TestAnchorInfoRetriever_GetAnchorInfo(t *testing.T) {
const (
anchorOrigin = "ipns://k51qzi5uqu5dgkmm1afrkmex5mzpu5r774jstpxjmro6mdsaullur27nfxle1q"
anchorURI = "hl:uEiALYp_C4wk2WegpfnCSoSTBdKZ1MVdDadn4rdmZl5GKzQ:uoQ-BeDVpcGZzOi8vUW1jcTZKV0RVa3l4ZWhxN1JWWmtQM052aUU0SHFSdW5SalgzOXZ1THZFSGFRTg" //nolint:lll
interimDID = "interimDID:orb:uAAA:EiAWMpJJMauUlAr58MBpdWrfL9Y274xwElaCsfb0P5kmjQ"
canonicalRef = "uEiDaapVGRRwUa8-8e0wJQknOeFDiYjnhysjsoA6vL8U60g"
)

t.Run("Success", func(t *testing.T) {
resourceInfoProvider := newMockResourceInfoProvider().
withAnchorOrigin(anchorOrigin).
withAnchorURI(anchorURI).
withCanonicalRef(canonicalRef)

r := restapi.NewAnchorInfoRetriever(registry.New(registry.WithResourceInfoProvider(resourceInfoProvider)))

info, err := r.GetAnchorInfo(interimDID)
require.NoError(t, err)
require.NotNil(t, info)
require.Equal(t, anchorOrigin, info.AnchorOrigin)
require.Equal(t, anchorURI, info.AnchorURI)
require.Equal(t, canonicalRef, info.CanonicalReference)
})

t.Run("Resource registry error", func(t *testing.T) {
errExpected := errors.New("injected resource registry error")

resourceInfoProvider := newMockResourceInfoProvider().withError(errExpected)

r := restapi.NewAnchorInfoRetriever(registry.New(registry.WithResourceInfoProvider(resourceInfoProvider)))

info, err := r.GetAnchorInfo(interimDID)
require.Error(t, err)
require.Contains(t, err.Error(), errExpected.Error())
require.Nil(t, info)
})

t.Run("No anchor origin -> error", func(t *testing.T) {
resourceInfoProvider := newMockResourceInfoProvider().
withAnchorOrigin(nil).
withAnchorURI(anchorURI).
withCanonicalRef(canonicalRef)

r := restapi.NewAnchorInfoRetriever(registry.New(registry.WithResourceInfoProvider(resourceInfoProvider)))

info, err := r.GetAnchorInfo(interimDID)
require.Error(t, err)
require.Contains(t, err.Error(), "property required [anchorOrigin]")
require.Nil(t, info)
})

t.Run("No anchor URI -> error", func(t *testing.T) {
resourceInfoProvider := newMockResourceInfoProvider().
withAnchorOrigin(anchorOrigin).
withAnchorURI("").
withCanonicalRef(canonicalRef)

r := restapi.NewAnchorInfoRetriever(registry.New(registry.WithResourceInfoProvider(resourceInfoProvider)))

info, err := r.GetAnchorInfo(interimDID)
require.Error(t, err)
require.Contains(t, err.Error(), "property required [anchorURI]")
require.Nil(t, info)
})

t.Run("No canonical reference -> success", func(t *testing.T) {
resourceInfoProvider := newMockResourceInfoProvider().
withAnchorOrigin(anchorOrigin).
withAnchorURI(anchorURI).
withCanonicalRef(nil)

r := restapi.NewAnchorInfoRetriever(registry.New(registry.WithResourceInfoProvider(resourceInfoProvider)))

info, err := r.GetAnchorInfo(interimDID)
require.NoError(t, err)
require.NotNil(t, info)
require.Equal(t, anchorOrigin, info.AnchorOrigin)
require.Equal(t, anchorURI, info.AnchorURI)
require.Equal(t, "", info.CanonicalReference)
})

t.Run("Invalid canonical reference -> error", func(t *testing.T) {
resourceInfoProvider := newMockResourceInfoProvider().
withAnchorOrigin(anchorOrigin).
withAnchorURI(anchorURI).
withCanonicalRef(1000)

r := restapi.NewAnchorInfoRetriever(registry.New(registry.WithResourceInfoProvider(resourceInfoProvider)))

info, err := r.GetAnchorInfo(interimDID)
require.Error(t, err)
require.Contains(t, err.Error(), "could not assert property as a string [canonicalReference]")
require.Nil(t, info)
})
}
59 changes: 26 additions & 33 deletions pkg/discovery/endpoint/restapi/operations.go
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,10 @@ type anchorLinkStore interface {
GetLinks(anchorHash string) ([]*url.URL, error)
}

type anchorInfoRetriever interface {
GetAnchorInfo(resource string) (*AnchorInfo, error)
}

// New returns discovery operations.
func New(c *Config, p *Providers) (*Operation, error) {
u, err := url.Parse(c.BaseURL)
Expand All @@ -89,14 +93,16 @@ func New(c *Config, p *Providers) (*Operation, error) {
discoveryMinimumResolvers: c.DiscoveryMinimumResolvers,
discoveryDomains: c.DiscoveryDomains,
discoveryVctDomains: c.DiscoveryVctDomains,
resourceRegistry: p.ResourceRegistry,
anchorInfoRetriever: NewAnchorInfoRetriever(p.ResourceRegistry),
cas: p.CAS,
anchorStore: p.AnchorLinkStore,
}, nil
}

// Operation defines handlers for discovery operations.
type Operation struct {
anchorInfoRetriever

pubKey []byte
kid string
host string
Expand All @@ -109,7 +115,6 @@ type Operation struct {
discoveryDomains []string
discoveryVctDomains []string
discoveryMinimumResolvers int
resourceRegistry *registry.Registry
cas cas
anchorStore anchorLinkStore
}
Expand Down Expand Up @@ -273,55 +278,32 @@ func (o *Operation) writeResponseForResourceRequest(rw http.ResponseWriter, reso
}
}

func (o *Operation) getAnchorInfo(resource string) (interface{}, string, error) {
// TODO (#537): Show IPFS alternates if configured.
metadata, err := o.resourceRegistry.GetResourceInfo(resource)
if err != nil {
return "", "", fmt.Errorf("get info for resource [%s]: %w", resource, err)
}

anchorOrigin, ok := metadata[registry.AnchorOriginProperty]
if !ok {
return "", "", fmt.Errorf("anchor origin property missing from metadata for resource [%s]", resource)
}

anchorURIRaw, ok := metadata[registry.AnchorURIProperty]
if !ok {
return "", "", fmt.Errorf("anchor URI property missing from metadata for resource [%s]", resource)
}

anchorURI, ok := anchorURIRaw.(string)
if !ok {
return "", "", fmt.Errorf("anchor URI could not be asserted as a string for resource [%s]", resource)
}

return anchorOrigin, anchorURI, nil
}

func (o *Operation) handleDIDOrbQuery(rw http.ResponseWriter, resource string) {
anchorOrigin, anchorURI, err := o.getAnchorInfo(resource)
anchorInfo, err := o.GetAnchorInfo(resource)
if err != nil {
writeErrorResponse(rw, http.StatusInternalServerError,
fmt.Sprintf("failed to get info on %s: %s", resource, err.Error()))

return
}

did := getCanonicalDID(resource, anchorInfo.CanonicalReference)

resp := &JRD{
Properties: map[string]interface{}{
"https://trustbloc.dev/ns/anchor-origin": anchorOrigin,
"https://trustbloc.dev/ns/anchor-origin": anchorInfo.AnchorOrigin,
minResolvers: o.discoveryMinimumResolvers,
},
Links: []Link{
{
Rel: selfRelation,
Type: didLDJSONType,
Href: fmt.Sprintf("%s%s%s", o.baseURL, "/sidetree/v1/identifiers/", resource),
Href: fmt.Sprintf("%s%s%s", o.baseURL, "/sidetree/v1/identifiers/", did),
},
{
Rel: viaRelation,
Type: ldJSONType,
Href: anchorURI,
Href: anchorInfo.AnchorURI,
},
{
Rel: serviceRelation,
Expand All @@ -331,11 +313,11 @@ func (o *Operation) handleDIDOrbQuery(rw http.ResponseWriter, resource string) {
},
}

for _, discoveryDomain := range o.appendAlternateDomains(o.discoveryDomains, anchorURI) {
for _, discoveryDomain := range o.appendAlternateDomains(o.discoveryDomains, anchorInfo.AnchorURI) {
resp.Links = append(resp.Links, Link{
Rel: alternateRelation,
Type: didLDJSONType,
Href: fmt.Sprintf("%s%s%s", discoveryDomain, "/sidetree/v1/identifiers/", resource),
Href: fmt.Sprintf("%s%s%s", discoveryDomain, "/sidetree/v1/identifiers/", did),
})
}

Expand Down Expand Up @@ -601,3 +583,14 @@ func contains(strs []string, str string) bool {

return false
}

func getCanonicalDID(resource, canonicalRef string) string {
if canonicalRef != "" {
i := strings.LastIndex(resource, ":")
if i > 0 {
return fmt.Sprintf("did:orb:%s:%s", canonicalRef, resource[i+1:])
}
}

return resource
}
Loading