Files
teleport/lib/kube/proxy/auth_test.go
T
rosstimothy 7541d6d7d0 Introduce logtest package (#56741)
Moves all test related logger initialization and creation to the
logtest package to reduce testing symbols in production code. The
existing helpers in lib/utils have been left in place until the
enterprise references can be converted.

Updates #51023.
2025-07-15 18:11:10 +00:00

364 lines
11 KiB
Go

/*
* Teleport
* Copyright (C) 2023 Gravitational, Inc.
*
* 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 proxy
import (
"context"
"crypto/tls"
"errors"
"net/http"
"os"
"path/filepath"
"slices"
"testing"
"github.com/google/go-cmp/cmp"
"github.com/google/go-cmp/cmp/cmpopts"
"github.com/jonboulle/clockwork"
"github.com/stretchr/testify/require"
authzapi "k8s.io/api/authorization/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
apimachineryversion "k8s.io/apimachinery/pkg/version"
"k8s.io/client-go/kubernetes"
authztypes "k8s.io/client-go/kubernetes/typed/authorization/v1"
"k8s.io/client-go/rest"
"k8s.io/client-go/transport"
"github.com/gravitational/teleport/api/types"
testingkubemock "github.com/gravitational/teleport/lib/kube/proxy/testing/kube_server"
"github.com/gravitational/teleport/lib/service/servicecfg"
"github.com/gravitational/teleport/lib/utils/log/logtest"
)
func TestCheckImpersonationPermissions(t *testing.T) {
tests := []struct {
desc string
sarErr error
allowedVerbs []string
allowedResources []string
errAssertion require.ErrorAssertionFunc
}{
{
desc: "request failure",
sarErr: errors.New("uh oh"),
errAssertion: require.Error,
},
{
desc: "all permissions granted",
allowedVerbs: []string{"impersonate"},
allowedResources: []string{"users", "groups", "serviceaccounts"},
errAssertion: require.NoError,
},
{
desc: "missing some permissions",
allowedVerbs: []string{"impersonate"},
allowedResources: []string{"users"},
errAssertion: require.Error,
},
}
for _, tt := range tests {
mock := &mockSARClient{
err: tt.sarErr,
allowedVerbs: tt.allowedVerbs,
allowedResources: tt.allowedResources,
}
err := checkImpersonationPermissions(context.Background(), "test", mock)
tt.errAssertion(t, err)
}
}
type mockSARClient struct {
authztypes.SelfSubjectAccessReviewInterface
err error
allowedVerbs []string
allowedResources []string
}
func (c *mockSARClient) Create(_ context.Context, sar *authzapi.SelfSubjectAccessReview, _ metav1.CreateOptions) (*authzapi.SelfSubjectAccessReview, error) {
if c.err != nil {
return nil, c.err
}
var verbAllowed, resourceAllowed bool
if slices.Contains(c.allowedVerbs, sar.Spec.ResourceAttributes.Verb) {
verbAllowed = true
}
if slices.Contains(c.allowedResources, sar.Spec.ResourceAttributes.Resource) {
resourceAllowed = true
}
sar.Status.Allowed = verbAllowed && resourceAllowed
return sar, nil
}
func alwaysSucceeds(context.Context, string, authztypes.SelfSubjectAccessReviewInterface) error {
return nil
}
func failsForCluster(clusterName string) servicecfg.ImpersonationPermissionsChecker {
return func(ctx context.Context, cluster string, a authztypes.SelfSubjectAccessReviewInterface) error {
if cluster == clusterName {
return errors.New("Kaboom")
}
return nil
}
}
func TestGetKubeCreds(t *testing.T) {
t.Parallel()
// kubeMock is a Kubernetes API mock for the session tests.
kubeMock, err := testingkubemock.NewKubeAPIMock(testingkubemock.WithTeleportRoleCRD)
require.NoError(t, err)
t.Cleanup(func() { kubeMock.Close() })
targetAddr := kubeMock.Address
ctx := context.TODO()
const teleClusterName = "teleport-cluster"
dir := t.TempDir()
kubeconfigPath := filepath.Join(dir, "kubeconfig")
data := []byte(`
apiVersion: v1
kind: Config
clusters:
- cluster:
server: ` + kubeMock.URL + `
insecure-skip-tls-verify: true
name: example
contexts:
- context:
cluster: example
user: creds
name: foo
- context:
cluster: example
user: creds
name: bar
- context:
cluster: example
user: creds
name: baz
users:
- name: creds
current-context: foo
`)
err = os.WriteFile(kubeconfigPath, data, 0o600)
require.NoError(t, err)
tests := []struct {
desc string
kubeconfigPath string
kubeCluster string
serviceType KubeServiceType
impersonationCheck servicecfg.ImpersonationPermissionsChecker
want map[string]*kubeDetails
assertErr require.ErrorAssertionFunc
}{
{
desc: "kubernetes_service, no kube creds",
serviceType: KubeService,
impersonationCheck: alwaysSucceeds,
assertErr: require.Error,
want: map[string]*kubeDetails{},
}, {
desc: "proxy_service, no kube creds",
serviceType: ProxyService,
impersonationCheck: alwaysSucceeds,
assertErr: require.NoError,
want: map[string]*kubeDetails{},
}, {
desc: "legacy proxy_service, no kube creds",
serviceType: ProxyService,
impersonationCheck: alwaysSucceeds,
assertErr: require.NoError,
want: map[string]*kubeDetails{},
}, {
desc: "kubernetes_service, with kube creds",
serviceType: KubeService,
kubeconfigPath: kubeconfigPath,
impersonationCheck: alwaysSucceeds,
want: map[string]*kubeDetails{
"foo": {
kubeCreds: &staticKubeCreds{
targetAddr: targetAddr,
transportConfig: &transport.Config{},
kubeClient: &kubernetes.Clientset{},
clientRestCfg: &rest.Config{},
},
kubeCluster: mustCreateKubernetesClusterV3(t, "foo"),
kubeClusterVersion: &apimachineryversion.Info{
Major: "1",
Minor: "20",
GitVersion: "1.20.0",
},
},
"bar": {
kubeCreds: &staticKubeCreds{
targetAddr: targetAddr,
transportConfig: &transport.Config{},
kubeClient: &kubernetes.Clientset{},
clientRestCfg: &rest.Config{},
},
kubeClusterVersion: &apimachineryversion.Info{
Major: "1",
Minor: "20",
GitVersion: "1.20.0",
},
kubeCluster: mustCreateKubernetesClusterV3(t, "bar"),
},
"baz": {
kubeCreds: &staticKubeCreds{
targetAddr: targetAddr,
transportConfig: &transport.Config{},
kubeClient: &kubernetes.Clientset{},
clientRestCfg: &rest.Config{},
},
kubeClusterVersion: &apimachineryversion.Info{
Major: "1",
Minor: "20",
GitVersion: "1.20.0",
},
kubeCluster: mustCreateKubernetesClusterV3(t, "baz"),
},
},
assertErr: require.NoError,
}, {
desc: "proxy_service, with kube creds",
kubeconfigPath: kubeconfigPath,
serviceType: ProxyService,
impersonationCheck: alwaysSucceeds,
want: map[string]*kubeDetails{},
assertErr: require.NoError,
}, {
desc: "legacy proxy_service, with kube creds",
kubeconfigPath: kubeconfigPath,
serviceType: LegacyProxyService,
impersonationCheck: alwaysSucceeds,
want: map[string]*kubeDetails{
teleClusterName: {
kubeCreds: &staticKubeCreds{
targetAddr: targetAddr,
transportConfig: &transport.Config{},
kubeClient: &kubernetes.Clientset{},
clientRestCfg: &rest.Config{},
},
kubeClusterVersion: &apimachineryversion.Info{
Major: "1",
Minor: "20",
GitVersion: "1.20.0",
},
kubeCluster: mustCreateKubernetesClusterV3(t, teleClusterName),
},
},
assertErr: require.NoError,
}, {
desc: "Missing cluster does not fail operation",
kubeconfigPath: kubeconfigPath,
serviceType: KubeService,
impersonationCheck: failsForCluster("bar"),
want: map[string]*kubeDetails{
"foo": {
kubeCreds: &staticKubeCreds{
targetAddr: targetAddr,
transportConfig: &transport.Config{},
kubeClient: &kubernetes.Clientset{},
clientRestCfg: &rest.Config{},
},
kubeClusterVersion: &apimachineryversion.Info{
Major: "1",
Minor: "20",
GitVersion: "1.20.0",
},
kubeCluster: mustCreateKubernetesClusterV3(t, "foo"),
},
"bar": {
kubeCreds: &staticKubeCreds{
targetAddr: targetAddr,
transportConfig: &transport.Config{},
kubeClient: &kubernetes.Clientset{},
clientRestCfg: &rest.Config{},
},
kubeClusterVersion: &apimachineryversion.Info{
Major: "1",
Minor: "20",
GitVersion: "1.20.0",
},
kubeCluster: mustCreateKubernetesClusterV3(t, "bar"),
},
"baz": {
kubeCreds: &staticKubeCreds{
targetAddr: targetAddr,
transportConfig: &transport.Config{},
kubeClient: &kubernetes.Clientset{},
clientRestCfg: &rest.Config{},
},
kubeClusterVersion: &apimachineryversion.Info{
Major: "1",
Minor: "20",
GitVersion: "1.20.0",
},
kubeCluster: mustCreateKubernetesClusterV3(t, "baz"),
},
},
assertErr: require.NoError,
},
}
for _, tt := range tests {
t.Run(tt.desc, func(t *testing.T) {
t.Parallel()
fwd := &Forwarder{
clusterDetails: map[string]*kubeDetails{},
cfg: ForwarderConfig{
ClusterName: teleClusterName,
KubeServiceType: tt.serviceType,
KubeconfigPath: tt.kubeconfigPath,
CheckImpersonationPermissions: tt.impersonationCheck,
Clock: clockwork.NewFakeClock(),
},
log: logtest.NewLogger(),
}
err := fwd.getKubeDetails(ctx)
tt.assertErr(t, err)
if err != nil {
return
}
require.Empty(t, cmp.Diff(fwd.clusterDetails, tt.want,
cmp.AllowUnexported(staticKubeCreds{}),
cmp.AllowUnexported(kubeDetails{}),
cmpopts.IgnoreFields(kubeDetails{}, "rwMu", "kubeCodecs", "wg", "cancelFunc", "gvkSupportedResources"),
cmp.Comparer(func(a, b *transport.Config) bool { return (a == nil) == (b == nil) }),
cmp.Comparer(func(a, b *tls.Config) bool { return true }),
cmp.Comparer(func(a, b *kubernetes.Clientset) bool { return (a == nil) == (b == nil) }),
cmp.Comparer(func(a, b *rest.Config) bool { return (a == nil) == (b == nil) }),
cmp.Comparer(func(a, b http.RoundTripper) bool { return true }),
cmp.Comparer(func(a, b rbacSupportedResources) bool { return true }),
))
})
}
}
func mustCreateKubernetesClusterV3(t *testing.T, name string) *types.KubernetesClusterV3 {
kubeCluster, err := types.NewKubernetesClusterV3(types.Metadata{
Name: name,
}, types.KubernetesClusterSpecV3{})
require.NoError(t, err)
return kubeCluster
}