Repository: hyperledger/firefly
Pull request #1038 (2022-09-02 12:49:30)

[address-resolver-route] POST /verifiers/resolve
`POST /api/v1/verifiers/resolve`
Payload:

```json
{
  "type": "ethereum_address",
  "value": "hd-1-0-3"
}
```
Response

```json
{
  "type": "ethereum_address",
  "value": "0x7301ed0266c954309e62f76749d6c75a32c7c0b1"
}
```


Patch:
diff --git a/docs/swagger/swagger.yaml b/docs/swagger/swagger.yaml
index 4d0d243a2..a82c1428b 100644
--- a/docs/swagger/swagger.yaml
+++ b/docs/swagger/swagger.yaml
@@ -26787,6 +26787,65 @@ paths:
           description: ""
       tags:
       - Non-Default Namespace
+  /namespaces/{ns}/verifiers/resolve:
+    post:
+      description: Resolves an input key to a signing key
+      operationId: postVerifiersResolveNamespace
+      parameters:
+      - description: The namespace which scopes this request
+        in: path
+        name: ns
+        required: true
+        schema:
+          example: default
+          type: string
+      - description: Server-side request timeout (milliseconds, or set a custom suffix
+          like 10s)
+        in: header
+        name: Request-Timeout
+        schema:
+          default: 2m0s
+          type: string
+      requestBody:
+        content:
+          application/json:
+            schema:
+              properties:
+                type:
+                  description: The type of the verifier
+                  enum:
+                  - ethereum_address
+                  - fabric_msp_id
+                  - dx_peer_id
+                  type: string
+                value:
+                  description: The verifier string, such as an Ethereum address, or
+                    Fabric MSP identifier
+                  type: string
+              type: object
+      responses:
+        "200":
+          content:
+            application/json:
+              schema:
+                properties:
+                  type:
+                    description: The type of the verifier
+                    enum:
+                    - ethereum_address
+                    - fabric_msp_id
+                    - dx_peer_id
+                    type: string
+                  value:
+                    description: The verifier string, such as an Ethereum address,
+                      or Fabric MSP identifier
+                    type: string
+                type: object
+          description: Success
+        default:
+          description: ""
+      tags:
+      - Non-Default Namespace
   /network/action:
     post:
       description: Notify all nodes in the network of a new governance action
@@ -34224,6 +34283,58 @@ paths:
           description: ""
       tags:
       - Default Namespace
+  /verifiers/resolve:
+    post:
+      description: Resolves an input key to a signing key
+      operationId: postVerifiersResolve
+      parameters:
+      - description: Server-side request timeout (milliseconds, or set a custom suffix
+          like 10s)
+        in: header
+        name: Request-Timeout
+        schema:
+          default: 2m0s
+          type: string
+      requestBody:
+        content:
+          application/json:
+            schema:
+              properties:
+                type:
+                  description: The type of the verifier
+                  enum:
+                  - ethereum_address
+                  - fabric_msp_id
+                  - dx_peer_id
+                  type: string
+                value:
+                  description: The verifier string, such as an Ethereum address, or
+                    Fabric MSP identifier
+                  type: string
+              type: object
+      responses:
+        "200":
+          content:
+            application/json:
+              schema:
+                properties:
+                  type:
+                    description: The type of the verifier
+                    enum:
+                    - ethereum_address
+                    - fabric_msp_id
+                    - dx_peer_id
+                    type: string
+                  value:
+                    description: The verifier string, such as an Ethereum address,
+                      or Fabric MSP identifier
+                    type: string
+                type: object
+          description: Success
+        default:
+          description: ""
+      tags:
+      - Default Namespace
   /websockets:
     get:
       description: Gets a list of the current WebSocket connections to this node
