Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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
5 changes: 5 additions & 0 deletions .circleci/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,9 @@ default_environment: &default_environment
CIRCLE_ARTIFACTS: /tmp/circleci-artifacts
GIT_PAGER: cat

orbs:
jq: circleci/jq@2.2.0

executors:
golang:
docker:
Expand Down Expand Up @@ -131,6 +134,8 @@ jobs:
steps:
- run: sudo apt update
- run: sudo apt install socat net-tools
- jq/install:
override: true
- checkout

- run:
Expand Down
17 changes: 15 additions & 2 deletions core/commands/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import (

"github.com/ipfs/go-ipfs/core/commands/cmdenv"
"github.com/ipfs/go-ipfs/repo"
. "github.com/ipfs/go-ipfs/repo/common"
"github.com/ipfs/go-ipfs/repo/fsrepo"

"github.com/elgris/jsondiff"
Expand Down Expand Up @@ -56,6 +57,11 @@ Get the value of the 'Datastore.Path' key:
Set the value of the 'Datastore.Path' key:

$ ipfs config Datastore.Path ~/.ipfs/datastore

Values behind map key names that include dots can be accessed like this:

$ ipfs config Pinning.RemoteServices["pins.example.org"].Policies

`,
},
Subcommands: map[string]*cmds.Command{
Expand Down Expand Up @@ -157,7 +163,8 @@ Set the value of the 'Datastore.Path' key:
// matchesGlobPrefix("foo.bar.baz", []string{"*", "bar"}) returns true
// matchesGlobPrefix("foo.bar", []string{"baz", "*"}) returns false
func matchesGlobPrefix(key string, glob []string) bool {
k := strings.Split(key, ".")
normalizedKey, _ := ConfigKeyToLookupData(key)
Copy link
Contributor

Choose a reason for hiding this comment

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

strings.EqualFold (below) no longer works, since now it is applied to a rewritten key which has UUIDs.

k := strings.Split(normalizedKey, ".")
for i, g := range glob {
if i >= len(k) {
break
Expand All @@ -176,7 +183,13 @@ var configShowCmd = &cmds.Command{
Helptext: cmds.HelpText{
Tagline: "Output config file contents.",
ShortDescription: `
NOTE: For security reasons, this command will omit your private key and remote services. If you would like to make a full backup of your config (private key included), you must copy the config file from your repo.
'ipfs config show' returns config contents without private keys and secrets.
`,
LongDescription: `
NOTE: For security reasons, this command will omit your private key and any
access tokens for remote services. If you would like to make a full backup of
your config (private key and secrets included), you must copy the config file
from your IPFS repository (IPFS_PATH).
`,
},
Type: make(map[string]interface{}),
Expand Down
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ require (
github.com/fsnotify/fsnotify v1.4.9
github.com/gabriel-vasile/mimetype v1.2.0
github.com/go-bindata/go-bindata/v3 v3.1.3
github.com/google/uuid v1.1.2
github.com/hashicorp/go-multierror v1.1.1
github.com/ipfs/go-bitswap v0.3.3
github.com/ipfs/go-block-format v0.0.3
Expand Down
52 changes: 46 additions & 6 deletions repo/common/common.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,26 +2,62 @@ package common

import (
"fmt"
"regexp"
"strings"

"github.com/google/uuid"
)

// Find dynamic map key names passed with Parent["foo"] notation
var bracketsRe = regexp.MustCompile(`\["([^\["\]]*)"\]`)
Copy link
Contributor

Choose a reason for hiding this comment

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

This regular expression does not support escape sequences. I wouldn't advice writing your own quoted string parser. It's hard. I would advice using a JS parser library for parsing the ENTIRE glob, e.g. https://pkg.go.dev/github.com/robertkrimen/otto/parser, and then work with the AST.

Copy link
Contributor

Choose a reason for hiding this comment

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

Alternatively, you can use the Go language parser for the glob. It has a different set of escape sequences than JS, but is still better than rolling your own.


// Normalization for supporting arbitrary dynamic keys with dots:
// Gateway.PublicGateways["gw.example.com"].UseSubdomains
// Pinning.RemoteServices["pins.example.org"].Policies.MFS.Enable
Comment on lines +15 to +16
Copy link
Contributor

@aschmahmann aschmahmann Apr 28, 2021

Choose a reason for hiding this comment

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

Is this notation standard/used in other software or are we just making this up this form of escaping? Either way we'd need to document this in the config command.

Copy link
Member Author

@lidel lidel Apr 28, 2021

Choose a reason for hiding this comment

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

This is how keys in JSON objects are addressed on the web, and since we use JSON for config anyway, means we don't invent anything new, but follow existing JSON convention.:

( {"foo.bar":{"a": "buz"}} )["foo.bar"].a  "buz"

I've added note about this notation to ipfs config --help in 0a2336e

Copy link
Contributor

Choose a reason for hiding this comment

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

The return value is not documented accurately on a public method. What about dynamicKeys?

func ConfigKeyToLookupData(key string) (normalizedKey string, dynamicKeys map[string]string) {
bracketedKeys := bracketsRe.FindAllString(key, -1)
dynamicKeys = make(map[string]string, len(bracketedKeys))
normalizedKey = key
for _, mapKeySegment := range bracketedKeys {
mapKey := strings.TrimPrefix(mapKeySegment, `["`)
mapKey = strings.TrimSuffix(mapKey, `"]`)
placeholder := uuid.New().String()
dynamicKeys[placeholder] = mapKey
normalizedKey = strings.Replace(normalizedKey, mapKeySegment, fmt.Sprintf(".%s", placeholder), 1)
Copy link
Contributor

Choose a reason for hiding this comment

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

This will convert a glob string like: ["abc"]["def"] into .UUID1.UUID2
Is this your intention? Specifically, I am referring to the first key.

}
return normalizedKey, dynamicKeys
}

