mirror of
https://github.com/pgsty/minio.git
synced 2026-09-07 19:16:09 +03:00
711b092f86
The embedded Console reaches the S3/STS API at https://127.0.0.1:<port> (minioConfigToConsoleFeatures), a loopback endpoint whose TLS certificate is not expected to carry a 127.0.0.1 SAN. silo-console v2.3.x began verifying every outbound TLS peer, so the Console's STS AssumeRole handshake to that loopback endpoint now fails certificate validation and BOTH local and LDAP logins fail with a generic "invalid login". The failure happens in the Console HTTP client before any request reaches a server auth/STS/LDAP handler, so no server-side auth error is logged, matching the report. Restore the documented loopback bypass by opting the embedded Console into its endpoint-scoped CONSOLE_MINIO_SERVER_TLS_SKIP_VERIFY switch whenever the server falls back to the 127.0.0.1 endpoint under TLS. The exemption is scoped to that single loopback origin inside Console; every other HTTPS peer (IdP, Prometheus, webhooks) stays verified, preserving the v2.3.x hardening. An explicitly configured endpoint is reached under its own verified name and is never exempted. initConsoleServer unsets CONSOLE_* before re-deriving them, so the switch cannot be supplied by the operator on the embedded path; the server must assert it. Fixes #108 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01L7qJqWwy8oFA6aCXWRzXQe Signed-off-by: Feng Ruohang <rh@vonng.com>
428 lines
11 KiB
Go
428 lines
11 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 cmd
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"os"
|
|
"reflect"
|
|
"slices"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/minio/minio/internal/config"
|
|
)
|
|
|
|
func Test_readFromSecret(t *testing.T) {
|
|
testCases := []struct {
|
|
content string
|
|
expectedErr bool
|
|
expectedValue string
|
|
}{
|
|
{
|
|
"value\n",
|
|
false,
|
|
"value",
|
|
},
|
|
{
|
|
" \t\n Hello, Gophers \n\t\r\n",
|
|
false,
|
|
"Hello, Gophers",
|
|
},
|
|
}
|
|
|
|
for _, testCase := range testCases {
|
|
t.Run("", func(t *testing.T) {
|
|
tmpfile, err := os.CreateTemp(t.TempDir(), "testfile")
|
|
if err != nil {
|
|
t.Error(err)
|
|
}
|
|
tmpfile.WriteString(testCase.content)
|
|
tmpfile.Sync()
|
|
tmpfile.Close()
|
|
|
|
value, err := readFromSecret(tmpfile.Name())
|
|
if err != nil && !testCase.expectedErr {
|
|
t.Error(err)
|
|
}
|
|
if err == nil && testCase.expectedErr {
|
|
t.Error(errors.New("expected error, found success"))
|
|
}
|
|
if value != testCase.expectedValue {
|
|
t.Errorf("Expected %s, got %s", testCase.expectedValue, value)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func Test_minioEnvironFromFile(t *testing.T) {
|
|
testCases := []struct {
|
|
content string
|
|
expectedErr bool
|
|
expectedEkvs []envKV
|
|
}{
|
|
{
|
|
`
|
|
export MINIO_ROOT_USER=minio
|
|
export MINIO_ROOT_PASSWORD=minio123`,
|
|
false,
|
|
[]envKV{
|
|
{
|
|
Key: "MINIO_ROOT_USER",
|
|
Value: "minio",
|
|
},
|
|
{
|
|
Key: "MINIO_ROOT_PASSWORD",
|
|
Value: "minio123",
|
|
},
|
|
},
|
|
},
|
|
// Value with double quotes
|
|
{
|
|
`export MINIO_ROOT_USER="minio"`,
|
|
false,
|
|
[]envKV{
|
|
{
|
|
Key: "MINIO_ROOT_USER",
|
|
Value: "minio",
|
|
},
|
|
},
|
|
},
|
|
// Value with single quotes
|
|
{
|
|
`export MINIO_ROOT_USER='minio'`,
|
|
false,
|
|
[]envKV{
|
|
{
|
|
Key: "MINIO_ROOT_USER",
|
|
Value: "minio",
|
|
},
|
|
},
|
|
},
|
|
{
|
|
`
|
|
MINIO_ROOT_USER=minio
|
|
MINIO_ROOT_PASSWORD=minio123`,
|
|
false,
|
|
[]envKV{
|
|
{
|
|
Key: "MINIO_ROOT_USER",
|
|
Value: "minio",
|
|
},
|
|
{
|
|
Key: "MINIO_ROOT_PASSWORD",
|
|
Value: "minio123",
|
|
},
|
|
},
|
|
},
|
|
{
|
|
`
|
|
export MINIO_ROOT_USERminio
|
|
export MINIO_ROOT_PASSWORD=minio123`,
|
|
true,
|
|
nil,
|
|
},
|
|
{
|
|
`
|
|
# simple comment
|
|
# MINIO_ROOT_USER=minioadmin
|
|
# MINIO_ROOT_PASSWORD=minioadmin
|
|
MINIO_ROOT_USER=minio
|
|
MINIO_ROOT_PASSWORD=minio123`,
|
|
false,
|
|
[]envKV{
|
|
{
|
|
Key: "MINIO_ROOT_USER",
|
|
Value: "minio",
|
|
},
|
|
{
|
|
Key: "MINIO_ROOT_PASSWORD",
|
|
Value: "minio123",
|
|
},
|
|
},
|
|
},
|
|
}
|
|
for _, testCase := range testCases {
|
|
t.Run("", func(t *testing.T) {
|
|
tmpfile, err := os.CreateTemp(t.TempDir(), "testfile")
|
|
if err != nil {
|
|
t.Error(err)
|
|
}
|
|
tmpfile.WriteString(testCase.content)
|
|
tmpfile.Sync()
|
|
tmpfile.Close()
|
|
|
|
ekvs, err := minioEnvironFromFile(tmpfile.Name())
|
|
if err != nil && !testCase.expectedErr {
|
|
t.Error(err)
|
|
}
|
|
if err == nil && testCase.expectedErr {
|
|
t.Error(errors.New("expected error, found success"))
|
|
}
|
|
|
|
if len(ekvs) != len(testCase.expectedEkvs) {
|
|
t.Errorf("expected %v keys, got %v keys", len(testCase.expectedEkvs), len(ekvs))
|
|
}
|
|
|
|
if !reflect.DeepEqual(ekvs, testCase.expectedEkvs) {
|
|
t.Errorf("expected %v, got %v", testCase.expectedEkvs, ekvs)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func Test_minioEnvironFromFileWhitespaceAndValidation(t *testing.T) {
|
|
testCases := []struct {
|
|
name string
|
|
content string
|
|
want []envKV
|
|
errLine int
|
|
errContains string
|
|
errExcludes string
|
|
}{
|
|
{
|
|
name: "spaces and tabs around separator",
|
|
content: "MINIO_ROOT_USER = minio\nMINIO_ROOT_PASSWORD\t=\tminio123",
|
|
want: []envKV{
|
|
{Key: "MINIO_ROOT_USER", Value: "minio"},
|
|
{Key: "MINIO_ROOT_PASSWORD", Value: "minio123"},
|
|
},
|
|
},
|
|
{
|
|
name: "export tab and quoted spaces",
|
|
content: "export\tMINIO_ROOT_USER = \" minio user \"\nexport MINIO_ROOT_PASSWORD = ' minio secret '",
|
|
want: []envKV{
|
|
{Key: "MINIO_ROOT_USER", Value: " minio user "},
|
|
{Key: "MINIO_ROOT_PASSWORD", Value: " minio secret "},
|
|
},
|
|
},
|
|
{
|
|
name: "export Unicode whitespace",
|
|
content: "export\u00a0MINIO_ROOT_USER=value",
|
|
want: []envKV{
|
|
{Key: "MINIO_ROOT_USER", Value: "value"},
|
|
},
|
|
},
|
|
{
|
|
name: "export is only a standalone prefix",
|
|
content: "export=value\nexportFOO=bar",
|
|
want: []envKV{
|
|
{Key: "export", Value: "value"},
|
|
{Key: "exportFOO", Value: "bar"},
|
|
},
|
|
},
|
|
{
|
|
name: "unquoted whitespace empty value and additional separators",
|
|
content: "UNQUOTED = value \nEMPTY =\nTOKEN = scheme://user:password@example.com?a=b",
|
|
want: []envKV{
|
|
{Key: "UNQUOTED", Value: "value"},
|
|
{Key: "EMPTY", Value: ""},
|
|
{Key: "TOKEN", Value: "scheme://user:password@example.com?a=b"},
|
|
},
|
|
},
|
|
{
|
|
name: "valid underscore and digits",
|
|
content: "_VALID_2=value",
|
|
want: []envKV{
|
|
{Key: "_VALID_2", Value: "value"},
|
|
},
|
|
},
|
|
{
|
|
name: "named target punctuation and unicode",
|
|
content: "MINIO_NOTIFY_WEBHOOK_ENABLE_my-hook=off\n" +
|
|
"MINIO_NOTIFY_WEBHOOK_ENABLE_site.eu=off\n" +
|
|
"MINIO_NOTIFY_WEBHOOK_ENABLE_team:blue=off\n" +
|
|
"MINIO_NOTIFY_WEBHOOK_ENABLE_目标=off",
|
|
want: []envKV{
|
|
{Key: "MINIO_NOTIFY_WEBHOOK_ENABLE_my-hook", Value: "off"},
|
|
{Key: "MINIO_NOTIFY_WEBHOOK_ENABLE_site.eu", Value: "off"},
|
|
{Key: "MINIO_NOTIFY_WEBHOOK_ENABLE_team:blue", Value: "off"},
|
|
{Key: "MINIO_NOTIFY_WEBHOOK_ENABLE_目标", Value: "off"},
|
|
},
|
|
},
|
|
{
|
|
name: "missing separator redacts the line",
|
|
content: "MINIO_ROOT_PASSWORD=valid\nsuper-secret-without-equals",
|
|
errLine: 2,
|
|
errContains: "missing '='",
|
|
errExcludes: "super-secret-without-equals",
|
|
},
|
|
{
|
|
name: "empty name",
|
|
content: "=empty-name-secret",
|
|
errLine: 1,
|
|
errContains: `invalid environment variable name ""`,
|
|
errExcludes: "empty-name-secret",
|
|
},
|
|
{
|
|
name: "os compatible leading digit and punctuation",
|
|
content: "1MINIO_ROOT_USER=digit-leading-secret\n-MINIO-ROOT-USER=hyphen-secret",
|
|
want: []envKV{
|
|
{Key: "1MINIO_ROOT_USER", Value: "digit-leading-secret"},
|
|
{Key: "-MINIO-ROOT-USER", Value: "hyphen-secret"},
|
|
},
|
|
},
|
|
{
|
|
name: "whitespace in name",
|
|
content: "MINIO ROOT USER=whitespace-secret",
|
|
errLine: 1,
|
|
errContains: `invalid environment variable name "MINIO ROOT USER"`,
|
|
errExcludes: "whitespace-secret",
|
|
},
|
|
{
|
|
name: "NUL in name",
|
|
content: "MINIO\x00ROOT=nul-name-secret",
|
|
errLine: 1,
|
|
errContains: "invalid environment variable name",
|
|
errExcludes: "nul-name-secret",
|
|
},
|
|
{
|
|
name: "format character in name",
|
|
content: "MINIO\u200bROOT=format-secret",
|
|
errLine: 1,
|
|
errContains: "invalid environment variable name",
|
|
errExcludes: "format-secret",
|
|
},
|
|
{
|
|
name: "NUL in value",
|
|
content: "MINIO_ROOT_USER=before\x00nul-value-secret",
|
|
errLine: 1,
|
|
errContains: "environment variable value contains NUL",
|
|
errExcludes: "nul-value-secret",
|
|
},
|
|
{
|
|
name: "diagnostic has file and line but no value",
|
|
content: "MINIO_ROOT_USER=valid\nBAD KEY=super-secret-value",
|
|
errLine: 2,
|
|
errContains: `invalid environment variable name "BAD KEY"`,
|
|
errExcludes: "super-secret-value",
|
|
},
|
|
}
|
|
|
|
for _, testCase := range testCases {
|
|
t.Run(testCase.name, func(t *testing.T) {
|
|
tmpfile, err := os.CreateTemp(t.TempDir(), "testfile")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err = tmpfile.WriteString(testCase.content); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err = tmpfile.Close(); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
got, err := minioEnvironFromFile(tmpfile.Name())
|
|
if testCase.errContains == "" {
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if !reflect.DeepEqual(got, testCase.want) {
|
|
t.Errorf("expected %v, got %v", testCase.want, got)
|
|
}
|
|
return
|
|
}
|
|
|
|
if err == nil {
|
|
t.Fatal("expected an error")
|
|
}
|
|
errText := err.Error()
|
|
location := fmt.Sprintf("%s:%d:", tmpfile.Name(), testCase.errLine)
|
|
if !strings.Contains(errText, location) {
|
|
t.Errorf("expected error to contain %q, got %q", location, errText)
|
|
}
|
|
if !strings.Contains(errText, testCase.errContains) {
|
|
t.Errorf("expected error to contain %q, got %q", testCase.errContains, errText)
|
|
}
|
|
if testCase.errExcludes != "" && strings.Contains(errText, testCase.errExcludes) {
|
|
t.Errorf("expected error to redact %q, got %q", testCase.errExcludes, errText)
|
|
}
|
|
if got != nil {
|
|
t.Errorf("expected no entries on parse error, got %v", got)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestConfigEnvFileNamedTargetDiscovery(t *testing.T) {
|
|
key := "MINIO_NOTIFY_WEBHOOK_ENABLE_my-hook"
|
|
t.Setenv(key, "off")
|
|
|
|
targets, err := (config.Config{}).GetAvailableTargets(config.NotifyWebhookSubSys)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if !slices.Contains(targets, "my-hook") {
|
|
t.Fatalf("named target %q not discovered from %s: %v", "my-hook", key, targets)
|
|
}
|
|
}
|
|
|
|
// TestConsoleMinIOServerEnv locks in the loopback TLS exemption that keeps
|
|
// embedded Console login working (issue #108) while ensuring an explicitly
|
|
// configured endpoint is never silently exempted from TLS verification.
|
|
func TestConsoleMinIOServerEnv(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
endpoint string
|
|
isTLS bool
|
|
port string
|
|
wantServer string
|
|
wantSkipVerify bool
|
|
}{
|
|
{
|
|
name: "loopback TLS is exempted so embedded login works",
|
|
isTLS: true,
|
|
port: "9000",
|
|
wantServer: "https://127.0.0.1:9000",
|
|
wantSkipVerify: true,
|
|
},
|
|
{
|
|
name: "loopback plain HTTP needs no exemption",
|
|
isTLS: false,
|
|
port: "9000",
|
|
wantServer: "http://127.0.0.1:9000",
|
|
},
|
|
{
|
|
name: "explicit https endpoint stays verified",
|
|
endpoint: "https://silo.example:9000",
|
|
isTLS: true,
|
|
port: "9000",
|
|
wantServer: "https://silo.example:9000",
|
|
},
|
|
{
|
|
name: "explicit http endpoint stays verified",
|
|
endpoint: "http://silo.example:9000",
|
|
isTLS: false,
|
|
port: "9000",
|
|
wantServer: "http://silo.example:9000",
|
|
},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
server, skipVerify := consoleMinIOServerEnv(tt.endpoint, tt.isTLS, tt.port)
|
|
if server != tt.wantServer {
|
|
t.Fatalf("server = %q, want %q", server, tt.wantServer)
|
|
}
|
|
if skipVerify != tt.wantSkipVerify {
|
|
t.Fatalf("skipVerify = %v, want %v", skipVerify, tt.wantSkipVerify)
|
|
}
|
|
})
|
|
}
|
|
}
|