mirror of
https://github.com/pgsty/minio.git
synced 2026-08-10 00:03:29 +03:00
162ded3438
GetNotifyNATS reads user_credentials, nkey_seed and tls_handshake_first and GetNotifyAMQP reads immediate, but none of them were registered in DefaultNATSKVS/DefaultAMQPKVS or the help schema, so CheckValidKeys rejected any enable=on target carrying them. Worse, the legacy config migration wrote exactly these keys - including the env var name MINIO_NOTIFY_NATS_USER_CREDENTIALS used as a config key, because the NATSUserCredentials constant doubled as both - so a migrated NATS config failed validation on every load, and the FetchEnabledTargets fail-fast then silently disabled all bucket notification targets. - Register user_credentials/nkey_seed/tls_handshake_first (NATS) and immediate (AMQP) in the default KVS and help schema; split NATSUserCredentials into a real config key plus EnvNATSUserCredentials (all env var names byte-stable) - Fix legacy migration: SetNotifyNATS writes the proper key; SetNotifyAMQP no longer writes cfg.Immediate under the internal key and now carries both immediate and internal - Tolerate the legacy MINIO_NOTIFY_NATS_USER_CREDENTIALS key written by pre-fix migrations (NATS-scoped, load path only) with fallback read; env > user_credentials > legacy key - Print key names only, never values, in the invalid-keys error of both CheckValidKeys forms; rejected values can carry credentials - Add an AST-based audit test asserting parser reads, migration writes and help entries stay within the registered key set for all ten notify subsystems, with floor assertions so collector drift fails loudly - Document (unchanged) FetchEnabledTargets fail-fast and pin it with a characterization test Known same-class gap left in place and pinned by the audit's allowlist: SetNotifyPostgres/SetNotifyMySQL write five unregistered DSN-era keys; tracked for a follow-up issue. Closes #39 Co-authored-by: ChatGPT <noreply@openai.com> Co-authored-by: Claude <noreply@anthropic.com>
185 lines
5.6 KiB
Go
185 lines
5.6 KiB
Go
// Copyright (c) 2015-2021 MinIO, Inc.
|
|
//
|
|
// This file is part of MinIO Object Storage stack
|
|
//
|
|
// This program is free software: you can redistribute it and/or modify
|
|
// it under the terms of the GNU Affero General Public License as published by
|
|
// the Free Software Foundation, either version 3 of the License, or
|
|
// (at your option) any later version.
|
|
//
|
|
// This program is distributed in the hope that it will be useful
|
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
// GNU Affero General Public License for more details.
|
|
//
|
|
// You should have received a copy of the GNU Affero General Public License
|
|
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
|
|
package config
|
|
|
|
import (
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
func TestKVFields(t *testing.T) {
|
|
tests := []struct {
|
|
input string
|
|
keys []string
|
|
expectedFields map[string]struct{}
|
|
}{
|
|
// No keys present
|
|
{
|
|
input: "",
|
|
keys: []string{"comment"},
|
|
expectedFields: map[string]struct{}{},
|
|
},
|
|
// No keys requested for tokenizing
|
|
{
|
|
input: `comment="Hi this is my comment ="`,
|
|
keys: []string{},
|
|
expectedFields: map[string]struct{}{},
|
|
},
|
|
// Single key requested and present
|
|
{
|
|
input: `comment="Hi this is my comment ="`,
|
|
keys: []string{"comment"},
|
|
expectedFields: map[string]struct{}{`comment="Hi this is my comment ="`: {}},
|
|
},
|
|
// Keys and input order of k=v is same.
|
|
{
|
|
input: `connection_string="host=localhost port=2832" comment="really long comment"`,
|
|
keys: []string{"connection_string", "comment"},
|
|
expectedFields: map[string]struct{}{
|
|
`connection_string="host=localhost port=2832"`: {},
|
|
`comment="really long comment"`: {},
|
|
},
|
|
},
|
|
// Keys with spaces in between
|
|
{
|
|
input: `enable=on format=namespace connection_string=" host=localhost port=5432 dbname = cesnietor sslmode=disable" table=holicrayoli`,
|
|
keys: []string{"enable", "connection_string", "comment", "format", "table"},
|
|
expectedFields: map[string]struct{}{
|
|
`enable=on`: {},
|
|
`format=namespace`: {},
|
|
`connection_string=" host=localhost port=5432 dbname = cesnietor sslmode=disable"`: {},
|
|
`table=holicrayoli`: {},
|
|
},
|
|
},
|
|
// One of the keys is not present and order of input has changed.
|
|
{
|
|
input: `comment="really long comment" connection_string="host=localhost port=2832"`,
|
|
keys: []string{"connection_string", "comment", "format"},
|
|
expectedFields: map[string]struct{}{
|
|
`connection_string="host=localhost port=2832"`: {},
|
|
`comment="really long comment"`: {},
|
|
},
|
|
},
|
|
// Incorrect delimiter, expected fields should be empty.
|
|
{
|
|
input: `comment:"really long comment" connection_string:"host=localhost port=2832"`,
|
|
keys: []string{"connection_string", "comment"},
|
|
expectedFields: map[string]struct{}{},
|
|
},
|
|
// Incorrect type of input v/s required keys.
|
|
{
|
|
input: `comme="really long comment" connection_str="host=localhost port=2832"`,
|
|
keys: []string{"connection_string", "comment"},
|
|
expectedFields: map[string]struct{}{},
|
|
},
|
|
}
|
|
for _, test := range tests {
|
|
t.Run("", func(t *testing.T) {
|
|
gotFields := kvFields(test.input, test.keys)
|
|
if len(gotFields) != len(test.expectedFields) {
|
|
t.Errorf("Expected keys %d, found %d", len(test.expectedFields), len(gotFields))
|
|
}
|
|
found := true
|
|
for _, field := range gotFields {
|
|
_, ok := test.expectedFields[field]
|
|
found = found && ok
|
|
}
|
|
if !found {
|
|
t.Errorf("Expected %s, got %s", test.expectedFields, gotFields)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestValidRegion(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
success bool
|
|
}{
|
|
{name: "us-east-1", success: true},
|
|
{name: "us_east", success: true},
|
|
{name: "helloWorld", success: true},
|
|
{name: "-fdslka", success: false},
|
|
{name: "^00[", success: false},
|
|
{name: "my region", success: false},
|
|
{name: "%%$#!", success: false},
|
|
}
|
|
|
|
for _, test := range tests {
|
|
t.Run(test.name, func(t *testing.T) {
|
|
ok := validRegionRegex.MatchString(test.name)
|
|
if test.success != ok {
|
|
t.Errorf("Expected %t, got %t", test.success, ok)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// The invalid-keys error is logged by the server and printed by `mc`. A
|
|
// rejected key may carry a credential, so only key names may appear in it.
|
|
func TestCheckValidKeysDoesNotLeakValues(t *testing.T) {
|
|
const (
|
|
badKey = "unknown_key"
|
|
badSecret = "s3cr3t-must-not-appear"
|
|
)
|
|
|
|
assertRedacted := func(t *testing.T, err error) {
|
|
t.Helper()
|
|
if err == nil {
|
|
t.Fatal("expected an error for an unregistered key")
|
|
}
|
|
msg := err.Error()
|
|
if strings.Contains(msg, badSecret) {
|
|
t.Errorf("error leaks the rejected value: %s", msg)
|
|
}
|
|
if !strings.Contains(msg, badKey) {
|
|
t.Errorf("error does not name the rejected key: %s", msg)
|
|
}
|
|
if !strings.Contains(msg, "mc admin config reset") {
|
|
t.Errorf("error lost the remediation hint: %s", msg)
|
|
}
|
|
}
|
|
|
|
t.Run("func", func(t *testing.T) {
|
|
kv := KVS{
|
|
KV{Key: Enable, Value: EnableOn},
|
|
KV{Key: badKey, Value: badSecret},
|
|
}
|
|
validKVS := KVS{KV{Key: Enable, Value: EnableOff}}
|
|
assertRedacted(t, CheckValidKeys("test_subsys", kv, validKVS))
|
|
})
|
|
|
|
t.Run("method", func(t *testing.T) {
|
|
const subSys = "test_subsys_method"
|
|
RegisterDefaultKVS(map[string]KVS{
|
|
subSys: {KV{Key: Enable, Value: EnableOff}},
|
|
})
|
|
t.Cleanup(func() { delete(DefaultKVS, subSys) })
|
|
|
|
c := Config{
|
|
subSys: map[string]KVS{
|
|
Default: {
|
|
KV{Key: Enable, Value: EnableOn},
|
|
KV{Key: badKey, Value: badSecret},
|
|
},
|
|
},
|
|
}
|
|
assertRedacted(t, c.CheckValidKeys(subSys, nil))
|
|
})
|
|
}
|