mirror of
https://github.com/vmware-tanzu/pinniped.git
synced 2026-09-10 10:06:07 +00:00
Fix some test expectations and also fix the production code in impersonator.go to make them compatible with both Go 1.27 and also with older versions, e.g. Go 1.26. With these changes, all unit tests pass when run with either Go 1.26 or Go 1.27. Signed-off-by: Ryan Richard <richardry@vmware.com>
547 lines
15 KiB
Go
547 lines
15 KiB
Go
// Copyright 2020-2026 the Pinniped contributors. All Rights Reserved.
|
|
// SPDX-License-Identifier: Apache-2.0
|
|
|
|
package supervisor
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"os"
|
|
"testing"
|
|
|
|
"github.com/stretchr/testify/require"
|
|
"k8s.io/utils/ptr"
|
|
|
|
"go.pinniped.dev/internal/here"
|
|
"go.pinniped.dev/internal/plog"
|
|
"go.pinniped.dev/internal/testutil"
|
|
)
|
|
|
|
func TestFromPath(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
yaml string
|
|
allowedCiphersError error
|
|
wantConfig *Config
|
|
wantError string
|
|
}{
|
|
{
|
|
name: "Happy",
|
|
yaml: here.Doc(`
|
|
---
|
|
apiGroupSuffix: some.suffix.com
|
|
labels:
|
|
myLabelKey1: myLabelValue1
|
|
myLabelKey2: myLabelValue2
|
|
names:
|
|
defaultTLSCertificateSecret: my-secret-name
|
|
endpoints:
|
|
https:
|
|
network: unix
|
|
address: :1234
|
|
http:
|
|
network: tcp
|
|
address: 127.0.0.1:1234
|
|
insecureAcceptExternalUnencryptedHttpRequests: false
|
|
log:
|
|
level: info
|
|
format: json
|
|
aggregatedAPIServerPort: 12345
|
|
aggregatedAPIServerDisableAdmissionPlugins:
|
|
- NamespaceLifecycle
|
|
- MutatingAdmissionWebhook
|
|
- ValidatingAdmissionPolicy
|
|
- ValidatingAdmissionWebhook
|
|
tls:
|
|
onedottwo:
|
|
allowedCiphers:
|
|
- foo
|
|
- bar
|
|
- TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305
|
|
audit:
|
|
logUsernamesAndGroups: enabled
|
|
logInternalPaths: enabled
|
|
oidc:
|
|
ignoreUserInfoEndpoint:
|
|
whenIssuerExactlyMatches:
|
|
- https://foo.com
|
|
- https://bar.com
|
|
`),
|
|
wantConfig: &Config{
|
|
APIGroupSuffix: ptr.To("some.suffix.com"),
|
|
Labels: map[string]string{
|
|
"myLabelKey1": "myLabelValue1",
|
|
"myLabelKey2": "myLabelValue2",
|
|
},
|
|
NamesConfig: NamesConfigSpec{
|
|
DefaultTLSCertificateSecret: "my-secret-name",
|
|
},
|
|
Endpoints: &Endpoints{
|
|
HTTPS: &Endpoint{
|
|
Network: "unix",
|
|
Address: ":1234",
|
|
},
|
|
HTTP: &Endpoint{
|
|
Network: "tcp",
|
|
Address: "127.0.0.1:1234",
|
|
},
|
|
},
|
|
Log: plog.LogSpec{
|
|
Level: plog.LevelInfo,
|
|
Format: plog.FormatJSON,
|
|
},
|
|
AggregatedAPIServerPort: ptr.To[int64](12345),
|
|
AggregatedAPIServerDisableAdmissionPlugins: []string{
|
|
"NamespaceLifecycle",
|
|
"MutatingAdmissionWebhook",
|
|
"ValidatingAdmissionPolicy",
|
|
"ValidatingAdmissionWebhook",
|
|
},
|
|
TLS: TLSSpec{
|
|
OneDotTwo: TLSProtocolSpec{
|
|
AllowedCiphers: []string{
|
|
"foo",
|
|
"bar",
|
|
"TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305",
|
|
},
|
|
},
|
|
},
|
|
Audit: AuditSpec{
|
|
LogUsernamesAndGroups: "enabled",
|
|
LogInternalPaths: "enabled",
|
|
},
|
|
OIDC: OIDCSpec{
|
|
IgnoreUserInfoEndpoint: IgnoreUserInfoEndpointSpec{
|
|
WhenIssuerExactlyMatches: []string{
|
|
"https://foo.com",
|
|
"https://bar.com",
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "cli is a bad log format when configured by the user",
|
|
yaml: here.Doc(`
|
|
---
|
|
names:
|
|
defaultTLSCertificateSecret: my-secret-name
|
|
log:
|
|
level: info
|
|
format: cli
|
|
`),
|
|
wantError: "decode yaml: error unmarshaling JSON: while decoding JSON: invalid log format, valid choices are the empty string or 'json'",
|
|
},
|
|
{
|
|
name: "When only the required fields are present, causes other fields to be defaulted",
|
|
yaml: here.Doc(`
|
|
---
|
|
names:
|
|
defaultTLSCertificateSecret: my-secret-name
|
|
`),
|
|
wantConfig: &Config{
|
|
APIGroupSuffix: ptr.To("pinniped.dev"),
|
|
Labels: map[string]string{},
|
|
NamesConfig: NamesConfigSpec{
|
|
DefaultTLSCertificateSecret: "my-secret-name",
|
|
},
|
|
Endpoints: &Endpoints{
|
|
HTTPS: &Endpoint{
|
|
Network: "tcp",
|
|
Address: ":8443",
|
|
},
|
|
HTTP: &Endpoint{
|
|
Network: "disabled",
|
|
},
|
|
},
|
|
AggregatedAPIServerPort: ptr.To[int64](10250),
|
|
Audit: AuditSpec{
|
|
LogInternalPaths: "",
|
|
LogUsernamesAndGroups: "",
|
|
},
|
|
AggregatedAPIServerDisableAdmissionPlugins: nil,
|
|
TLS: TLSSpec{},
|
|
Log: plog.LogSpec{},
|
|
OIDC: OIDCSpec{},
|
|
},
|
|
},
|
|
{
|
|
name: "audit settings can be disabled explicitly",
|
|
yaml: here.Doc(`
|
|
---
|
|
names:
|
|
defaultTLSCertificateSecret: my-secret-name
|
|
audit:
|
|
logInternalPaths: disabled
|
|
logUsernamesAndGroups: disabled
|
|
`),
|
|
wantConfig: &Config{
|
|
APIGroupSuffix: ptr.To("pinniped.dev"),
|
|
Labels: map[string]string{},
|
|
NamesConfig: NamesConfigSpec{
|
|
DefaultTLSCertificateSecret: "my-secret-name",
|
|
},
|
|
Endpoints: &Endpoints{
|
|
HTTPS: &Endpoint{
|
|
Network: "tcp",
|
|
Address: ":8443",
|
|
},
|
|
HTTP: &Endpoint{
|
|
Network: "disabled",
|
|
},
|
|
},
|
|
AggregatedAPIServerPort: ptr.To[int64](10250),
|
|
Audit: AuditSpec{
|
|
LogInternalPaths: "disabled",
|
|
LogUsernamesAndGroups: "disabled",
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "ignoreUserInfoEndpoint can be explicitly an empty list",
|
|
yaml: here.Doc(`
|
|
---
|
|
names:
|
|
defaultTLSCertificateSecret: my-secret-name
|
|
oidc:
|
|
ignoreUserInfoEndpoint:
|
|
whenIssuerExactlyMatches: []
|
|
`),
|
|
wantConfig: &Config{
|
|
APIGroupSuffix: ptr.To("pinniped.dev"),
|
|
Labels: map[string]string{},
|
|
NamesConfig: NamesConfigSpec{
|
|
DefaultTLSCertificateSecret: "my-secret-name",
|
|
},
|
|
Endpoints: &Endpoints{
|
|
HTTPS: &Endpoint{
|
|
Network: "tcp",
|
|
Address: ":8443",
|
|
},
|
|
HTTP: &Endpoint{
|
|
Network: "disabled",
|
|
},
|
|
},
|
|
AggregatedAPIServerPort: ptr.To[int64](10250),
|
|
OIDC: OIDCSpec{
|
|
IgnoreUserInfoEndpoint: IgnoreUserInfoEndpointSpec{WhenIssuerExactlyMatches: []string{}},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "all endpoints disabled",
|
|
yaml: here.Doc(`
|
|
---
|
|
names:
|
|
defaultTLSCertificateSecret: my-secret-name
|
|
endpoints:
|
|
https:
|
|
network: disabled
|
|
http:
|
|
network: disabled
|
|
`),
|
|
wantError: "validate endpoints: all endpoints are disabled",
|
|
},
|
|
{
|
|
name: "invalid https endpoint",
|
|
yaml: here.Doc(`
|
|
---
|
|
names:
|
|
defaultTLSCertificateSecret: my-secret-name
|
|
endpoints:
|
|
https:
|
|
network: foo
|
|
http:
|
|
network: disabled
|
|
`),
|
|
wantError: `validate https endpoint: unknown network "foo"`,
|
|
},
|
|
{
|
|
name: "invalid http endpoint",
|
|
yaml: here.Doc(`
|
|
---
|
|
names:
|
|
defaultTLSCertificateSecret: my-secret-name
|
|
endpoints:
|
|
https:
|
|
network: disabled
|
|
http:
|
|
network: bar
|
|
`),
|
|
wantError: `validate http endpoint: unknown network "bar"`,
|
|
},
|
|
{
|
|
name: "http endpoint uses tcp but binds to more than only loopback interfaces with insecureAcceptExternalUnencryptedHttpRequests missing",
|
|
yaml: here.Doc(`
|
|
---
|
|
names:
|
|
defaultTLSCertificateSecret: my-secret-name
|
|
endpoints:
|
|
https:
|
|
network: disabled
|
|
http:
|
|
network: tcp
|
|
address: :8080
|
|
`),
|
|
wantError: `validate http endpoint: http listener address ":8080" for "tcp" network may only bind to loopback interfaces`,
|
|
},
|
|
{
|
|
name: "http endpoint uses tcp but binds to more than only loopback interfaces",
|
|
yaml: here.Doc(`
|
|
---
|
|
names:
|
|
defaultTLSCertificateSecret: my-secret-name
|
|
endpoints:
|
|
https:
|
|
network: disabled
|
|
http:
|
|
network: tcp
|
|
address: :8080
|
|
`),
|
|
wantError: `validate http endpoint: http listener address ":8080" for "tcp" network may only bind to loopback interfaces`,
|
|
},
|
|
{
|
|
name: "endpoint disabled with non-empty address",
|
|
yaml: here.Doc(`
|
|
---
|
|
names:
|
|
defaultTLSCertificateSecret: my-secret-name
|
|
endpoints:
|
|
https:
|
|
network: disabled
|
|
address: wee
|
|
`),
|
|
wantError: `validate https endpoint: address set to "wee" when disabled, should be empty`,
|
|
},
|
|
{
|
|
name: "endpoint tcp with empty address",
|
|
yaml: here.Doc(`
|
|
---
|
|
names:
|
|
defaultTLSCertificateSecret: my-secret-name
|
|
endpoints:
|
|
http:
|
|
network: tcp
|
|
`),
|
|
wantError: `validate http endpoint: address must be set with "tcp" network`,
|
|
},
|
|
{
|
|
name: "endpoint unix with empty address",
|
|
yaml: here.Doc(`
|
|
---
|
|
names:
|
|
defaultTLSCertificateSecret: my-secret-name
|
|
endpoints:
|
|
https:
|
|
network: unix
|
|
`),
|
|
wantError: `validate https endpoint: address must be set with "unix" network`,
|
|
},
|
|
{
|
|
name: "Missing defaultTLSCertificateSecret name",
|
|
yaml: here.Doc(`
|
|
---
|
|
`),
|
|
wantError: "validate names: missing required names: defaultTLSCertificateSecret",
|
|
},
|
|
{
|
|
name: "apiGroupSuffix is prefixed with '.'",
|
|
yaml: here.Doc(`
|
|
---
|
|
apiGroupSuffix: .starts.with.dot
|
|
names:
|
|
defaultTLSCertificateSecret: my-secret-name
|
|
`),
|
|
wantError: "validate apiGroupSuffix: a lowercase RFC 1123 subdomain must consist of lower case alphanumeric characters, '-' or '.', and must start and end with an alphanumeric character (e.g. 'example.com', regex used for validation is '[a-z0-9]([-a-z0-9]*[a-z0-9])?(\\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*')",
|
|
},
|
|
{
|
|
name: "AggregatedAPIServerPortDefault too small",
|
|
yaml: here.Doc(`
|
|
---
|
|
aggregatedAPIServerPort: 1023
|
|
`),
|
|
wantError: "validate aggregatedAPIServerPort: must be within range 1024 to 65535",
|
|
},
|
|
{
|
|
name: "AggregatedAPIServerPortDefault too large",
|
|
yaml: here.Doc(`
|
|
---
|
|
aggregatedAPIServerPort: 65536
|
|
`),
|
|
wantError: "validate aggregatedAPIServerPort: must be within range 1024 to 65535",
|
|
},
|
|
{
|
|
name: "invalid audit.logUsernamesAndGroups format",
|
|
yaml: here.Doc(`
|
|
---
|
|
names:
|
|
defaultTLSCertificateSecret: my-secret-name
|
|
audit:
|
|
logUsernamesAndGroups: this-is-not-a-valid-value
|
|
`),
|
|
wantError: "validate audit: invalid logUsernamesAndGroups format, valid choices are 'enabled', 'disabled', or empty string (equivalent to 'disabled')",
|
|
},
|
|
{
|
|
name: "invalid audit.logInternalPaths format",
|
|
yaml: here.Doc(`
|
|
---
|
|
names:
|
|
defaultTLSCertificateSecret: my-secret-name
|
|
audit:
|
|
logInternalPaths: this-is-not-a-valid-value
|
|
`),
|
|
wantError: "validate audit: invalid logInternalPaths format, valid choices are 'enabled', 'disabled', or empty string (equivalent to 'disabled')",
|
|
},
|
|
{
|
|
name: "returns setAllowedCiphers errors",
|
|
yaml: here.Doc(`
|
|
---
|
|
names:
|
|
defaultTLSCertificateSecret: my-secret-name
|
|
tls:
|
|
onedottwo:
|
|
allowedCiphers:
|
|
- TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256
|
|
`),
|
|
allowedCiphersError: fmt.Errorf("some error from setAllowedCiphers"),
|
|
wantError: "validate tls: some error from setAllowedCiphers",
|
|
},
|
|
{
|
|
name: "invalid aggregatedAPIServerDisableAdmissionPlugins",
|
|
yaml: here.Doc(`
|
|
---
|
|
names:
|
|
defaultTLSCertificateSecret: my-secret-name
|
|
aggregatedAPIServerDisableAdmissionPlugins: [foobar, ValidatingAdmissionWebhook, foobaz]
|
|
`),
|
|
wantError: "validate aggregatedAPIServerDisableAdmissionPlugins: admission plugin names not recognized: [foobar foobaz] (each must be one of [NamespaceLifecycle MutatingAdmissionPolicy MutatingAdmissionWebhook ValidatingAdmissionPolicy ValidatingAdmissionWebhook])",
|
|
},
|
|
{
|
|
name: "invalid ignoreUserInfoEndpoint",
|
|
yaml: here.Doc(`
|
|
---
|
|
names:
|
|
defaultTLSCertificateSecret: my-secret-name
|
|
oidc:
|
|
ignoreUserInfoEndpoint: "should be a struct, but is a string"
|
|
`),
|
|
wantError: testutil.StringBasedOnGoVersion(testutil.GoVersionDependentString{
|
|
GoVersion: "go1.27",
|
|
BeforeVersion: "decode yaml: error unmarshaling JSON: while decoding JSON: json: cannot unmarshal string into Go struct field OIDCSpec.oidc.ignoreUserInfoEndpoint of type supervisor.IgnoreUserInfoEndpointSpec",
|
|
SinceVersion: "decode yaml: error unmarshaling JSON: while decoding JSON: json: cannot unmarshal string into Go struct field .oidc.ignoreUserInfoEndpoint of type supervisor.IgnoreUserInfoEndpointSpec",
|
|
}),
|
|
},
|
|
}
|
|
for _, test := range tests {
|
|
t.Run(test.name, func(t *testing.T) {
|
|
// this is a serial test because it sets the global logger
|
|
|
|
// Write yaml to temp file
|
|
f, err := os.CreateTemp("", "pinniped-test-config-yaml-*")
|
|
require.NoError(t, err)
|
|
t.Cleanup(func() {
|
|
err := os.Remove(f.Name())
|
|
require.NoError(t, err)
|
|
})
|
|
_, err = f.WriteString(test.yaml)
|
|
require.NoError(t, err)
|
|
err = f.Close()
|
|
require.NoError(t, err)
|
|
|
|
// Test FromPath()
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
t.Cleanup(cancel)
|
|
|
|
var actualCiphers []string
|
|
setAllowedCiphers := func(ciphers []string) error {
|
|
actualCiphers = ciphers
|
|
return test.allowedCiphersError
|
|
}
|
|
|
|
config, err := FromPath(ctx, f.Name(), setAllowedCiphers)
|
|
|
|
if test.wantError != "" {
|
|
require.EqualError(t, err, test.wantError)
|
|
return
|
|
}
|
|
|
|
require.NoError(t, err)
|
|
require.Equal(t, test.wantConfig, config)
|
|
require.Equal(t, test.wantConfig.TLS.OneDotTwo.AllowedCiphers, actualCiphers)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestAddrIsOnlyOnLoopback(t *testing.T) {
|
|
tests := []struct {
|
|
addr string
|
|
want bool
|
|
}{
|
|
{addr: "localhost:", want: true},
|
|
{addr: "localhost:0", want: true},
|
|
{addr: "localhost:80", want: true},
|
|
{addr: "localhost:http", want: true},
|
|
{addr: "ip6-localhost:", want: true},
|
|
{addr: "ip6-localhost:0", want: true},
|
|
{addr: "ip6-localhost:80", want: true},
|
|
{addr: "ip6-localhost:http", want: true},
|
|
{addr: "ip6-loopback:", want: true},
|
|
{addr: "ip6-loopback:0", want: true},
|
|
{addr: "ip6-loopback:80", want: true},
|
|
{addr: "ip6-loopback:http", want: true},
|
|
{addr: "127.0.0.1:", want: true},
|
|
{addr: "127.0.0.1:0", want: true},
|
|
{addr: "127.0.0.1:80", want: true},
|
|
{addr: "127.0.0.1:http", want: true},
|
|
{addr: "[::1]:", want: true},
|
|
{addr: "[::1]:0", want: true},
|
|
{addr: "[::1]:80", want: true},
|
|
{addr: "[::1]:http", want: true},
|
|
{addr: "[0:0:0:0:0:0:0:1]:", want: true},
|
|
{addr: "[0:0:0:0:0:0:0:1]:0", want: true},
|
|
{addr: "[0:0:0:0:0:0:0:1]:80", want: true},
|
|
{addr: "[0:0:0:0:0:0:0:1]:http", want: true},
|
|
{addr: "", want: false}, // illegal input, can't be empty
|
|
{addr: "host", want: false}, // illegal input, need colon
|
|
{addr: "localhost", want: false}, // illegal input, need colon
|
|
{addr: "127.0.0.1", want: false}, // illegal input, need colon
|
|
{addr: ":", want: false}, // illegal input, need either host or port
|
|
{addr: "2001:db8::1:80", want: false}, // illegal input, forgot square brackets
|
|
{addr: ":0", want: false},
|
|
{addr: ":80", want: false},
|
|
{addr: ":http", want: false},
|
|
{addr: "notlocalhost:", want: false},
|
|
{addr: "notlocalhost:0", want: false},
|
|
{addr: "notlocalhost:80", want: false},
|
|
{addr: "notlocalhost:http", want: false},
|
|
{addr: "0.0.0.0:", want: false},
|
|
{addr: "0.0.0.0:0", want: false},
|
|
{addr: "0.0.0.0:80", want: false},
|
|
{addr: "0.0.0.0:http", want: false},
|
|
{addr: "[::]:", want: false},
|
|
{addr: "[::]:0", want: false},
|
|
{addr: "[::]:80", want: false},
|
|
{addr: "[::]:http", want: false},
|
|
{addr: "42.42.42.42:", want: false},
|
|
{addr: "42.42.42.42:0", want: false},
|
|
{addr: "42.42.42.42:80", want: false},
|
|
{addr: "42.42.42.42:http", want: false},
|
|
{addr: "[2001:db8::1]:", want: false},
|
|
{addr: "[2001:db8::1]:0", want: false},
|
|
{addr: "[2001:db8::1]:80", want: false},
|
|
{addr: "[2001:db8::1]:http", want: false},
|
|
{addr: "[fe80::1%zone]:", want: false},
|
|
{addr: "[fe80::1%zone]:0", want: false},
|
|
{addr: "[fe80::1%zone]:80", want: false},
|
|
{addr: "[fe80::1%zone]:http", want: false},
|
|
}
|
|
for _, test := range tests {
|
|
tt := test
|
|
t.Run(fmt.Sprintf("address %s should be %t", tt.addr, tt.want), func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
require.Equal(t, tt.want, addrIsOnlyOnLoopback(tt.addr))
|
|
})
|
|
}
|
|
}
|