Skip to content

Commit

Permalink
Reject configuration with unknown fields (#2981)
Browse files Browse the repository at this point in the history
  • Loading branch information
roman-khimov authored Nov 5, 2024
2 parents ed2a5e0 + 652e230 commit 2e583e5
Show file tree
Hide file tree
Showing 18 changed files with 1,040 additions and 58 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ attribute, which is used for container domain name in NNS contracts (#2954)
- In inner ring config, default ports for TCP addresses are used if they are missing (#2969)
- Metabase is refilled if an object exists in write-cache but is missing in metabase (#2977)
- Pprof and metrics services stop at the end of SN's application lifecycle (#2976)
- Reject configuration with unknown fields (#2981)

### Removed
- Support for node.key configuration (#2959)
Expand Down
106 changes: 106 additions & 0 deletions cmd/internal/configvalidator/validate.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
package configvalidator

import (
"errors"
"fmt"
"reflect"
"strings"
)

// ErrUnknowField returns when an unknown field appears in the config.
var ErrUnknowField = errors.New("unknown field")

// CheckForUnknownFields validates the config struct for unknown fields with the config map.
// If the field in the config is a map of a key and some value, need to use `mapstructure:,remain` and
// `prefix` tag with the key prefix, even it is empty string.
func CheckForUnknownFields(configMap map[string]any, config any) error {
return checkForUnknownFields(configMap, config, "")
}

func checkForUnknownFields(configMap map[string]any, config any, currentPath string) error {
expectedFields := getFieldsFromStruct(config)

for key, val := range configMap {
fullPath := key
if currentPath != "" {
fullPath = currentPath + "." + key
}

_, other := expectedFields[",remain"]
_, exists := expectedFields[key]
if !exists && !other {
return fmt.Errorf("%w: %s", ErrUnknowField, fullPath)
}

var fieldVal reflect.Value
if other && !exists {
fieldVal = reflect.ValueOf(config).
FieldByName(getStructFieldByTag(config, "prefix", key, strings.HasPrefix))
} else {
fieldVal = reflect.ValueOf(config).
FieldByName(getStructFieldByTag(config, "mapstructure", key, func(a, b string) bool {
return a == b
}))
}

switch fieldVal.Kind() {
case reflect.Slice:
if fieldVal.Len() == 0 {
return nil
}
fieldVal = fieldVal.Index(0)
case reflect.Map:
fieldVal = fieldVal.MapIndex(reflect.ValueOf(key))
default:
}

if !fieldVal.IsValid() {
return fmt.Errorf("%w: %s", ErrUnknowField, fullPath)
}

nestedMap, okMap := val.(map[string]any)
nestedSlice, okSlice := val.([]any)
if (okMap || okSlice) && fieldVal.Kind() == reflect.Struct {
if okSlice {
for _, slice := range nestedSlice {
if nestedMap, okMap = slice.(map[string]any); okMap {
if err := checkForUnknownFields(nestedMap, fieldVal.Interface(), fullPath); err != nil {
return err
}
}
}
} else if err := checkForUnknownFields(nestedMap, fieldVal.Interface(), fullPath); err != nil {
return err
}
} else if okMap != (fieldVal.Kind() == reflect.Struct) {
return fmt.Errorf("%w: %s", ErrUnknowField, fullPath)
}
}

return nil
}

func getFieldsFromStruct(config any) map[string]struct{} {
fields := make(map[string]struct{})
t := reflect.TypeOf(config)
for i := range t.NumField() {
field := t.Field(i)
if jsonTag := field.Tag.Get("mapstructure"); jsonTag != "" {
fields[jsonTag] = struct{}{}
} else {
fields[field.Name] = struct{}{}
}
}
return fields
}

func getStructFieldByTag(config any, tagKey, tagValue string, comparison func(a, b string) bool) string {
t := reflect.TypeOf(config)
for i := range t.NumField() {
field := t.Field(i)
if jsonTag, ok := field.Tag.Lookup(tagKey); comparison(tagValue, jsonTag) && ok {
return field.Name
}
}
return ""
}
12 changes: 10 additions & 2 deletions cmd/neofs-ir/defaults.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"strings"
"time"

"github.com/nspcc-dev/neofs-node/cmd/neofs-ir/internal/validate"
"github.com/spf13/viper"
)

Expand All @@ -29,9 +30,17 @@ func newConfig(path string) (*viper.Viper, error) {
v.SetConfigType("yml")
}
err = v.ReadInConfig()
if err != nil {
return nil, err
}
}

err = validate.ValidateStruct(v)
if err != nil {
return nil, err
}

return v, err
return v, nil
}

func defaultConfiguration(cfg *viper.Viper) {
Expand Down Expand Up @@ -60,7 +69,6 @@ func defaultConfiguration(cfg *viper.Viper) {
cfg.SetDefault("wallet.address", "") // account address
cfg.SetDefault("wallet.password", "") // password

cfg.SetDefault("timers.emit", "0")
cfg.SetDefault("timers.stop_estimation.mul", 1)
cfg.SetDefault("timers.stop_estimation.div", 4)
cfg.SetDefault("timers.collect_basic_income.mul", 1)
Expand Down
17 changes: 17 additions & 0 deletions cmd/neofs-ir/defaults_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package main

import (
"testing"

"github.com/nspcc-dev/neofs-node/cmd/neofs-ir/internal/validate"
"github.com/spf13/viper"
"github.com/stretchr/testify/require"
)

func TestValidateDefaultConfig(t *testing.T) {
v := viper.New()

defaultConfiguration(v)

require.NoError(t, validate.ValidateStruct(v))
}
213 changes: 213 additions & 0 deletions cmd/neofs-ir/internal/validate/config.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,213 @@
package validate

import "time"

type validConfig struct {
Logger struct {
Level string `mapstructure:"level"`
} `mapstructure:"logger"`

Wallet struct {
Path string `mapstructure:"path"`
Address string `mapstructure:"address"`
Password string `mapstructure:"password"`
} `mapstructure:"wallet"`

WithoutMainnet bool `mapstructure:"without_mainnet"`

Morph struct {
DialTimeout time.Duration `mapstructure:"dial_timeout"`
ReconnectionsNumber int `mapstructure:"reconnections_number"`
ReconnectionsDelay time.Duration `mapstructure:"reconnections_delay"`
Endpoints []string `mapstructure:"endpoints"`
Validators []string `mapstructure:"validators"`
Consensus struct {
Magic uint32 `mapstructure:"magic"`
Committee []string `mapstructure:"committee"`

Storage struct {
Type string `mapstructure:"type"`
Path string `mapstructure:"path"`
} `mapstructure:"storage"`

TimePerBlock time.Duration `mapstructure:"time_per_block"`
MaxTraceableBlocks uint32 `mapstructure:"max_traceable_blocks"`
SeedNodes []string `mapstructure:"seed_nodes"`

Hardforks struct {
Name map[string]uint32 `mapstructure:",remain" prefix:""`
} `mapstructure:"hardforks"`

ValidatorsHistory struct {
Height map[string]int `mapstructure:",remain" prefix:""`
} `mapstructure:"validators_history"`

RPC struct {
Listen []string `mapstructure:"listen"`
TLS struct {
Enabled bool `mapstructure:"enabled"`
Listen []string `mapstructure:"listen"`
CertFile string `mapstructure:"cert_file"`
KeyFile string `mapstructure:"key_file"`
} `mapstructure:"tls"`
} `mapstructure:"rpc"`

P2P struct {
DialTimeout time.Duration `mapstructure:"dial_timeout"`
ProtoTickInterval time.Duration `mapstructure:"proto_tick_interval"`
Listen []string `mapstructure:"listen"`
Peers struct {
Min int `mapstructure:"min"`
Max int `mapstructure:"max"`
Attempts int `mapstructure:"attempts"`
} `mapstructure:"peers"`
Ping struct {
Interval time.Duration `mapstructure:"interval"`
Timeout time.Duration `mapstructure:"timeout"`
} `mapstructure:"ping"`
} `mapstructure:"p2p"`
SetRolesInGenesis bool `mapstructure:"set_roles_in_genesis"`
} `mapstructure:"consensus"`
} `mapstructure:"morph"`

FSChainAutodeploy bool `mapstructure:"fschain_autodeploy"`

NNS struct {
SystemEmail string `mapstructure:"system_email"`
} `mapstructure:"nns"`

Mainnet struct {
DialTimeout time.Duration `mapstructure:"dial_timeout"`
ReconnectionsNumber int `mapstructure:"reconnections_number"`
ReconnectionsDelay time.Duration `mapstructure:"reconnections_delay"`
Endpoints []string `mapstructure:"endpoints"`
} `mapstructure:"mainnet"`

Control struct {
AuthorizedKeys []string `mapstructure:"authorized_keys"`
GRPC struct {
Endpoint string `mapstructure:"endpoint"`
} `mapstructure:"grpc"`
} `mapstructure:"control"`

Governance struct {
Disable bool `mapstructure:"disable"`
} `mapstructure:"governance"`

Node struct {
PersistentState struct {
Path string `mapstructure:"path"`
} `mapstructure:"persistent_state"`
} `mapstructure:"node"`

Fee struct {
MainChain int64 `mapstructure:"main_chain"`
SideChain int64 `mapstructure:"side_chain"`
NamedContainerRegister int64 `mapstructure:"named_container_register"`
} `mapstructure:"fee"`

Timers struct {
StopEstimation struct {
Mul int `mapstructure:"mul"`
Div int `mapstructure:"div"`
} `mapstructure:"stop_estimation"`
CollectBasicIncome struct {
Mul int `mapstructure:"mul"`
Div int `mapstructure:"div"`
} `mapstructure:"collect_basic_income"`
DistributeBasicIncome struct {
Mul int `mapstructure:"mul"`
Div int `mapstructure:"div"`
} `mapstructure:"distribute_basic_income"`
} `mapstructure:"timers"`

Emit struct {
Storage struct {
Amount int64 `mapstructure:"amount"`
} `mapstructure:"storage"`
Mint struct {
Value int64 `mapstructure:"value"`
CacheSize int `mapstructure:"cache_size"`
Threshold int `mapstructure:"threshold"`
} `mapstructure:"mint"`
Gas struct {
BalanceThreshold int64 `mapstructure:"balance_threshold"`
} `mapstructure:"gas"`
} `mapstructure:"emit"`

Workers struct {
Alphabet int `mapstructure:"alphabet"`
Balance int `mapstructure:"balance"`
Container int `mapstructure:"container"`
NeoFS int `mapstructure:"neofs"`
Netmap int `mapstructure:"netmap"`
Reputation int `mapstructure:"reputation"`
} `mapstructure:"workers"`

Audit struct {
Timeout struct {
Get time.Duration `mapstructure:"get"`
Head time.Duration `mapstructure:"head"`
RangeHash time.Duration `mapstructure:"rangehash"`
Search time.Duration `mapstructure:"search"`
} `mapstructure:"timeout"`
Task struct {
ExecPoolSize int `mapstructure:"exec_pool_size"`
QueueCapacity int `mapstructure:"queue_capacity"`
} `mapstructure:"task"`
PDP struct {
PairsPoolSize int `mapstructure:"pairs_pool_size"`
MaxSleepInterval time.Duration `mapstructure:"max_sleep_interval"`
} `mapstructure:"pdp"`
POR struct {
PoolSize int `mapstructure:"pool_size"`
} `mapstructure:"por"`
} `mapstructure:"audit"`

Indexer struct {
CacheTimeout time.Duration `mapstructure:"cache_timeout"`
} `mapstructure:"indexer"`

NetmapCleaner struct {
Enabled bool `mapstructure:"enabled"`
Threshold int `mapstructure:"threshold"`
} `mapstructure:"netmap_cleaner"`

Contracts struct {
NeoFS string `mapstructure:"neofs"`
Processing string `mapstructure:"processing"`
Audit string `mapstructure:"audit"`
Balance string `mapstructure:"balance"`
Container string `mapstructure:"container"`
NeoFSID string `mapstructure:"neofsid"`
Netmap string `mapstructure:"netmap"`
Proxy string `mapstructure:"proxy"`
Reputation string `mapstructure:"reputation"`
Alphabet struct {
AZ string `mapstructure:"az"`
Buky string `mapstructure:"buky"`
Vedi string `mapstructure:"vedi"`
Glagoli string `mapstructure:"glagoli"`
Dobro string `mapstructure:"dobro"`
Yest string `mapstructure:"yest"`
Zhivete string `mapstructure:"zhivete"`
} `mapstructure:"alphabet"`
} `mapstructure:"contracts"`

Pprof struct {
Enabled bool `mapstructure:"enabled"`
Address string `mapstructure:"address"`
ShutdownTimeout time.Duration `mapstructure:"shutdown_timeout"`
} `mapstructure:"pprof"`

Prometheus struct {
Enabled bool `mapstructure:"enabled"`
Address string `mapstructure:"address"`
ShutdownTimeout time.Duration `mapstructure:"shutdown_timeout"`
} `mapstructure:"prometheus"`

Settlement struct {
BasicIncomeRate int64 `mapstructure:"basic_income_rate"`
AuditFee int64 `mapstructure:"audit_fee"`
} `mapstructure:"settlement"`
}
Loading

0 comments on commit 2e583e5

Please sign in to comment.