[#414] ir/control: Implement service server

Implement `ControlServiceServer` on `Server` type. The `Server` requires all
requests to be signed with keys from the so-called whitelist. To obtain
health status, it uses the abstraction in the form of `HealthChecker`
interface.

Signed-off-by: Leonard Lyubich <leonard@nspcc.ru>
This commit is contained in:
Leonard Lyubich 2021-06-09 18:40:02 +03:00 committed by Alex Vanin
parent 93803b1a90
commit dcfe9a6504
6 changed files with 200 additions and 0 deletions

View file

@ -0,0 +1,34 @@
package control
import (
"context"
control "github.com/nspcc-dev/neofs-node/pkg/services/control/ir"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
)
// HealthCheck returns health status of the local IR node.
//
// If request is not signed with a key from white list, permission error returns.
func (s *Server) HealthCheck(_ context.Context, req *control.HealthCheckRequest) (*control.HealthCheckResponse, error) {
// verify request
if err := s.isValidRequest(req); err != nil {
return nil, status.Error(codes.PermissionDenied, err.Error())
}
// create and fill response
resp := new(control.HealthCheckResponse)
body := new(control.HealthCheckResponse_Body)
resp.SetBody(body)
body.SetHealthStatus(s.prm.healthChecker.HealthStatus())
// sign the response
if err := SignMessage(s.prm.key, resp); err != nil {
return nil, status.Error(codes.Internal, err.Error())
}
return resp, nil
}

View file

@ -0,0 +1,13 @@
package control
import control "github.com/nspcc-dev/neofs-node/pkg/services/control/ir"
// HealthChecker is component interface for calculating
// the current health status of a node.
type HealthChecker interface {
// Must calculate and return current health status of the IR application.
//
// If status can not be calculated for any reason,
// control.HealthStatus_HEALTH_STATUS_UNDEFINED should be returned.
HealthStatus() control.HealthStatus
}

View file

@ -0,0 +1,20 @@
package control
// Option specifies Server's optional parameter.
type Option func(*options)
type options struct {
allowedKeys [][]byte
}
func defaultOptions() *options {
return new(options)
}
// WithAllowedKeys returns option to add public keys
// to white list of the Control service.
func WithAllowedKeys(keys [][]byte) Option {
return func(o *options) {
o.allowedKeys = append(o.allowedKeys, keys...)
}
}

View file

@ -0,0 +1,24 @@
package control
import (
"crypto/ecdsa"
)
// Prm groups required parameters of
// Server's constructor.
type Prm struct {
key *ecdsa.PrivateKey
healthChecker HealthChecker
}
// SetPrivateKey sets private key to sign responses.
func (x *Prm) SetPrivateKey(key *ecdsa.PrivateKey) {
x.key = key
}
// SetHealthChecker sets HealthChecker to calculate
// health status.
func (x *Prm) SetHealthChecker(hc HealthChecker) {
x.healthChecker = hc
}

View file

@ -0,0 +1,55 @@
package control
import (
"fmt"
crypto "github.com/nspcc-dev/neofs-crypto"
)
// Server is an entity that serves
// Control service on IR node.
//
// To gain access to the service, any request must be
// signed with a key from the white list.
type Server struct {
prm Prm
allowedKeys [][]byte
}
func panicOnPrmValue(n string, v interface{}) {
const invalidPrmValFmt = "invalid %s parameter (%T): %v"
panic(fmt.Sprintf(invalidPrmValFmt, n, v, v))
}
// New creates a new instance of the Server.
//
// Panics if:
// - parameterized private key is nil;
// - parameterized HealthChecker is nil.
//
// Forms white list from all keys specified via
// WithAllowedKeys option and a public key of
// the parameterized private key.
func New(prm Prm, opts ...Option) *Server {
// verify required parameters
switch {
case prm.key == nil:
panicOnPrmValue("key", prm.key)
case prm.healthChecker == nil:
panicOnPrmValue("health checker", prm.healthChecker)
}
// compute optional parameters
o := defaultOptions()
for _, opt := range opts {
opt(o)
}
return &Server{
prm: prm,
allowedKeys: append(o.allowedKeys, crypto.MarshalPublicKey(&prm.key.PublicKey)),
}
}

View file

@ -0,0 +1,54 @@
package control
import (
"bytes"
"crypto/ecdsa"
"errors"
"github.com/nspcc-dev/neofs-api-go/util/signature"
control "github.com/nspcc-dev/neofs-node/pkg/services/control/ir"
)
// SignedMessage is an interface of Control service message.
type SignedMessage interface {
signature.DataSource
GetSignature() *control.Signature
SetSignature(*control.Signature)
}
var errDisallowedKey = errors.New("key is not in the allowed list")
func (s *Server) isValidRequest(req SignedMessage) error {
var (
sign = req.GetSignature()
key = sign.GetKey()
allowed = false
)
// check if key is allowed
for i := range s.allowedKeys {
if allowed = bytes.Equal(s.allowedKeys[i], key); allowed {
break
}
}
if !allowed {
return errDisallowedKey
}
// verify signature
return signature.VerifyDataWithSource(req, func() ([]byte, []byte) {
return key, sign.GetSign()
})
}
// SignMessage signs Control service message with private key.
func SignMessage(key *ecdsa.PrivateKey, msg SignedMessage) error {
return signature.SignDataWithHandler(key, msg, func(key []byte, sig []byte) {
s := new(control.Signature)
s.SetKey(key)
s.SetSign(sig)
msg.SetSignature(s)
})
}