diff --git a/docs/swagger/swagger.yaml b/docs/swagger/swagger.yaml index 218a54cbbd..ba0bc5d61c 100644 --- a/docs/swagger/swagger.yaml +++ b/docs/swagger/swagger.yaml @@ -9069,6 +9069,61 @@ paths: description: Success default: description: "" + /network/identities/{did}: + get: + description: 'TODO: Description' + operationId: getIdentityByDID + parameters: + - description: 'TODO: Description' + in: path + name: did + required: true + schema: + type: string + - description: Server-side request timeout (millseconds, or set a custom suffix + like 10s) + in: header + name: Request-Timeout + schema: + default: 120s + type: string + responses: + "200": + content: + application/json: + schema: + properties: + created: {} + description: + type: string + did: + type: string + id: {} + messages: + properties: + claim: {} + update: {} + verification: {} + type: object + name: + type: string + namespace: + type: string + parent: {} + profile: + additionalProperties: {} + type: object + type: + enum: + - org + - node + - custom + type: string + updated: {} + type: object + description: Success + default: + description: "" /network/nodes: get: description: 'TODO: Description' @@ -9542,7 +9597,8 @@ paths: type: string name: type: string - parent: {} + parent: + type: string profile: additionalProperties: {} type: object diff --git a/internal/apiserver/route_get_net_did.go b/internal/apiserver/route_get_net_did.go new file mode 100644 index 0000000000..d7cfa1bdd1 --- /dev/null +++ b/internal/apiserver/route_get_net_did.go @@ -0,0 +1,43 @@ +// Copyright © 2022 Kaleido, Inc. +// +// SPDX-License-Identifier: Apache-2.0 +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package apiserver + +import ( + "net/http" + + "github.com/hyperledger/firefly/internal/i18n" + "github.com/hyperledger/firefly/internal/oapispec" + "github.com/hyperledger/firefly/pkg/fftypes" +) + +var getIdentityByDID = &oapispec.Route{ + Name: "getIdentityByDID", + Path: "network/identities/{did:.+}", + Method: http.MethodGet, + PathParams: []*oapispec.PathParam{ + {Name: "did", Description: i18n.MsgTBD}, + }, + FilterFactory: nil, + Description: i18n.MsgTBD, + JSONInputValue: nil, + JSONOutputValue: func() interface{} { return &fftypes.Identity{} }, + JSONOutputCodes: []int{http.StatusOK}, + JSONHandler: func(r *oapispec.APIRequest) (output interface{}, err error) { + output, err = getOr(r.Ctx).NetworkMap().GetIdentityByDID(r.Ctx, r.PP["did"]) + return output, err + }, +} diff --git a/internal/apiserver/route_get_net_did_test.go b/internal/apiserver/route_get_net_did_test.go new file mode 100644 index 0000000000..a0eb5c4968 --- /dev/null +++ b/internal/apiserver/route_get_net_did_test.go @@ -0,0 +1,42 @@ +// Copyright © 2021 Kaleido, Inc. +// +// SPDX-License-Identifier: Apache-2.0 +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package apiserver + +import ( + "net/http/httptest" + "testing" + + "github.com/hyperledger/firefly/mocks/networkmapmocks" + "github.com/hyperledger/firefly/pkg/fftypes" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" +) + +func TestGetIdentityByDID(t *testing.T) { + o, r := newTestAPIServer() + nmn := &networkmapmocks.Manager{} + o.On("NetworkMap").Return(nmn) + req := httptest.NewRequest("GET", "/api/v1/network/identities/did:firefly:org/org_1", nil) + req.Header.Set("Content-Type", "application/json; charset=utf-8") + res := httptest.NewRecorder() + + nmn.On("GetIdentityByDID", mock.Anything, "did:firefly:org/org_1"). + Return(&fftypes.Identity{}, nil) + r.ServeHTTP(res, req) + + assert.Equal(t, 200, res.Result().StatusCode) +} diff --git a/internal/apiserver/routes.go b/internal/apiserver/routes.go index 560c4b13a9..ff6a6e4989 100644 --- a/internal/apiserver/routes.go +++ b/internal/apiserver/routes.go @@ -47,6 +47,7 @@ var routes = []*oapispec.Route{ getGroups, getIdentities, getIdentityByID, + getIdentityByDID, getIdentityDID, getIdentityVerifiers, getMsgByID, diff --git a/internal/networkmap/data_query.go b/internal/networkmap/data_query.go index f2572318d5..d150c6a5f3 100644 --- a/internal/networkmap/data_query.go +++ b/internal/networkmap/data_query.go @@ -96,6 +96,17 @@ func (nm *networkMap) GetIdentityByID(ctx context.Context, ns, id string) (*ffty return identity, nil } +func (nm *networkMap) GetIdentityByDID(ctx context.Context, did string) (*fftypes.Identity, error) { + identity, _, err := nm.identity.CachedIdentityLookup(ctx, did) + if err != nil { + return nil, err + } + if identity == nil { + return nil, i18n.NewError(ctx, i18n.Msg404NotFound) + } + return identity, nil +} + func (nm *networkMap) GetIdentities(ctx context.Context, ns string, filter database.AndFilter) ([]*fftypes.Identity, *database.FilterResult, error) { filter.Condition(filter.Builder().Eq("namespace", ns)) return nm.database.GetIdentities(ctx, filter) diff --git a/internal/networkmap/data_query_test.go b/internal/networkmap/data_query_test.go index 6644766a69..364a270062 100644 --- a/internal/networkmap/data_query_test.go +++ b/internal/networkmap/data_query_test.go @@ -21,6 +21,7 @@ import ( "testing" "github.com/hyperledger/firefly/mocks/databasemocks" + "github.com/hyperledger/firefly/mocks/identitymanagermocks" "github.com/hyperledger/firefly/pkg/database" "github.com/hyperledger/firefly/pkg/fftypes" "github.com/stretchr/testify/assert" @@ -286,3 +287,33 @@ func TestGetVerifierByHashBadUUID(t *testing.T) { _, err := nm.GetVerifierByHash(nm.ctx, "ns1", "bad") assert.Regexp(t, "FF10232", err) } + +func TestGetVerifierByDIDOk(t *testing.T) { + nm, cancel := newTestNetworkmap(t) + defer cancel() + nm.identity.(*identitymanagermocks.Manager).On("CachedIdentityLookup", nm.ctx, "did:firefly:org/abc"). + Return(testOrg("abc"), true, nil) + id, err := nm.GetIdentityByDID(nm.ctx, "did:firefly:org/abc") + assert.NoError(t, err) + assert.Equal(t, "did:firefly:org/abc", id.DID) +} + +func TestGetVerifierByDIDNotFound(t *testing.T) { + nm, cancel := newTestNetworkmap(t) + defer cancel() + nm.identity.(*identitymanagermocks.Manager).On("CachedIdentityLookup", nm.ctx, "did:firefly:org/abc"). + Return(nil, true, nil) + id, err := nm.GetIdentityByDID(nm.ctx, "did:firefly:org/abc") + assert.Regexp(t, "FF10109", err) + assert.Nil(t, id) +} + +func TestGetVerifierByDIDNotErr(t *testing.T) { + nm, cancel := newTestNetworkmap(t) + defer cancel() + nm.identity.(*identitymanagermocks.Manager).On("CachedIdentityLookup", nm.ctx, "did:firefly:org/abc"). + Return(nil, true, fmt.Errorf("pop")) + id, err := nm.GetIdentityByDID(nm.ctx, "did:firefly:org/abc") + assert.Regexp(t, "pop", err) + assert.Nil(t, id) +} diff --git a/internal/networkmap/manager.go b/internal/networkmap/manager.go index 875652eb94..09bf6e2faf 100644 --- a/internal/networkmap/manager.go +++ b/internal/networkmap/manager.go @@ -40,6 +40,7 @@ type Manager interface { GetNodeByNameOrID(ctx context.Context, nameOrID string) (*fftypes.Identity, error) GetNodes(ctx context.Context, filter database.AndFilter) ([]*fftypes.Identity, *database.FilterResult, error) GetIdentityByID(ctx context.Context, ns string, id string) (*fftypes.Identity, error) + GetIdentityByDID(ctx context.Context, did string) (*fftypes.Identity, error) GetIdentities(ctx context.Context, ns string, filter database.AndFilter) ([]*fftypes.Identity, *database.FilterResult, error) GetIdentityVerifiers(ctx context.Context, ns, id string, filter database.AndFilter) ([]*fftypes.Verifier, *database.FilterResult, error) GetVerifiers(ctx context.Context, ns string, filter database.AndFilter) ([]*fftypes.Verifier, *database.FilterResult, error) diff --git a/internal/networkmap/register_identity.go b/internal/networkmap/register_identity.go index a2956abfe4..8cd0fb5aea 100644 --- a/internal/networkmap/register_identity.go +++ b/internal/networkmap/register_identity.go @@ -25,6 +25,20 @@ import ( func (nm *networkMap) RegisterIdentity(ctx context.Context, ns string, dto *fftypes.IdentityCreateDTO, waitConfirm bool) (identity *fftypes.Identity, err error) { + // The parent can be a UUID directly + var parent *fftypes.UUID + if dto.Parent != "" { + parent, err = fftypes.ParseUUID(ctx, dto.Parent) + if err != nil { + // Or a DID + parentIdentity, _, err := nm.identity.CachedIdentityLookup(ctx, dto.Parent) + if err != nil { + return nil, err + } + parent = parentIdentity.ID + } + } + // Parse the input DTO identity = &fftypes.Identity{ IdentityBase: fftypes.IdentityBase{ @@ -32,7 +46,7 @@ func (nm *networkMap) RegisterIdentity(ctx context.Context, ns string, dto *ffty Namespace: ns, Name: dto.Name, Type: dto.Type, - Parent: dto.Parent, + Parent: parent, }, IdentityProfile: fftypes.IdentityProfile{ Description: dto.Description, diff --git a/internal/networkmap/register_identity_test.go b/internal/networkmap/register_identity_test.go index 1ae41c53a7..f578e43af7 100644 --- a/internal/networkmap/register_identity_test.go +++ b/internal/networkmap/register_identity_test.go @@ -66,7 +66,7 @@ func TestRegisterIdentityOrgWithParentOk(t *testing.T) { org, err := nm.RegisterIdentity(nm.ctx, fftypes.SystemNamespace, &fftypes.IdentityCreateDTO{ Name: "child1", Key: "0x12345", - Parent: fftypes.NewUUID(), + Parent: fftypes.NewUUID().String(), }, false) assert.NoError(t, err) assert.Equal(t, *mockMsg1.Header.ID, *org.Messages.Claim) @@ -124,7 +124,7 @@ func TestRegisterIdentityOrgWithParentWaitConfirmOk(t *testing.T) { _, err := nm.RegisterIdentity(nm.ctx, fftypes.SystemNamespace, &fftypes.IdentityCreateDTO{ Name: "child1", Key: "0x12345", - Parent: fftypes.NewUUID(), + Parent: fftypes.NewUUID().String(), }, true) assert.NoError(t, err) @@ -142,6 +142,12 @@ func TestRegisterIdentityCustomWithParentFail(t *testing.T) { mim := nm.identity.(*identitymanagermocks.Manager) mim.On("VerifyIdentityChain", nm.ctx, mock.AnythingOfType("*fftypes.Identity")).Return(parentIdentity, false, nil) + mim.On("CachedIdentityLookup", nm.ctx, "did:firefly:org/parent1").Return(&fftypes.Identity{ + IdentityBase: fftypes.IdentityBase{ + ID: fftypes.NewUUID(), + DID: "did:firefly:org/parent1", + }, + }, false, nil) mim.On("ResolveIdentitySigner", nm.ctx, parentIdentity).Return(&fftypes.SignerRef{ Key: "0x23456", }, nil) @@ -168,7 +174,7 @@ func TestRegisterIdentityCustomWithParentFail(t *testing.T) { _, err := nm.RegisterIdentity(nm.ctx, "ns1", &fftypes.IdentityCreateDTO{ Name: "custom1", Key: "0x12345", - Parent: fftypes.NewUUID(), + Parent: "did:firefly:org/parent1", }, false) assert.Regexp(t, "pop", err) @@ -190,7 +196,7 @@ func TestRegisterIdentityGetParentMsgFail(t *testing.T) { _, err := nm.RegisterIdentity(nm.ctx, "ns1", &fftypes.IdentityCreateDTO{ Name: "custom1", Key: "0x12345", - Parent: fftypes.NewUUID(), + Parent: fftypes.NewUUID().String(), }, false) assert.Regexp(t, "pop", err) @@ -215,8 +221,9 @@ func TestRegisterIdentityRootBroadcastFail(t *testing.T) { fftypes.SystemTagIdentityClaim, false).Return(nil, fmt.Errorf("pop")) _, err := nm.RegisterIdentity(nm.ctx, "ns1", &fftypes.IdentityCreateDTO{ - Name: "custom1", - Key: "0x12345", + Name: "custom1", + Key: "0x12345", + Parent: fftypes.NewUUID().String(), }, false) assert.Regexp(t, "pop", err) @@ -233,7 +240,8 @@ func TestRegisterIdentityMissingKey(t *testing.T) { mim.On("VerifyIdentityChain", nm.ctx, mock.AnythingOfType("*fftypes.Identity")).Return(nil, false, nil) _, err := nm.RegisterIdentity(nm.ctx, "ns1", &fftypes.IdentityCreateDTO{ - Name: "custom1", + Name: "custom1", + Parent: fftypes.NewUUID().String(), }, false) assert.Regexp(t, "FF10352", err) @@ -249,7 +257,25 @@ func TestRegisterIdentityVerifyFail(t *testing.T) { mim.On("VerifyIdentityChain", nm.ctx, mock.AnythingOfType("*fftypes.Identity")).Return(nil, false, fmt.Errorf("pop")) _, err := nm.RegisterIdentity(nm.ctx, "ns1", &fftypes.IdentityCreateDTO{ - Name: "custom1", + Name: "custom1", + Parent: fftypes.NewUUID().String(), + }, false) + assert.Regexp(t, "pop", err) + + mim.AssertExpectations(t) +} + +func TestRegisterIdentityBadParent(t *testing.T) { + + nm, cancel := newTestNetworkmap(t) + defer cancel() + + mim := nm.identity.(*identitymanagermocks.Manager) + mim.On("CachedIdentityLookup", nm.ctx, "did:firefly:org/1").Return(nil, false, fmt.Errorf("pop")) + + _, err := nm.RegisterIdentity(nm.ctx, "ns1", &fftypes.IdentityCreateDTO{ + Name: "custom1", + Parent: "did:firefly:org/1", }, false) assert.Regexp(t, "pop", err) diff --git a/internal/networkmap/register_node.go b/internal/networkmap/register_node.go index f860f86b01..12c106ca93 100644 --- a/internal/networkmap/register_node.go +++ b/internal/networkmap/register_node.go @@ -32,7 +32,7 @@ func (nm *networkMap) RegisterNode(ctx context.Context, waitConfirm bool) (ident } nodeRequest := &fftypes.IdentityCreateDTO{ - Parent: nodeOwningOrg.ID, + Parent: nodeOwningOrg.ID.String(), Name: config.GetString(config.NodeName), Type: fftypes.IdentityTypeNode, IdentityProfile: fftypes.IdentityProfile{ diff --git a/internal/oapispec/openapi3.go b/internal/oapispec/openapi3.go index fbd53e5837..1c95cdd095 100644 --- a/internal/oapispec/openapi3.go +++ b/internal/oapispec/openapi3.go @@ -23,6 +23,7 @@ import ( "log" "net/http" "reflect" + "regexp" "sort" "strconv" "strings" @@ -41,6 +42,8 @@ type SwaggerGenConfig struct { Description string } +var customRegexRemoval = regexp.MustCompile(`{(\w+)\:[^}]+}`) + func SwaggerGen(ctx context.Context, routes []*Route, conf *SwaggerGenConfig) *openapi3.T { doc := &openapi3.T{ @@ -72,6 +75,7 @@ func getPathItem(doc *openapi3.T, path string) *openapi3.PathItem { if !strings.HasPrefix(path, "/") { path = "/" + path } + path = customRegexRemoval.ReplaceAllString(path, `{$1}`) if doc.Paths == nil { doc.Paths = openapi3.Paths{} } diff --git a/internal/oapispec/openapi3_test.go b/internal/oapispec/openapi3_test.go index 6fd85ce061..f9b3afe587 100644 --- a/internal/oapispec/openapi3_test.go +++ b/internal/oapispec/openapi3_test.go @@ -178,3 +178,24 @@ func TestBadCustomSchema(t *testing.T) { }) }) } + +func TestWildcards(t *testing.T) { + + config.Reset() + routes := []*Route{ + { + Name: "op1", + Path: "namespaces/{ns}/example1/{id:.*wildcard.*}", + Method: http.MethodPost, + JSONInputValue: func() interface{} { return &fftypes.Message{} }, + JSONInputMask: []string{"id"}, + JSONOutputCodes: []int{http.StatusOK}, + }, + } + swagger := SwaggerGen(context.Background(), routes, &SwaggerGenConfig{ + Title: "UnitTest", + Version: "1.0", + BaseURL: "http://localhost:12345/api/v1", + }) + assert.NotNil(t, swagger.Paths["/namespaces/{ns}/example1/{id}"]) +} diff --git a/mocks/networkmapmocks/manager.go b/mocks/networkmapmocks/manager.go index 24618e70cb..77105d8e79 100644 --- a/mocks/networkmapmocks/manager.go +++ b/mocks/networkmapmocks/manager.go @@ -73,6 +73,29 @@ func (_m *Manager) GetIdentities(ctx context.Context, ns string, filter database return r0, r1, r2 } +// GetIdentityByDID provides a mock function with given fields: ctx, did +func (_m *Manager) GetIdentityByDID(ctx context.Context, did string) (*fftypes.Identity, error) { + ret := _m.Called(ctx, did) + + var r0 *fftypes.Identity + if rf, ok := ret.Get(0).(func(context.Context, string) *fftypes.Identity); ok { + r0 = rf(ctx, did) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*fftypes.Identity) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(context.Context, string) error); ok { + r1 = rf(ctx, did) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + // GetIdentityByID provides a mock function with given fields: ctx, ns, id func (_m *Manager) GetIdentityByID(ctx context.Context, ns string, id string) (*fftypes.Identity, error) { ret := _m.Called(ctx, ns, id) diff --git a/pkg/fftypes/identity.go b/pkg/fftypes/identity.go index e0f17d164d..5d3cab7055 100644 --- a/pkg/fftypes/identity.go +++ b/pkg/fftypes/identity.go @@ -82,7 +82,7 @@ type Identity struct { type IdentityCreateDTO struct { Name string `json:"name"` Type IdentityType `json:"type,omitempty"` - Parent *UUID `json:"parent,omitempty"` + Parent string `json:"parent,omitempty"` // can be a DID for resolution, or the UUID directly Key string `json:"key,omitempty"` IdentityProfile } diff --git a/test/e2e/restclient_test.go b/test/e2e/restclient_test.go index c5210f5a31..19976e424e 100644 --- a/test/e2e/restclient_test.go +++ b/test/e2e/restclient_test.go @@ -263,7 +263,7 @@ func ClaimCustomIdentity(t *testing.T, client *resty.Client, key, name, desc str SetBody(fftypes.IdentityCreateDTO{ Name: name, Type: fftypes.IdentityTypeCustom, - Parent: parent, + Parent: parent.String(), Key: key, IdentityProfile: fftypes.IdentityProfile{ Description: desc,