Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: Issue #9364 JSON config validation #10679

Merged
merged 32 commits into from
Feb 4, 2025
Merged
Show file tree
Hide file tree
Changes from 16 commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
c5649c2
Fix JSON config validation
gsergey418alt Jan 28, 2025
b69676b
Add changelog entry.
gsergey418alt Jan 28, 2025
42692c6
Fix test
gsergey418alt Jan 28, 2025
f589555
Fix more tests, 53 still failing
gsergey418alt Jan 28, 2025
c35ee66
Remove JSON test due to fragilty, fix some more tests, Lib tests left.
gsergey418alt Jan 28, 2025
dd9627e
Remove lib tests as they don't seem to be relevant anymore.
gsergey418alt Jan 28, 2025
f7b44d0
Move changelog into v0.34
gsergey418alt Jan 28, 2025
11f7f06
Fix config validation.
gsergey418alt Jan 29, 2025
ef3ff89
Delete test file
gsergey418alt Jan 29, 2025
7b88c81
Use reflection to ignore encoding/json tags.
gsergey418alt Jan 29, 2025
2765ec0
Add exceptions for fields that were already maps in the config, fix o…
gsergey418alt Jan 29, 2025
354256a
Fix last (hopefully) test
gsergey418alt Jan 29, 2025
12a0185
test: fix the socat tests after the ubuntu 24.04 upgrade
galargh Jan 29, 2025
c546382
Merge branch 'socat-ubuntu-noble' of https://github.com/ipfs/kubo int…
gsergey418alt Jan 29, 2025
a091fbe
Merge branch 'master' into fix/9364-validation
guillaumemichel Jan 29, 2025
ab2657e
Merge branch 'master' into fix/9364-validation
guillaumemichel Jan 30, 2025
af0c5c6
docker sharness
guillaumemichel Jan 30, 2025
7a3732f
updated ReflectToMap
guillaumemichel Jan 31, 2025
9086971
Fix nested maps and write a test for it.
gsergey418alt Jan 31, 2025
564d9dd
Add tests for other data types
gsergey418alt Jan 31, 2025
c34c068
Revert "Fix nested maps and write a test for it."
gsergey418alt Feb 3, 2025
44fbd88
Validate structs nested in maps.
gsergey418alt Feb 3, 2025
8a669cd
Add comments.
gsergey418alt Feb 3, 2025
0a25331
Remove debug
gsergey418alt Feb 3, 2025
eae4382
Add test for PublicGateways
gsergey418alt Feb 3, 2025
7e8d3ca
Add test for sturct in map
gsergey418alt Feb 3, 2025
db3cd49
Merge branch 'master' into fix/9364-validation
guillaumemichel Feb 4, 2025
bed120f
comment typo
guillaumemichel Feb 4, 2025
69235e2
slight refactor
guillaumemichel Feb 4, 2025
8e72a65
Add CheckKey tests.
gsergey418alt Feb 4, 2025
87e42d8
Add handling for interface{} in the config
gsergey418alt Feb 4, 2025
7d7633e
"any" -> nil
gsergey418alt Feb 4, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
63 changes: 63 additions & 0 deletions config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
"fmt"
"os"
"path/filepath"
"reflect"
"strings"

"github.com/ipfs/kubo/misc/fsutil"
Expand Down Expand Up @@ -137,6 +138,29 @@
return m, nil
}

// Convert config to a map, without using encoding/json.
func ReflectToMap(conf interface{}) map[string]interface{} {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what is the reason to define a new function, and not use ToMap defined above?

Copy link
Contributor Author

@gsergey418alt gsergey418alt Jan 30, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's no way to get encoding/json package (which is used in the ToMap function) to ignore the struct tags like json:",omitempty" that hide the map fields. That means that the user wouldn't be able to set these fields, since this map is used to validate user's input from ipfs config. Had to use the reflect package to make that happen.

confmap := make(map[string]interface{})
rv := reflect.ValueOf(conf)
if rv.Kind() == reflect.Ptr {
rv = rv.Elem()
}

for i := 0; i < rv.NumField(); i++ {
field := rv.Field(i)
if field.CanInterface() {
if field.Kind() == reflect.Struct {
confmap[rv.Type().Field(i).Name] = ReflectToMap(field.Interface())
} else if field.Kind() == reflect.Map {
confmap[rv.Type().Field(i).Name] = "map"
} else {
confmap[rv.Type().Field(i).Name] = field.Interface()
}
}
}
return confmap
}

