From 3e2169be84799d6e791645c9eb14bcb5f1663c48 Mon Sep 17 00:00:00 2001 From: Leonard Lyubich Date: Thu, 14 Sep 2023 21:26:59 +0400 Subject: [PATCH] innerring: Prepare a platform for private attributes of storage nodes Private node attribute are coming. The term private means that the system controls access to the declaration of certain attributes by storage nodes. This feature will allow you to prevent unauthorized installation of attributes that semantically require confirmation. Until now, storage nodes were free to set format-valid attributes. Therefore, to prepare for the feature arrival, a new attribute validator (`netmap.NodeValidator`) is introduced: it fakes the real validation and allows all nodes to access all attributes. In the future, verification will be tightened. Refs #2280. Signed-off-by: Leonard Lyubich --- pkg/innerring/innerring.go | 4 + pkg/innerring/netmap.go | 9 + .../nodevalidation/attributes/validator.go | 128 ++++++++++++ .../attributes/validator_test.go | 196 ++++++++++++++++++ 4 files changed, 337 insertions(+) create mode 100644 pkg/innerring/processors/netmap/nodevalidation/attributes/validator.go create mode 100644 pkg/innerring/processors/netmap/nodevalidation/attributes/validator_test.go diff --git a/pkg/innerring/innerring.go b/pkg/innerring/innerring.go index c97fe312b7d..6651459060a 100644 --- a/pkg/innerring/innerring.go +++ b/pkg/innerring/innerring.go @@ -23,6 +23,7 @@ import ( "github.com/nspcc-dev/neofs-node/pkg/innerring/processors/neofs" "github.com/nspcc-dev/neofs-node/pkg/innerring/processors/netmap" nodevalidator "github.com/nspcc-dev/neofs-node/pkg/innerring/processors/netmap/nodevalidation" + attributevalidator "github.com/nspcc-dev/neofs-node/pkg/innerring/processors/netmap/nodevalidation/attributes" availabilityvalidator "github.com/nspcc-dev/neofs-node/pkg/innerring/processors/netmap/nodevalidation/availability" statevalidation "github.com/nspcc-dev/neofs-node/pkg/innerring/processors/netmap/nodevalidation/state" addrvalidator "github.com/nspcc-dev/neofs-node/pkg/innerring/processors/netmap/nodevalidation/structure" @@ -730,6 +731,8 @@ func New(ctx context.Context, log *zap.Logger, cfg *viper.Viper, errChan chan<- var netMapCandidateStateValidator statevalidation.NetMapCandidateValidator netMapCandidateStateValidator.SetNetworkSettings(netSettings) + var attributeAccessCtrl publicNodeAttributesAccessController + // create netmap processor server.netmapProcessor, err = netmap.New(&netmap.Params{ Log: log, @@ -755,6 +758,7 @@ func New(ctx context.Context, log *zap.Logger, cfg *viper.Viper, errChan chan<- &netMapCandidateStateValidator, addrvalidator.New(), availabilityvalidator.New(), + attributevalidator.New(attributeAccessCtrl), locodeValidator, ), NodeStateSettings: netSettings, diff --git a/pkg/innerring/netmap.go b/pkg/innerring/netmap.go index 03b13b05305..2faad6cc289 100644 --- a/pkg/innerring/netmap.go +++ b/pkg/innerring/netmap.go @@ -5,6 +5,7 @@ import ( "github.com/nspcc-dev/neofs-node/pkg/innerring/processors/netmap/nodevalidation/state" netmapclient "github.com/nspcc-dev/neofs-node/pkg/morph/client/netmap" + neofscrypto "github.com/nspcc-dev/neofs-sdk-go/crypto" ) /* @@ -27,3 +28,11 @@ func (s *networkSettings) MaintenanceModeAllowed() error { return state.ErrMaintenanceModeDisallowed } + +// [attributevalidator.AttributeAccessController] that allows all nodes to +// freely use any attributes. +type publicNodeAttributesAccessController struct{} + +func (x publicNodeAttributesAccessController) CheckNodeAccessToAttribute(_ neofscrypto.PublicKey, _, _ string) error { + return nil +} diff --git a/pkg/innerring/processors/netmap/nodevalidation/attributes/validator.go b/pkg/innerring/processors/netmap/nodevalidation/attributes/validator.go new file mode 100644 index 00000000000..f6f76917f4c --- /dev/null +++ b/pkg/innerring/processors/netmap/nodevalidation/attributes/validator.go @@ -0,0 +1,128 @@ +package attributevalidator + +import ( + "errors" + "fmt" + "unicode/utf8" + + neofscrypto "github.com/nspcc-dev/neofs-sdk-go/crypto" + neofsecdsa "github.com/nspcc-dev/neofs-sdk-go/crypto/ecdsa" + "github.com/nspcc-dev/neofs-sdk-go/netmap" +) + +// ErrAccessDenied is returned when access is denied. +var ErrAccessDenied = errors.New("node is not allowed to use attribute") + +// AttributeAccessController provides access control over the use of attributes +// by storage nodes. +type AttributeAccessController interface { + // CheckNodeAccessToAttribute checks whether storage node with the given key is + // allowed to have specified key-value attribute or not. Returns + // [ErrAccessDenied] when access is denied, or any other error encountered which + // prevented access check. + // + // Key to the attribute is a non-empty UTF-8 string. Value of the attribute is + // any non-empty string. + CheckNodeAccessToAttribute(nodeKey neofscrypto.PublicKey, attrKey, attrValue string) error +} + +// Validator validates node attributes declared by the storage nodes on their +// attempts to enter the NeoFS network map. +type Validator struct { + ctrl AttributeAccessController +} + +// New returns new Validator that checks storage nodes against provided +// [AttributeAccessController]. +func New(ctrl AttributeAccessController) *Validator { + return &Validator{ + ctrl: ctrl, + } +} + +// various errors useful for testing. +var ( + errInvalidNodeBinaryKey = errors.New("invalid node binary key") + errEmptyAttributeKey = errors.New("empty attribute key") + errEmptyAttributeValue = errors.New("empty value of the attribute") + errNonUTF8AttributeKey = errors.New("attribute key is not a valid UTF-8 string") + errDuplicatedAttributeKey = errors.New("duplicated attribute key") +) + +// VerifyAndUpdate checks allowance of the storage node represented by the given +// descriptor to use declared attributes. Returns an error if at least one of +// the attributes is forbidden to be used by the storage node or access check +// cannot be done at the moment. +// +// VerifyAndUpdate also pre-checks following NeoFS protocol requirements: +// - public key binary must be a compressed ECDSA public key +// - all keys must be non-empty unique valid UTF-8 string +// - all values must be non-empty strings +// +// VerifyAndUpdate does not mutate the argument. +func (x *Validator) VerifyAndUpdate(info *netmap.NodeInfo) error { + return x.verifyAndUpdate(info) +} + +// interface of [netmap.NodeInfo] used by [Validator] to bypass SDK protection +// to compose obviously invalid instance. Needed for testing only. +type nodeInfo interface { + PublicKey() []byte + NumberOfAttributes() int + IterateAttributes(func(string, string)) +} + +func (x *Validator) verifyAndUpdate(n nodeInfo) error { + var nodePubKey neofsecdsa.PublicKey + + err := nodePubKey.Decode(n.PublicKey()) + if err != nil { + return fmt.Errorf("%w: %v", errInvalidNodeBinaryKey, err) + } + + mAttr := make(map[string]string, n.NumberOfAttributes()) + + n.IterateAttributes(func(key, value string) { + if err != nil { + return + } + + if key == "" { + err = errEmptyAttributeKey + return + } + + _, was := mAttr[key] + if was { + err = fmt.Errorf("%w %s", errDuplicatedAttributeKey, key) + return + } + + if !utf8.ValidString(key) { + err = fmt.Errorf("%w: %q", errNonUTF8AttributeKey, key) + return + } + + if value == "" { + err = fmt.Errorf("%w %q", errEmptyAttributeValue, value) + return + } + + mAttr[key] = value + }) + if err != nil { + return err + } + + for k, v := range mAttr { + err = x.ctrl.CheckNodeAccessToAttribute(&nodePubKey, k, v) + if err != nil { + if errors.Is(err, ErrAccessDenied) { + return fmt.Errorf("%w %q=%q", ErrAccessDenied, k, v) + } + return fmt.Errorf("check access to use attribute %q=%q: %w", k, v, err) + } + } + + return nil +} diff --git a/pkg/innerring/processors/netmap/nodevalidation/attributes/validator_test.go b/pkg/innerring/processors/netmap/nodevalidation/attributes/validator_test.go new file mode 100644 index 00000000000..c8c34b6ef29 --- /dev/null +++ b/pkg/innerring/processors/netmap/nodevalidation/attributes/validator_test.go @@ -0,0 +1,196 @@ +package attributevalidator + +import ( + "errors" + "fmt" + "testing" + "unicode/utf8" + + neofscrypto "github.com/nspcc-dev/neofs-sdk-go/crypto" + "github.com/nspcc-dev/neofs-sdk-go/crypto/test" + "github.com/nspcc-dev/neofs-sdk-go/netmap" + "github.com/stretchr/testify/require" +) + +type testController struct { + tb testing.TB + + nodeKey neofscrypto.PublicKey + mAttr map[string]string + + mProcessedAttr map[string]struct{} + + retErr error +} + +func newTestController(tb testing.TB, nodeKey neofscrypto.PublicKey, mAttr map[string]string) *testController { + return &testController{ + tb: tb, + nodeKey: nodeKey, + mAttr: mAttr, + mProcessedAttr: make(map[string]struct{}), + } +} + +func (x *testController) setRetErr(err error) { + x.retErr = err +} + +func (x *testController) CheckNodeAccessToAttribute(nodeKey neofscrypto.PublicKey, attrKey, attrValue string) error { + require.Equal(x.tb, x.nodeKey, nodeKey, "node public key must be decoded from the original binary") + require.NotEmpty(x.tb, attrKey, "empty attribute key must not be passed", attrKey) + require.True(x.tb, utf8.ValidString(attrKey), "invalid UTF-8 attribute key must not be passed", attrKey) + require.NotEmptyf(x.tb, attrValue, "empty attribute value must not be passed", attrValue) + + _, ok := x.mProcessedAttr[attrKey] + require.False(x.tb, ok, "attribute key must be passed once", attrKey) + + x.mProcessedAttr[attrKey] = struct{}{} + + val, ok := x.mAttr[attrKey] + require.True(x.tb, ok, "attribute key must be from the original attributes", attrKey) + require.Equal(x.tb, val, attrValue, "attribute value must be from the original attributes", attrValue) + + return x.retErr +} + +func newNodeInfo(bNodeKey []byte, mAttr map[string]string) netmap.NodeInfo { + var res netmap.NodeInfo + res.SetPublicKey(bNodeKey) + + for k, v := range mAttr { + res.SetAttribute(k, v) + } + + return res +} + +type unprotectedNodeInfo struct { + bNodeKey []byte + attrs [][2]string +} + +func (x unprotectedNodeInfo) PublicKey() []byte { + return x.bNodeKey +} + +func (x unprotectedNodeInfo) NumberOfAttributes() int { + return len(x.attrs) +} + +func (x unprotectedNodeInfo) IterateAttributes(f func(string, string)) { + for _, a := range x.attrs { + f(a[0], a[1]) + } +} + +func newUnprotectedNodeInfo(bNodeKey []byte, attrs [][2]string) nodeInfo { + return unprotectedNodeInfo{ + bNodeKey: bNodeKey, + attrs: attrs, + } +} + +func TestValidator_VerifyAndUpdate(t *testing.T) { + nodeKey := test.RandomSigner(t).Public() + bNodeKey := neofscrypto.PublicKeyBytes(nodeKey) + + t.Run("invalid node key", func(t *testing.T) { + mAttr := map[string]string{ + "any_key": "any_value", + } + + node := newNodeInfo([]byte("definitely not a key"), mAttr) + ctrl := newTestController(t, nodeKey, mAttr) + v := New(ctrl) + + err := v.VerifyAndUpdate(&node) + require.ErrorIs(t, err, errInvalidNodeBinaryKey) + }) + + for _, tc := range []struct { + desc string + attrs [][2]string + expectedErr error + }{ + { + desc: "empty key", + attrs: [][2]string{{"", "any_value"}}, + expectedErr: errEmptyAttributeKey, + }, + { + desc: "not a UTF-8 key", + attrs: [][2]string{ + {"\xed\xa0\x80", "any_value"}, // U+D800 UTF-16 high surrogate mentioned in RFC 3629 + }, + expectedErr: errNonUTF8AttributeKey, + }, + { + desc: "empty value", + attrs: [][2]string{{"any_key", ""}}, + expectedErr: errEmptyAttributeValue, + }, + { + desc: "duplicated attribute", + attrs: [][2]string{ + {"any_key", "val1"}, + {"any_key", "val2"}, + }, + expectedErr: errDuplicatedAttributeKey, + }, + } { + t.Run(fmt.Sprintf("invalid attributes (%s)", tc.desc), func(t *testing.T) { + node := newUnprotectedNodeInfo(bNodeKey, tc.attrs) + + mAttr := make(map[string]string) + for _, a := range tc.attrs { + mAttr[a[0]] = a[1] + } + + ctrl := newTestController(t, nodeKey, mAttr) + v := New(ctrl) + + err := v.verifyAndUpdate(node) + require.ErrorIs(t, err, tc.expectedErr, tc.desc) + }) + } + + t.Run("failed check", func(t *testing.T) { + checkErr := func(expectedErr error) { + mAttr := map[string]string{ + "any_key1": "any_value2", + "any_key2": "any_value2", + } + + node := newNodeInfo(bNodeKey, mAttr) + ctrl := newTestController(t, nodeKey, mAttr) + v := New(ctrl) + + ctrl.setRetErr(expectedErr) + err := v.VerifyAndUpdate(&node) + require.ErrorIs(t, err, expectedErr) + } + + t.Run("access denied", func(t *testing.T) { + checkErr(ErrAccessDenied) + }) + + t.Run("other reason", func(t *testing.T) { + anyErr := errors.New("any error") + checkErr(anyErr) + }) + }) + + mAttr := map[string]string{ + "any_key1": "any_value1", + "any_key2": "any_value2", + } + + node := newNodeInfo(bNodeKey, mAttr) + ctrl := newTestController(t, nodeKey, mAttr) + v := New(ctrl) + + err := v.VerifyAndUpdate(&node) + require.NoError(t, err) + require.Len(t, ctrl.mProcessedAttr, len(mAttr)) +}