// Produces a part of config key with original map key names.
// Used only for better UX in error messages.
func buildSubKey(i int, parts []string, dynamicKeys map[string]string) string {
subkey := strings.Join(parts[:i], ".")
for placeholder, realKey := range dynamicKeys {
subkey = strings.Replace(subkey, fmt.Sprintf(".%s", placeholder), fmt.Sprintf(`["%s"]`, realKey), 1)
}
return subkey
}

func MapGetKV(v map[string]interface{}, key string) (interface{}, error) {
var ok bool
var mcursor map[string]interface{}
var cursor interface{} = v

parts := strings.Split(key, ".")
normalizedKey, dynamicKeys := ConfigKeyToLookupData(key)
parts := strings.Split(normalizedKey, ".")
Copy link
Contributor

Choose a reason for hiding this comment

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

Fails of normalizedKey starts with a ".", which technically it can.

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

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

if dynamicPart, ok := dynamicKeys[part]; ok {
part = dynamicPart
}
cursor, ok = mcursor[part]
if !ok {
return nil, fmt.Errorf("%s key has no attributes", sofar)
return nil, fmt.Errorf("%s key has no attribute %s", sofar, part)
}
}
return cursor, nil
Expand All @@ -32,13 +68,17 @@ func MapSetKV(v map[string]interface{}, key string, value interface{}) error {
var mcursor map[string]interface{}
var cursor interface{} = v

parts := strings.Split(key, ".")
normalizedKey, dynamicKeys := ConfigKeyToLookupData(key)
parts := strings.Split(normalizedKey, ".")
for i, part := range parts {
mcursor, ok = cursor.(map[string]interface{})
if !ok {
sofar := strings.Join(parts[:i], ".")
sofar := buildSubKey(i, parts, dynamicKeys)
return fmt.Errorf("%s key is not a map", sofar)
}
if dynamicPart, ok := dynamicKeys[part]; ok {
part = dynamicPart
}

// last part? set here
if i == (len(parts) - 1) {
Expand Down
11 changes: 11 additions & 0 deletions test/sharness/t0021-config.sh
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,17 @@ test_config_cmd() {
test_cmp replconfig.json newconfig.json
'

# Dynamic keys with dot in their names
test_config_cmd_set "--json" "Gateway.PublicGateways[\"some.example.com\"].UseSubdomains" "true"
test_expect_success "'ipfs config show' after Foo[\"bar.buzz\"] returns a valid JSON" '
ipfs config show | jq -e > /dev/null 2>&1
'
# TODO: ipfs config show | jq -e > /dev/null 2>&1
test_expect_success "'ipfs config' after Foo[\"bar.buzz\"] shows updated value" '
ipfs config Gateway.PublicGateways[\"some.example.com\"].UseSubdomains > bool_out &&
grep true bool_out
'

# SECURITY
# Those tests are here to prevent exposing the PrivKey on the network

Expand Down
22 changes: 22 additions & 0 deletions test/sharness/t0700-remotepin.sh
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ test_expect_success "test 'ipfs pin remote service ls' JSON on empty list" '
test_expect_success "creating test user on remote pinning service" '
echo CI host IP address ${TEST_PIN_SVC} &&
ipfs pin remote service add test_pin_svc ${TEST_PIN_SVC} ${TEST_PIN_SVC_KEY} &&
ipfs pin remote service add test.dots.in.name ${TEST_PIN_SVC} ${TEST_PIN_SVC_KEY} &&
ipfs pin remote service add test_invalid_key_svc ${TEST_PIN_SVC} fake_api_key &&
ipfs pin remote service add test_invalid_url_path_svc ${TEST_PIN_SVC}/invalid-path fake_api_key &&
ipfs pin remote service add test_invalid_url_dns_svc https://invalid-service.example.com fake_api_key &&
Expand Down Expand Up @@ -100,6 +101,8 @@ test_expect_success "output does not include API.Key" '
test_expect_code 1 grep -q Key config_out
'

# dot notation

test_expect_success "'ipfs config Pinning.RemoteServices.test_pin_svc.API.Key' fails" '
test_expect_code 1 ipfs config Pinning.RemoteServices.test_pin_svc.API.Key 2> config_out
'
Expand All @@ -116,6 +119,25 @@ test_expect_success "output includes meaningful error" '
test_cmp config_exp config_out
'

# json map key notation

test_expect_success "'ipfs config Pinning.RemoteServices[\"test.dots.in.name\"]' fails" '
test_expect_code 1 ipfs config Pinning.RemoteServices[\"test.dots.in.name\"] 2> config_out
'
test_expect_success "output includes meaningful error" '
test_cmp config_exp config_out
'

test_expect_success "'ipfs config Pinning.RemoteServices[\"test.dots.in.name\"].API.Key' fails" '
test_expect_code 1 ipfs config Pinning.RemoteServices[\"test.dots.in.name\"].API.Key 2> config_out
'

test_expect_success "output includes meaningful error" '
test_cmp config_exp config_out
'

# config show

test_expect_success "'ipfs config show' does not include Pinning.RemoteServices[*].API.Key" '
ipfs config show | tee show_config | jq -r .Pinning.RemoteServices > remote_services &&
test_expect_code 1 grep \"Key\" remote_services &&
Expand Down