// Clone copies the config. Use when updating.
func (c *Config) Clone() (*Config, error) {
var newConfig Config
Expand All @@ -152,3 +176,42 @@

return &newConfig, nil
}

// Check if the provided key is present in the structure.
func CheckKey(key string) error {
guillaumemichel marked this conversation as resolved.
Show resolved Hide resolved
conf := Config{}

// Convert an empty config to a map without JSON.
confmap := ReflectToMap(&conf)

// Parse the key and verify it's presence in the map.
var ok bool
var mcursor map[string]interface{}
var cursor interface{} = confmap

parts := strings.Split(key, ".")
for i, part := range parts {
sofar := strings.Join(parts[:i], ".")

mcursor, ok = cursor.(map[string]interface{})
if !ok {
s, ok := cursor.(string)
if ok && s == "map" {
return nil
}
return fmt.Errorf("%s key is not a map", sofar)

Check warning on line 202 in config/config.go

View check run for this annotation

Codecov / codecov/patch

config/config.go#L202

Added line #L202 was not covered by tests
}

cursor, ok = mcursor[part]
if !ok {
// Construct the current path traversed to print a nice error message
var path string
if len(sofar) > 0 {
path += sofar + "."
}
path += part
return fmt.Errorf("%s not found", path)

Check warning on line 213 in config/config.go

View check run for this annotation

Codecov / codecov/patch

config/config.go#L207-L213

Added lines #L207 - L213 were not covered by tests
}
}
return nil
}
8 changes: 6 additions & 2 deletions docs/changelogs/v0.34.md
Original file line number Diff line number Diff line change
@@ -1,19 +1,23 @@
# Kubo changelog v0.34

