forked from TrueCloudLab/frostfs-node
[#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:
parent
93803b1a90
commit
dcfe9a6504
6 changed files with 200 additions and 0 deletions
34
pkg/services/control/ir/server/calls.go
Normal file
34
pkg/services/control/ir/server/calls.go
Normal 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
|
||||||
|
}
|
13
pkg/services/control/ir/server/deps.go
Normal file
13
pkg/services/control/ir/server/deps.go
Normal 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
|
||||||
|
}
|
20
pkg/services/control/ir/server/opts.go
Normal file
20
pkg/services/control/ir/server/opts.go
Normal 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...)
|
||||||
|
}
|
||||||
|
}
|
24
pkg/services/control/ir/server/prm.go
Normal file
24
pkg/services/control/ir/server/prm.go
Normal 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
|
||||||
|
}
|
55
pkg/services/control/ir/server/server.go
Normal file
55
pkg/services/control/ir/server/server.go
Normal 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)),
|
||||||
|
}
|
||||||
|
}
|
54
pkg/services/control/ir/server/sign.go
Normal file
54
pkg/services/control/ir/server/sign.go
Normal 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)
|
||||||
|
})
|
||||||
|
}
|
Loading…
Reference in a new issue