| 
 | 1 | +package kubernetes  | 
 | 2 | + | 
 | 3 | +import (  | 
 | 4 | +	"context"  | 
 | 5 | +	"os"  | 
 | 6 | +	"path/filepath"  | 
 | 7 | +	"strings"  | 
 | 8 | +	"testing"  | 
 | 9 | + | 
 | 10 | +	"github.com/containers/kubernetes-mcp-server/internal/test"  | 
 | 11 | +	"github.com/containers/kubernetes-mcp-server/pkg/config"  | 
 | 12 | +	"github.com/stretchr/testify/suite"  | 
 | 13 | +)  | 
 | 14 | + | 
 | 15 | +type DerivedTestSuite struct {  | 
 | 16 | +	suite.Suite  | 
 | 17 | +}  | 
 | 18 | + | 
 | 19 | +func (s *DerivedTestSuite) TestKubeConfig() {  | 
 | 20 | +	// Create a temporary kubeconfig file for testing  | 
 | 21 | +	tempDir := s.T().TempDir()  | 
 | 22 | +	kubeconfigPath := filepath.Join(tempDir, "config")  | 
 | 23 | +	kubeconfigContent := `  | 
 | 24 | +apiVersion: v1  | 
 | 25 | +kind: Config  | 
 | 26 | +clusters:  | 
 | 27 | +- cluster:  | 
 | 28 | +    server: https://test-cluster.example.com  | 
 | 29 | +  name: test-cluster  | 
 | 30 | +contexts:  | 
 | 31 | +- context:  | 
 | 32 | +    cluster: test-cluster  | 
 | 33 | +    user: test-user  | 
 | 34 | +  name: test-context  | 
 | 35 | +current-context: test-context  | 
 | 36 | +users:  | 
 | 37 | +- name: test-user  | 
 | 38 | +  user:  | 
 | 39 | +    username: test-username  | 
 | 40 | +    password: test-password  | 
 | 41 | +`  | 
 | 42 | +	err := os.WriteFile(kubeconfigPath, []byte(kubeconfigContent), 0644)  | 
 | 43 | +	s.Require().NoError(err, "failed to create kubeconfig file")  | 
 | 44 | + | 
 | 45 | +	s.Run("with no RequireOAuth (default) config", func() {  | 
 | 46 | +		testStaticConfig := test.Must(config.ReadToml([]byte(`  | 
 | 47 | +			kubeconfig = "` + strings.ReplaceAll(kubeconfigPath, `\`, `\\`) + `"  | 
 | 48 | +		`)))  | 
 | 49 | +		s.Run("without authorization header returns original manager", func() {  | 
 | 50 | +			testManager, err := NewManager(testStaticConfig)  | 
 | 51 | +			s.Require().NoErrorf(err, "failed to create test manager: %v", err)  | 
 | 52 | +			s.T().Cleanup(testManager.Close)  | 
 | 53 | + | 
 | 54 | +			derived, err := testManager.Derived(s.T().Context())  | 
 | 55 | +			s.Require().NoErrorf(err, "failed to create derived manager: %v", err)  | 
 | 56 | + | 
 | 57 | +			s.Equal(derived.manager, testManager, "expected original manager, got different manager")  | 
 | 58 | +		})  | 
 | 59 | + | 
 | 60 | +		s.Run("with invalid authorization header returns original manager", func() {  | 
 | 61 | +			testManager, err := NewManager(testStaticConfig)  | 
 | 62 | +			s.Require().NoErrorf(err, "failed to create test manager: %v", err)  | 
 | 63 | +			s.T().Cleanup(testManager.Close)  | 
 | 64 | + | 
 | 65 | +			ctx := context.WithValue(s.T().Context(), HeaderKey("Authorization"), "invalid-token")  | 
 | 66 | +			derived, err := testManager.Derived(ctx)  | 
 | 67 | +			s.Require().NoErrorf(err, "failed to create derived manager: %v", err)  | 
 | 68 | + | 
 | 69 | +			s.Equal(derived.manager, testManager, "expected original manager, got different manager")  | 
 | 70 | +		})  | 
 | 71 | + | 
 | 72 | +		s.Run("with valid bearer token creates derived manager with correct configuration", func() {  | 
 | 73 | +			testManager, err := NewManager(testStaticConfig)  | 
 | 74 | +			s.Require().NoErrorf(err, "failed to create test manager: %v", err)  | 
 | 75 | +			s.T().Cleanup(testManager.Close)  | 
 | 76 | + | 
 | 77 | +			ctx := context.WithValue(s.T().Context(), HeaderKey("Authorization"), "Bearer aiTana-julIA")  | 
 | 78 | +			derived, err := testManager.Derived(ctx)  | 
 | 79 | +			s.Require().NoErrorf(err, "failed to create derived manager: %v", err)  | 
 | 80 | + | 
 | 81 | +			s.NotEqual(derived.manager, testManager, "expected new derived manager, got original manager")  | 
 | 82 | +			s.Equal(derived.manager.staticConfig, testStaticConfig, "staticConfig not properly wired to derived manager")  | 
 | 83 | + | 
 | 84 | +			s.Run("RestConfig is correctly copied and sensitive fields are omitted", func() {  | 
 | 85 | +				derivedCfg := derived.manager.cfg  | 
 | 86 | +				s.Require().NotNil(derivedCfg, "derived config is nil")  | 
 | 87 | + | 
 | 88 | +				originalCfg := testManager.cfg  | 
 | 89 | +				s.Equalf(originalCfg.Host, derivedCfg.Host, "expected Host %s, got %s", originalCfg.Host, derivedCfg.Host)  | 
 | 90 | +				s.Equalf(originalCfg.APIPath, derivedCfg.APIPath, "expected APIPath %s, got %s", originalCfg.APIPath, derivedCfg.APIPath)  | 
 | 91 | +				s.Equalf(originalCfg.QPS, derivedCfg.QPS, "expected QPS %f, got %f", originalCfg.QPS, derivedCfg.QPS)  | 
 | 92 | +				s.Equalf(originalCfg.Burst, derivedCfg.Burst, "expected Burst %d, got %d", originalCfg.Burst, derivedCfg.Burst)  | 
 | 93 | +				s.Equalf(originalCfg.Timeout, derivedCfg.Timeout, "expected Timeout %v, got %v", originalCfg.Timeout, derivedCfg.Timeout)  | 
 | 94 | + | 
 | 95 | +				s.Equalf(originalCfg.Insecure, derivedCfg.Insecure, "expected TLS Insecure %v, got %v", originalCfg.Insecure, derivedCfg.Insecure)  | 
 | 96 | +				s.Equalf(originalCfg.ServerName, derivedCfg.ServerName, "expected TLS ServerName %s, got %s", originalCfg.ServerName, derivedCfg.ServerName)  | 
 | 97 | +				s.Equalf(originalCfg.CAFile, derivedCfg.CAFile, "expected TLS CAFile %s, got %s", originalCfg.CAFile, derivedCfg.CAFile)  | 
 | 98 | +				s.Equalf(string(originalCfg.CAData), string(derivedCfg.CAData), "expected TLS CAData %s, got %s", string(originalCfg.CAData), string(derivedCfg.CAData))  | 
 | 99 | + | 
 | 100 | +				s.Equalf("aiTana-julIA", derivedCfg.BearerToken, "expected BearerToken %s, got %s", "aiTana-julIA", derivedCfg.BearerToken)  | 
 | 101 | +				s.Equalf("kubernetes-mcp-server/bearer-token-auth", derivedCfg.UserAgent, "expected UserAgent \"kubernetes-mcp-server/bearer-token-auth\", got %s", derivedCfg.UserAgent)  | 
 | 102 | + | 
 | 103 | +				// Verify that sensitive fields are NOT copied to prevent credential leakage  | 
 | 104 | +				// The derived config should only use the bearer token from the Authorization header  | 
 | 105 | +				// and not inherit any authentication credentials from the original kubeconfig  | 
 | 106 | +				s.Emptyf(derivedCfg.CertFile, "expected TLS CertFile to be empty, got %s", derivedCfg.CertFile)  | 
 | 107 | +				s.Emptyf(derivedCfg.KeyFile, "expected TLS KeyFile to be empty, got %s", derivedCfg.KeyFile)  | 
 | 108 | +				s.Emptyf(len(derivedCfg.CertData), "expected TLS CertData to be empty, got %v", derivedCfg.CertData)  | 
 | 109 | +				s.Emptyf(len(derivedCfg.KeyData), "expected TLS KeyData to be empty, got %v", derivedCfg.KeyData)  | 
 | 110 | + | 
 | 111 | +				s.Emptyf(derivedCfg.Username, "expected Username to be empty, got %s", derivedCfg.Username)  | 
 | 112 | +				s.Emptyf(derivedCfg.Password, "expected Password to be empty, got %s", derivedCfg.Password)  | 
 | 113 | +				s.Nilf(derivedCfg.AuthProvider, "expected AuthProvider to be nil, got %v", derivedCfg.AuthProvider)  | 
 | 114 | +				s.Nilf(derivedCfg.ExecProvider, "expected ExecProvider to be nil, got %v", derivedCfg.ExecProvider)  | 
 | 115 | +				s.Emptyf(derivedCfg.BearerTokenFile, "expected BearerTokenFile to be empty, got %s", derivedCfg.BearerTokenFile)  | 
 | 116 | +				s.Emptyf(derivedCfg.Impersonate.UserName, "expected Impersonate.UserName to be empty, got %s", derivedCfg.Impersonate.UserName)  | 
 | 117 | + | 
 | 118 | +				// Verify that the original manager still has the sensitive data  | 
 | 119 | +				s.Falsef(originalCfg.Username == "" && originalCfg.Password == "", "original kubeconfig shouldn't be modified")  | 
 | 120 | + | 
 | 121 | +			})  | 
 | 122 | +			s.Run("derived manager has initialized clients", func() {  | 
 | 123 | +				// Verify that the derived manager has proper clients initialized  | 
 | 124 | +				s.NotNilf(derived.manager.accessControlClientSet, "expected accessControlClientSet to be initialized")  | 
 | 125 | +				s.Equalf(testStaticConfig, derived.manager.accessControlClientSet.staticConfig, "staticConfig not properly wired to derived manager")  | 
 | 126 | +				s.NotNilf(derived.manager.discoveryClient, "expected discoveryClient to be initialized")  | 
 | 127 | +				s.NotNilf(derived.manager.accessControlRESTMapper, "expected accessControlRESTMapper to be initialized")  | 
 | 128 | +				s.Equalf(testStaticConfig, derived.manager.accessControlRESTMapper.staticConfig, "staticConfig not properly wired to derived manager")  | 
 | 129 | +				s.NotNilf(derived.manager.dynamicClient, "expected dynamicClient to be initialized")  | 
 | 130 | +			})  | 
 | 131 | +		})  | 
 | 132 | +	})  | 
 | 133 | + | 
 | 134 | +	s.Run("with RequireOAuth=true", func() {  | 
 | 135 | +		testStaticConfig := test.Must(config.ReadToml([]byte(`  | 
 | 136 | +			kubeconfig = "` + strings.ReplaceAll(kubeconfigPath, `\`, `\\`) + `"  | 
 | 137 | +			require_oauth = true  | 
 | 138 | +		`)))  | 
 | 139 | + | 
 | 140 | +		s.Run("with no authorization header returns oauth token required error", func() {  | 
 | 141 | +			testManager, err := NewManager(testStaticConfig)  | 
 | 142 | +			s.Require().NoErrorf(err, "failed to create test manager: %v", err)  | 
 | 143 | +			s.T().Cleanup(testManager.Close)  | 
 | 144 | + | 
 | 145 | +			derived, err := testManager.Derived(s.T().Context())  | 
 | 146 | +			s.Require().Error(err, "expected error for missing oauth token, got nil")  | 
 | 147 | +			s.EqualError(err, "oauth token required", "expected error 'oauth token required', got %s", err.Error())  | 
 | 148 | +			s.Nil(derived, "expected nil derived manager when oauth token required")  | 
 | 149 | +		})  | 
 | 150 | + | 
 | 151 | +		s.Run("with invalid authorization header returns oauth token required error", func() {  | 
 | 152 | +			testManager, err := NewManager(testStaticConfig)  | 
 | 153 | +			s.Require().NoErrorf(err, "failed to create test manager: %v", err)  | 
 | 154 | +			s.T().Cleanup(testManager.Close)  | 
 | 155 | + | 
 | 156 | +			ctx := context.WithValue(s.T().Context(), HeaderKey("Authorization"), "invalid-token")  | 
 | 157 | +			derived, err := testManager.Derived(ctx)  | 
 | 158 | +			s.Require().Error(err, "expected error for invalid oauth token, got nil")  | 
 | 159 | +			s.EqualError(err, "oauth token required", "expected error 'oauth token required', got %s", err.Error())  | 
 | 160 | +			s.Nil(derived, "expected nil derived manager when oauth token required")  | 
 | 161 | +		})  | 
 | 162 | + | 
 | 163 | +		s.Run("with valid bearer token creates derived manager", func() {  | 
 | 164 | +			testManager, err := NewManager(testStaticConfig)  | 
 | 165 | +			s.Require().NoErrorf(err, "failed to create test manager: %v", err)  | 
 | 166 | +			s.T().Cleanup(testManager.Close)  | 
 | 167 | + | 
 | 168 | +			ctx := context.WithValue(s.T().Context(), HeaderKey("Authorization"), "Bearer aiTana-julIA")  | 
 | 169 | +			derived, err := testManager.Derived(ctx)  | 
 | 170 | +			s.Require().NoErrorf(err, "failed to create derived manager: %v", err)  | 
 | 171 | + | 
 | 172 | +			s.NotEqual(derived.manager, testManager, "expected new derived manager, got original manager")  | 
 | 173 | +			s.Equal(derived.manager.staticConfig, testStaticConfig, "staticConfig not properly wired to derived manager")  | 
 | 174 | + | 
 | 175 | +			derivedCfg := derived.manager.cfg  | 
 | 176 | +			s.Require().NotNil(derivedCfg, "derived config is nil")  | 
 | 177 | + | 
 | 178 | +			s.Equalf("aiTana-julIA", derivedCfg.BearerToken, "expected BearerToken %s, got %s", "aiTana-julIA", derivedCfg.BearerToken)  | 
 | 179 | +		})  | 
 | 180 | +	})  | 
 | 181 | +}  | 
 | 182 | + | 
 | 183 | +func TestDerived(t *testing.T) {  | 
 | 184 | +	suite.Run(t, new(DerivedTestSuite))  | 
 | 185 | +}  | 
0 commit comments