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)) +}