- [v0.34.0](#v0310)
- [v0.34.0](#v0340)

## v0.34.0

- [Overview](#overview)
- [🔦 Highlights](#-highlights)
- [JSON config validation](#json-config-validation)
- [📝 Changelog](#-changelog)
- [👨‍👩‍👧‍👦 Contributors](#-contributors)

### Overview

### 🔦 Highlights

#### JSON config validation

`ipfs config` is now validating json fields ([#10679](https://github.com/ipfs/kubo/pull/10679)).

### 📝 Changelog

### 👨‍👩‍👧‍👦 Contributors

6 changes: 6 additions & 0 deletions repo/fsrepo/fsrepo.go
Original file line number Diff line number Diff line change
Expand Up @@ -676,6 +676,12 @@
return errors.New("repo is closed")
}

// Validate the key's presence in the config structure.
err := config.CheckKey(key)
if err != nil {
return err
}

Check warning on line 683 in repo/fsrepo/fsrepo.go

View check run for this annotation

Codecov / codecov/patch

repo/fsrepo/fsrepo.go#L682-L683

Added lines #L682 - L683 were not covered by tests

// Load into a map so we don't end up writing any additional defaults to the config file.
var mapconf map[string]interface{}
if err := serialize.ReadConfigFile(r.configFilePath, &mapconf); err != nil {
Expand Down
8 changes: 4 additions & 4 deletions test/sharness/t0002-docker-image.sh
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,8 @@ test_expect_success "docker image build succeeds" '
'

test_expect_success "write init scripts" '
echo "ipfs config Foo Bar" > 001.sh &&
echo "ipfs config Baz Qux" > 002.sh &&
echo "ipfs config Provider.Strategy Bar" > 001.sh &&
echo "ipfs config Datastore.Path Qux" > 002.sh &&
chmod +x 002.sh
'

Expand Down Expand Up @@ -65,10 +65,10 @@ test_expect_success "check that init scripts were run correctly and in the corre

test_expect_success "check that init script configs were applied" '
echo Bar > expected &&
docker exec "$DOC_ID" ipfs config Foo > actual &&
docker exec "$DOC_ID" ipfs config Provider.Strategy > actual &&
test_cmp actual expected &&
echo Qux > expected &&
docker exec "$DOC_ID" ipfs config Baz > actual &&
docker exec "$DOC_ID" ipfs config Datastore.Path > actual &&
test_cmp actual expected
'

Expand Down
67 changes: 21 additions & 46 deletions test/sharness/t0021-config.sh
Original file line number Diff line number Diff line change
Expand Up @@ -13,41 +13,17 @@ test_config_cmd_set() {

cfg_key=$1
cfg_val=$2
test_expect_success "ipfs config succeeds" '
ipfs config $cfg_flags "$cfg_key" "$cfg_val"
'

test_expect_success "ipfs config output looks good" '
echo "$cfg_val" >expected &&
ipfs config "$cfg_key" >actual &&
test_cmp expected actual
'
test_expect_success "ipfs config succeeds" "
ipfs config $cfg_flags \"$cfg_key\" \"$cfg_val\"
"

# also test our lib function. it should work too.
cfg_key="Lib.$cfg_key"
test_expect_success "test_config_set succeeds" '
test_config_set $cfg_flags "$cfg_key" "$cfg_val"
'

test_expect_success "test_config_set value looks good" '
echo "$cfg_val" >expected &&
ipfs config "$cfg_key" >actual &&
guillaumemichel marked this conversation as resolved.
Show resolved Hide resolved
test_expect_success "ipfs config output looks good" "
echo \"$cfg_val\" >expected &&
ipfs config \"$cfg_key\" >actual &&
test_cmp expected actual
'
"
}

# this is a bit brittle. the problem is we need to test
# with something that will be forced to unmarshal as a struct.
# (i.e. just setting 'ipfs config --json foo "[1, 2, 3]"') may
# set it as astring instead of proper json. We leverage the
# unmarshalling that has to happen.
CONFIG_SET_JSON_TEST='{
"MDNS": {
"Enabled": true,
"Interval": 10
}
}'

test_profile_apply_revert() {
profile=$1
inverse_profile=$2
Expand Down Expand Up @@ -87,27 +63,26 @@ test_profile_apply_dry_run_not_alter() {
}

test_config_cmd() {
test_config_cmd_set "beep" "boop"
test_config_cmd_set "beep1" "boop2"
test_config_cmd_set "beep1" "boop2"
test_config_cmd_set "--bool" "beep2" "true"
test_config_cmd_set "--bool" "beep2" "false"
test_config_cmd_set "--json" "beep3" "true"
test_config_cmd_set "--json" "beep3" "false"
test_config_cmd_set "--json" "Discovery" "$CONFIG_SET_JSON_TEST"
test_config_cmd_set "--json" "deep-not-defined.prop" "true"
test_config_cmd_set "--json" "deep-null" "null"
test_config_cmd_set "--json" "deep-null.prop" "true"
test_config_cmd_set "Addresses.API" "foo"
test_config_cmd_set "Addresses.Gateway" "bar"
test_config_cmd_set "Datastore.GCPeriod" "baz"
test_config_cmd_set "--bool" "Discovery.MDNS.Enabled" "true"
test_config_cmd_set "--bool" "Discovery.MDNS.Enabled" "false"
test_config_cmd_set "--json" "Datastore.HashOnRead" "true"
test_config_cmd_set "--json" "Datastore.HashOnRead" "false"
test_config_cmd_set "--json" "Experimental.FilestoreEnabled" "true"
test_config_cmd_set "--json" "Import.BatchMaxSize" "null"
test_config_cmd_set "--json" "Import.UnixFSRawLeaves" "true"

test_expect_success "'ipfs config show' works" '
ipfs config show >actual
'

test_expect_success "'ipfs config show' output looks good" '
grep "\"beep\": \"boop\"," actual &&
grep "\"beep1\": \"boop2\"," actual &&
grep "\"beep2\": false," actual &&
grep "\"beep3\": false," actual
grep "\"API\": \"foo\"," actual &&
grep "\"Gateway\": \"bar\"" actual &&
grep "\"Enabled\": false" actual &&
grep "\"HashOnRead\": false" actual
'

test_expect_success "'ipfs config show --config-file' works" '
Expand Down
8 changes: 5 additions & 3 deletions test/sharness/t0070-user-config.sh
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,12 @@ test_description="Test user-provided config values"
test_init_ipfs

test_expect_success "bootstrap doesn't overwrite user-provided config keys (top-level)" '
ipfs config Foo.Bar baz &&
ipfs config Provider.Strategy >previous &&
ipfs config Provider.Strategy foo &&
ipfs bootstrap rm --all &&
echo "baz" >expected &&
ipfs config Foo.Bar >actual &&
echo "foo" >expected &&
ipfs config Provider.Strategy >actual &&
ipfs config Provider.Strategy $(cat previous) &&
test_cmp expected actual
'

Expand Down
Loading