diff --git a/internal/apiserver/route_post_verifiers_resolve.go b/internal/apiserver/route_post_verifiers_resolve.go
new file mode 100644
index 000000000..29091d06c
--- /dev/null
+++ b/internal/apiserver/route_post_verifiers_resolve.go
@@ -0,0 +1,42 @@
+// 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-common/pkg/ffapi"
+	"github.com/hyperledger/firefly/internal/coremsgs"
+	"github.com/hyperledger/firefly/pkg/core"
+)
+
+var postVerifiersResolve = &ffapi.Route{
+	Name:            "postVerifiersResolve",
+	Path:            "verifiers/resolve",
+	Method:          http.MethodPost,
+	PathParams:      nil,
+	QueryParams:     nil,
+	Description:     coremsgs.APIEndpointsPostVerifiersResolve,
+	JSONInputValue:  func() interface{} { return &core.VerifierRef{} },
+	JSONOutputValue: func() interface{} { return &core.VerifierRef{} },
+	JSONOutputCodes: []int{http.StatusOK},
+	Extensions: &coreExtensions{
+		CoreJSONHandler: func(r *ffapi.APIRequest, cr *coreRequest) (output interface{}, err error) {
+			return cr.or.Identity().ResolveInputSigningKey(cr.ctx, r.Input.(*core.VerifierRef))
+		},
+	},
+}
diff --git a/internal/apiserver/routes.go b/internal/apiserver/routes.go
index 5dc3d8609..30514d745 100644
--- a/internal/apiserver/routes.go
+++ b/internal/apiserver/routes.go
@@ -154,6 +154,7 @@ var routes = append(
 		postTokenTransfer,
 		putContractAPI,
 		putSubscription,
+		postVerifiersResolve,
 	})...,
 )
 
diff --git a/internal/coremsgs/en_api_translations.go b/internal/coremsgs/en_api_translations.go
index e91d82638..5ba86caf8 100644
--- a/internal/coremsgs/en_api_translations.go
+++ b/internal/coremsgs/en_api_translations.go
@@ -177,6 +177,7 @@ var (
 	APIEndpointsPutSubscription                 = ffm("api.endpoints.putSubscription", "Update an existing subscription")
 	APIEndpointsGetContractAPIInterface         = ffm("api.endpoints.getContractAPIInterface", "Gets a contract interface for a contract API")
 	APIEndpointsPostNetworkAction               = ffm("api.endpoints.postNetworkAction", "Notify all nodes in the network of a new governance action")
+	APIEndpointsPostVerifiersResolve            = ffm("api.endpoints.postVerifiersResolve", "Resolves an input key to a signing key")
 
 	APIFilterParamDesc         = ffm("api.filterParam", "Data filter field. Prefixes supported: > >= < <= @ ^ ! !@ !^")
 	APIFilterSortDesc          = ffm("api.filterSort", "Sort field. For multi-field sort use comma separated values (or multiple query values) with '-' prefix for descending")
diff --git a/internal/coremsgs/en_error_messages.go b/internal/coremsgs/en_error_messages.go
index 43acd4ee5..98beea603 100644
--- a/internal/coremsgs/en_error_messages.go
+++ b/internal/coremsgs/en_error_messages.go
@@ -266,4 +266,5 @@ var (
 	MsgCacheMissTTLKeyInternal            = ffe("FF10425", "could not initialize cache - ttl config key is not provided")
 	MsgCacheConfigKeyMismatchInternal     = ffe("FF10426", "could not initialize cache - '%s' and '%s' do not have identical prefix, mismatching prefixes are: '%s','%s'")
 	MsgCacheUnexpectedSizeKeyNameInternal = ffe("FF10427", "could not initialize cache - '%s' is not an expected size configuration key suffix. Expected values are: 'size', 'limit'")
+	MsgUnknownVerifierType                = ffe("FF10428", "Unknown verifier type", 400)
 )
diff --git a/internal/identity/identitymanager.go b/internal/identity/identitymanager.go
index 42bb170d8..7744aa2a3 100644
--- a/internal/identity/identitymanager.go
+++ b/internal/identity/identitymanager.go
@@ -40,6 +40,7 @@ const (
 
 type Manager interface {
 	ResolveInputSigningIdentity(ctx context.Context, signerRef *core.SignerRef) (err error)
+	ResolveInputSigningKey(ctx context.Context, inputKey *core.VerifierRef) (*core.VerifierRef, error)
 	NormalizeSigningKey(ctx context.Context, inputKey string, keyNormalizationMode int) (signingKey string, err error)
 	FindIdentityForVerifier(ctx context.Context, iTypes []core.IdentityType, verifier *core.VerifierRef) (identity *core.Identity, err error)
 	ResolveIdentitySigner(ctx context.Context, identity *core.Identity) (parentSigner *core.SignerRef, err error)
@@ -161,6 +162,34 @@ func (im *identityManager) NormalizeSigningKey(ctx context.Context, inputKey str
 	return signer.Value, nil
 }
 
+func (im *identityManager) ResolveInputSigningKey(ctx context.Context, inputKey *core.VerifierRef) (*core.VerifierRef, error) {
+	log.L(ctx).Debugf("Resolving input signing key: type='%s' value='%s'", inputKey.Type, inputKey.Value)
+
+	if im.blockchain == nil {
+		return nil, i18n.NewError(ctx, coremsgs.MsgBlockchainNotConfigured)
+	}
+
+	verifierType := inputKey.Type
+
+	if verifierType.String() == "" {
+		verifierType = im.blockchain.VerifierType()
+	}
+
+	if verifierType != im.blockchain.VerifierType() {
+		return nil, i18n.NewError(ctx, coremsgs.MsgUnknownVerifierType)
+	}
+
+	signingKey, err := im.blockchain.NormalizeSigningKey(ctx, inputKey.Value)
+	if err != nil {
+		return nil, err
+	}
+
+	return &core.VerifierRef{
+		Type:  verifierType,
+		Value: signingKey,
+	}, nil
+}
+
 // ResolveInputIdentity takes in blockchain signing input information from an API call (which may
 // include author or key or both), and updates it with fully resolved and normalized values
 func (im *identityManager) ResolveInputSigningIdentity(ctx context.Context, signerRef *core.SignerRef) (err error) {
diff --git a/internal/orchestrator/orchestrator.go b/internal/orchestrator/orchestrator.go
index 5497024c5..edfabc4c0 100644
--- a/internal/orchestrator/orchestrator.go
+++ b/internal/orchestrator/orchestrator.go
@@ -68,6 +68,7 @@ type Orchestrator interface {
 	Events() events.EventManager
 	NetworkMap() networkmap.Manager
 	Operations() operations.Manager
+	Identity() identity.Manager
 
 	// Status
 	GetStatus(ctx context.Context) (*core.NamespaceStatus, error)
@@ -356,6 +357,10 @@ func (or *orchestrator) MultiParty() multiparty.Manager {
 	return or.multiparty
 }
 
+func (or *orchestrator) Identity() identity.Manager {
+	return or.identity
+}
+
 func (or *orchestrator) initHandlers(ctx context.Context) (err error) {
 	or.plugins.Database.Plugin.SetHandler(or.namespace.Name, or)
 
diff --git a/mocks/identitymanagermocks/manager.go b/mocks/identitymanagermocks/manager.go
index 2ee76545c..693e46f76 100644
--- a/mocks/identitymanagermocks/manager.go
+++ b/mocks/identitymanagermocks/manager.go
@@ -249,6 +249,29 @@ func (_m *Manager) ResolveInputSigningIdentity(ctx context.Context, signerRef *c
 	return r0
 }
 
+// ResolveInputSigningKey provides a mock function with given fields: ctx, inputKey
+func (_m *Manager) ResolveInputSigningKey(ctx context.Context, inputKey *core.VerifierRef) (*core.VerifierRef, error) {
+	ret := _m.Called(ctx, inputKey)
+
+	var r0 *core.VerifierRef
+	if rf, ok := ret.Get(0).(func(context.Context, *core.VerifierRef) *core.VerifierRef); ok {
+		r0 = rf(ctx, inputKey)
+	} else {
+		if ret.Get(0) != nil {
+			r0 = ret.Get(0).(*core.VerifierRef)
+		}
+	}
+
+	var r1 error
+	if rf, ok := ret.Get(1).(func(context.Context, *core.VerifierRef) error); ok {
+		r1 = rf(ctx, inputKey)
+	} else {
+		r1 = ret.Error(1)
+	}
+
+	return r0, r1
+}
+
 // ValidateNodeOwner provides a mock function with given fields: ctx, node, _a2
 func (_m *Manager) ValidateNodeOwner(ctx context.Context, node *core.Identity, _a2 *core.Identity) (bool, error) {
 	ret := _m.Called(ctx, node, _a2)
diff --git a/mocks/orchestratormocks/orchestrator.go b/mocks/orchestratormocks/orchestrator.go
index d3ae3ef3a..d219a272d 100644
--- a/mocks/orchestratormocks/orchestrator.go
+++ b/mocks/orchestratormocks/orchestrator.go
@@ -24,6 +24,8 @@ import (
 
 	fftypes "github.com/hyperledger/firefly-common/pkg/fftypes"
 
+	identity "github.com/hyperledger/firefly/internal/identity"
+
 	mock "github.com/stretchr/testify/mock"
 
 	multiparty "github.com/hyperledger/firefly/internal/multiparty"
@@ -1168,6 +1170,22 @@ func (_m *Orchestrator) GetTransactions(ctx context.Context, filter database.And
 	return r0, r1, r2
 }
 
+// Identity provides a mock function with given fields:
+func (_m *Orchestrator) Identity() identity.Manager {
+	ret := _m.Called()
+
+	var r0 identity.Manager
+	if rf, ok := ret.Get(0).(func() identity.Manager); ok {
+		r0 = rf()
+	} else {
+		if ret.Get(0) != nil {
+			r0 = ret.Get(0).(identity.Manager)
+		}
+	}
+
+	return r0
+}
+
 // Init provides a mock function with given fields: ctx, cancelCtx
 func (_m *Orchestrator) Init(ctx context.Context, cancelCtx context.CancelFunc) error {
 	ret := _m.Called(ctx, cancelCtx)


Test patch:
diff --git a/internal/apiserver/route_post_verifiers_resolve_test.go b/internal/apiserver/route_post_verifiers_resolve_test.go
new file mode 100644
index 000000000..956571350
--- /dev/null
+++ b/internal/apiserver/route_post_verifiers_resolve_test.go
@@ -0,0 +1,48 @@
+// 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 (
+	"bytes"
+	"encoding/json"
+	"net/http/httptest"
+	"testing"
+
+	"github.com/hyperledger/firefly/mocks/identitymanagermocks"
+	"github.com/hyperledger/firefly/pkg/core"
+	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/mock"
+)
+
+func TestPostVerifiersResolve(t *testing.T) {
+	o, r := newTestAPIServer()
+	o.On("Authorize", mock.Anything, mock.Anything).Return(nil)
+	im := &identitymanagermocks.Manager{}
+	o.On("Identity").Return(im)
+	input := core.VerifierRef{}
+	var buf bytes.Buffer
+	json.NewEncoder(&buf).Encode(&input)
+	req := httptest.NewRequest("POST", "/api/v1/namespaces/ns1/verifiers/resolve", &buf)
+	req.Header.Set("Content-Type", "application/json; charset=utf-8")
+	res := httptest.NewRecorder()
+
+	im.On("ResolveInputSigningKey", mock.Anything, mock.AnythingOfType("*core.VerifierRef")).
+		Return(&core.VerifierRef{}, nil)
+	r.ServeHTTP(res, req)
+
+	assert.Equal(t, 200, res.Result().StatusCode)
+}
diff --git a/internal/identity/identitymanager_test.go b/internal/identity/identitymanager_test.go
index 9af0c70a4..7252661ae 100644
--- a/internal/identity/identitymanager_test.go
+++ b/internal/identity/identitymanager_test.go
@@ -69,6 +69,93 @@ func TestNewIdentityManagerMissingDeps(t *testing.T) {
 	assert.Regexp(t, "FF10128", err)
 }
 
+func TestResolveInputSigningKeyMissingBlockchain(t *testing.T) {
+	ctx, im := newTestIdentityManager(t)
+	im.blockchain = nil
+
+	inputKey := &core.VerifierRef{
+		Type:  "testType",
+		Value: "testValue",
+	}
+
+	_, err := im.ResolveInputSigningKey(ctx, inputKey)
+	assert.Regexp(t, "FF10417", err)
+}
+
+func TestResolveInputSigningKeyUnknownVerifier(t *testing.T) {
+	ctx, im := newTestIdentityManager(t)
+
+	mbi := im.blockchain.(*blockchainmocks.Plugin)
+	mbi.On("VerifierType").Return(core.VerifierTypeEthAddress, nil)
+
+	inputKey := &core.VerifierRef{
+		Type:  "unknownVerifier",
+		Value: "testValue",
+	}
+
+	_, err := im.ResolveInputSigningKey(ctx, inputKey)
+
+	assert.Regexp(t, "FF10428", err)
+	mbi.AssertExpectations(t)
+}
+
+func TestResolveInputSigningKeyNoVerifierOK(t *testing.T) {
+	ctx, im := newTestIdentityManager(t)
+
+	mbi := im.blockchain.(*blockchainmocks.Plugin)
+	mbi.On("VerifierType").Return(core.VerifierTypeEthAddress, nil)
+	mbi.On("NormalizeSigningKey", ctx, "testValue").Return("fullkey123", nil)
+
+	inputKey := &core.VerifierRef{
+		Value: "testValue",
+	}
+
+	res, err := im.ResolveInputSigningKey(ctx, inputKey)
+
+	assert.NoError(t, err)
+	assert.Equal(t, core.VerifierTypeEthAddress, res.Type)
+	assert.Equal(t, "fullkey123", res.Value)
+	mbi.AssertExpectations(t)
+}
+
+func TestResolveInputSigningKeyErrorNormalizingKey(t *testing.T) {
+	ctx, im := newTestIdentityManager(t)
+
+	mbi := im.blockchain.(*blockchainmocks.Plugin)
+	mbi.On("VerifierType").Return(core.VerifierTypeEthAddress, nil)
+	mbi.On("NormalizeSigningKey", ctx, "testValue").Return("", fmt.Errorf("pop"))
+
+	inputKey := &core.VerifierRef{
+		Type:  core.VerifierTypeEthAddress,
+		Value: "testValue",
+	}
+
+	_, err := im.ResolveInputSigningKey(ctx, inputKey)
+
+	assert.Regexp(t, "pop", err)
+	mbi.AssertExpectations(t)
+}
+
+func TestResolveInputSigningKeySuccess(t *testing.T) {
+	ctx, im := newTestIdentityManager(t)
+
+	mbi := im.blockchain.(*blockchainmocks.Plugin)
+	mbi.On("VerifierType").Return(core.VerifierTypeEthAddress, nil)
+	mbi.On("NormalizeSigningKey", ctx, "testValue").Return("fullkey123", nil)
+
+	inputKey := &core.VerifierRef{
+		Type:  core.VerifierTypeEthAddress,
+		Value: "testValue",
+	}
+
+	res, err := im.ResolveInputSigningKey(ctx, inputKey)
+
+	assert.NoError(t, err)
+	assert.Equal(t, core.VerifierTypeEthAddress, res.Type)
+	assert.Equal(t, "fullkey123", res.Value)
+	mbi.AssertExpectations(t)
+}
+
 func TestResolveInputSigningIdentityNoKey(t *testing.T) {
 
 	ctx, im := newTestIdentityManager(t)