mirror of
https://github.com/portainer/portainer.git
synced 2025-07-29 18:29:44 +02:00
fix(transport): portainer generated kubeconfig causes kubectl exec fail [R8S-430] (#929)
This commit is contained in:
parent
bba3751268
commit
bdb2e2f417
12 changed files with 417 additions and 25 deletions
|
@ -451,7 +451,7 @@ func buildServer(flags *portainer.CLIFlags) portainer.Server {
|
|||
|
||||
snapshotService.Start()
|
||||
|
||||
proxyManager.NewProxyFactory(dataStore, signatureService, reverseTunnelService, dockerClientFactory, kubernetesClientFactory, kubernetesTokenCacheManager, gitService, snapshotService)
|
||||
proxyManager.NewProxyFactory(dataStore, signatureService, reverseTunnelService, dockerClientFactory, kubernetesClientFactory, kubernetesTokenCacheManager, gitService, snapshotService, jwtService)
|
||||
|
||||
helmPackageManager, err := initHelmPackageManager()
|
||||
if err != nil {
|
||||
|
|
|
@ -22,7 +22,7 @@ func TestEndpointDeleteEdgeGroupsConcurrently(t *testing.T) {
|
|||
handler := NewHandler(testhelpers.NewTestRequestBouncer())
|
||||
handler.DataStore = store
|
||||
handler.ProxyManager = proxy.NewManager(nil)
|
||||
handler.ProxyManager.NewProxyFactory(nil, nil, nil, nil, nil, nil, nil, nil)
|
||||
handler.ProxyManager.NewProxyFactory(nil, nil, nil, nil, nil, nil, nil, nil, nil)
|
||||
|
||||
// Create all the environments and add them to the same edge group
|
||||
|
||||
|
|
|
@ -24,11 +24,12 @@ type (
|
|||
kubernetesTokenCacheManager *kubernetes.TokenCacheManager
|
||||
gitService portainer.GitService
|
||||
snapshotService portainer.SnapshotService
|
||||
jwtService portainer.JWTService
|
||||
}
|
||||
)
|
||||
|
||||
// NewProxyFactory returns a pointer to a new instance of a ProxyFactory
|
||||
func NewProxyFactory(dataStore dataservices.DataStore, signatureService portainer.DigitalSignatureService, tunnelService portainer.ReverseTunnelService, clientFactory *dockerclient.ClientFactory, kubernetesClientFactory *cli.ClientFactory, kubernetesTokenCacheManager *kubernetes.TokenCacheManager, gitService portainer.GitService, snapshotService portainer.SnapshotService) *ProxyFactory {
|
||||
func NewProxyFactory(dataStore dataservices.DataStore, signatureService portainer.DigitalSignatureService, tunnelService portainer.ReverseTunnelService, clientFactory *dockerclient.ClientFactory, kubernetesClientFactory *cli.ClientFactory, kubernetesTokenCacheManager *kubernetes.TokenCacheManager, gitService portainer.GitService, snapshotService portainer.SnapshotService, jwtService portainer.JWTService) *ProxyFactory {
|
||||
return &ProxyFactory{
|
||||
dataStore: dataStore,
|
||||
signatureService: signatureService,
|
||||
|
@ -38,6 +39,7 @@ func NewProxyFactory(dataStore dataservices.DataStore, signatureService portaine
|
|||
kubernetesTokenCacheManager: kubernetesTokenCacheManager,
|
||||
gitService: gitService,
|
||||
snapshotService: snapshotService,
|
||||
jwtService: jwtService,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -38,7 +38,7 @@ func (factory *ProxyFactory) newKubernetesLocalProxy(endpoint *portainer.Endpoin
|
|||
return nil, err
|
||||
}
|
||||
|
||||
transport, err := kubernetes.NewLocalTransport(tokenManager, endpoint, factory.kubernetesClientFactory, factory.dataStore)
|
||||
transport, err := kubernetes.NewLocalTransport(tokenManager, endpoint, factory.kubernetesClientFactory, factory.dataStore, factory.jwtService)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
@ -74,7 +74,7 @@ func (factory *ProxyFactory) newKubernetesEdgeHTTPProxy(endpoint *portainer.Endp
|
|||
|
||||
endpointURL.Scheme = "http"
|
||||
proxy := NewSingleHostReverseProxyWithHostHeader(endpointURL)
|
||||
proxy.Transport = kubernetes.NewEdgeTransport(factory.dataStore, factory.signatureService, factory.reverseTunnelService, endpoint, tokenManager, factory.kubernetesClientFactory)
|
||||
proxy.Transport = kubernetes.NewEdgeTransport(factory.dataStore, factory.signatureService, factory.reverseTunnelService, endpoint, tokenManager, factory.kubernetesClientFactory, factory.jwtService)
|
||||
|
||||
return proxy, nil
|
||||
}
|
||||
|
@ -105,7 +105,7 @@ func (factory *ProxyFactory) newKubernetesAgentHTTPSProxy(endpoint *portainer.En
|
|||
}
|
||||
|
||||
proxy := NewSingleHostReverseProxyWithHostHeader(remoteURL)
|
||||
proxy.Transport = kubernetes.NewAgentTransport(factory.signatureService, tlsConfig, tokenManager, endpoint, factory.kubernetesClientFactory, factory.dataStore)
|
||||
proxy.Transport = kubernetes.NewAgentTransport(factory.signatureService, tlsConfig, tokenManager, endpoint, factory.kubernetesClientFactory, factory.dataStore, factory.jwtService)
|
||||
|
||||
return proxy, nil
|
||||
}
|
||||
|
|
|
@ -16,7 +16,7 @@ type agentTransport struct {
|
|||
}
|
||||
|
||||
// NewAgentTransport returns a new transport that can be used to send signed requests to a Portainer agent
|
||||
func NewAgentTransport(signatureService portainer.DigitalSignatureService, tlsConfig *tls.Config, tokenManager *tokenManager, endpoint *portainer.Endpoint, k8sClientFactory *cli.ClientFactory, dataStore dataservices.DataStore) *agentTransport {
|
||||
func NewAgentTransport(signatureService portainer.DigitalSignatureService, tlsConfig *tls.Config, tokenManager *tokenManager, endpoint *portainer.Endpoint, k8sClientFactory *cli.ClientFactory, dataStore dataservices.DataStore, jwtService portainer.JWTService) *agentTransport {
|
||||
transport := &agentTransport{
|
||||
baseTransport: newBaseTransport(
|
||||
&http.Transport{
|
||||
|
@ -26,6 +26,7 @@ func NewAgentTransport(signatureService portainer.DigitalSignatureService, tlsCo
|
|||
endpoint,
|
||||
k8sClientFactory,
|
||||
dataStore,
|
||||
jwtService,
|
||||
),
|
||||
signatureService: signatureService,
|
||||
}
|
||||
|
|
|
@ -16,7 +16,7 @@ type edgeTransport struct {
|
|||
}
|
||||
|
||||
// NewAgentTransport returns a new transport that can be used to send signed requests to a Portainer Edge agent
|
||||
func NewEdgeTransport(dataStore dataservices.DataStore, signatureService portainer.DigitalSignatureService, reverseTunnelService portainer.ReverseTunnelService, endpoint *portainer.Endpoint, tokenManager *tokenManager, k8sClientFactory *cli.ClientFactory) *edgeTransport {
|
||||
func NewEdgeTransport(dataStore dataservices.DataStore, signatureService portainer.DigitalSignatureService, reverseTunnelService portainer.ReverseTunnelService, endpoint *portainer.Endpoint, tokenManager *tokenManager, k8sClientFactory *cli.ClientFactory, jwtService portainer.JWTService) *edgeTransport {
|
||||
transport := &edgeTransport{
|
||||
reverseTunnelService: reverseTunnelService,
|
||||
signatureService: signatureService,
|
||||
|
@ -26,6 +26,7 @@ func NewEdgeTransport(dataStore dataservices.DataStore, signatureService portain
|
|||
endpoint,
|
||||
k8sClientFactory,
|
||||
dataStore,
|
||||
jwtService,
|
||||
),
|
||||
}
|
||||
|
||||
|
|
|
@ -14,7 +14,7 @@ type localTransport struct {
|
|||
}
|
||||
|
||||
// NewLocalTransport returns a new transport that can be used to send requests to the local Kubernetes API
|
||||
func NewLocalTransport(tokenManager *tokenManager, endpoint *portainer.Endpoint, k8sClientFactory *cli.ClientFactory, dataStore dataservices.DataStore) (*localTransport, error) {
|
||||
func NewLocalTransport(tokenManager *tokenManager, endpoint *portainer.Endpoint, k8sClientFactory *cli.ClientFactory, dataStore dataservices.DataStore, jwtService portainer.JWTService) (*localTransport, error) {
|
||||
config, err := crypto.CreateTLSConfigurationFromBytes(nil, nil, nil, true, true)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
@ -29,6 +29,7 @@ func NewLocalTransport(tokenManager *tokenManager, endpoint *portainer.Endpoint,
|
|||
endpoint,
|
||||
k8sClientFactory,
|
||||
dataStore,
|
||||
jwtService,
|
||||
),
|
||||
}
|
||||
|
||||
|
|
|
@ -2,12 +2,18 @@ package kubernetes
|
|||
|
||||
import (
|
||||
"net/http"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func (transport *baseTransport) proxyPodsRequest(request *http.Request, namespace, requestPath string) (*http.Response, error) {
|
||||
func (transport *baseTransport) proxyPodsRequest(request *http.Request, namespace string) (*http.Response, error) {
|
||||
if request.Method == http.MethodDelete {
|
||||
transport.refreshRegistry(request, namespace)
|
||||
}
|
||||
|
||||
if request.Method == http.MethodPost && strings.Contains(request.URL.Path, "/exec") {
|
||||
if err := transport.addTokenForExec(request); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
return transport.executeKubernetesRequest(request)
|
||||
}
|
||||
|
|
|
@ -26,15 +26,17 @@ type baseTransport struct {
|
|||
endpoint *portainer.Endpoint
|
||||
k8sClientFactory *cli.ClientFactory
|
||||
dataStore dataservices.DataStore
|
||||
jwtService portainer.JWTService
|
||||
}
|
||||
|
||||
func newBaseTransport(httpTransport *http.Transport, tokenManager *tokenManager, endpoint *portainer.Endpoint, k8sClientFactory *cli.ClientFactory, dataStore dataservices.DataStore) *baseTransport {
|
||||
func newBaseTransport(httpTransport *http.Transport, tokenManager *tokenManager, endpoint *portainer.Endpoint, k8sClientFactory *cli.ClientFactory, dataStore dataservices.DataStore, jwtService portainer.JWTService) *baseTransport {
|
||||
return &baseTransport{
|
||||
httpTransport: httpTransport,
|
||||
tokenManager: tokenManager,
|
||||
endpoint: endpoint,
|
||||
k8sClientFactory: k8sClientFactory,
|
||||
dataStore: dataStore,
|
||||
jwtService: jwtService,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -82,7 +84,7 @@ func (transport *baseTransport) proxyNamespacedRequest(request *http.Request, fu
|
|||
|
||||
switch {
|
||||
case strings.HasPrefix(requestPath, "pods"):
|
||||
return transport.proxyPodsRequest(request, namespace, requestPath)
|
||||
return transport.proxyPodsRequest(request, namespace)
|
||||
case strings.HasPrefix(requestPath, "deployments"):
|
||||
return transport.proxyDeploymentsRequest(request, namespace, requestPath)
|
||||
case requestPath == "" && request.Method == "DELETE":
|
||||
|
@ -92,6 +94,23 @@ func (transport *baseTransport) proxyNamespacedRequest(request *http.Request, fu
|
|||
}
|
||||
}
|
||||
|
||||
// addTokenForExec injects a kubeconfig token into the request header
|
||||
// this is only used with kubeconfig for kubectl exec requests
|
||||
func (transport *baseTransport) addTokenForExec(request *http.Request) error {
|
||||
tokenData, err := security.RetrieveTokenData(request)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
token, err := transport.jwtService.GenerateTokenForKubeconfig(tokenData)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
request.Header.Set("Authorization", "Bearer "+token)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (transport *baseTransport) executeKubernetesRequest(request *http.Request) (*http.Response, error) {
|
||||
|
||||
resp, err := transport.httpTransport.RoundTrip(request)
|
||||
|
|
359
api/http/proxy/factory/kubernetes/transport_test.go
Normal file
359
api/http/proxy/factory/kubernetes/transport_test.go
Normal file
|
@ -0,0 +1,359 @@
|
|||
package kubernetes
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/datastore"
|
||||
"github.com/portainer/portainer/api/http/security"
|
||||
"github.com/portainer/portainer/api/jwt"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// MockJWTService implements portainer.JWTService for testing
|
||||
type MockJWTService struct {
|
||||
generateTokenFunc func(data *portainer.TokenData) (string, error)
|
||||
}
|
||||
|
||||
func (m *MockJWTService) GenerateToken(data *portainer.TokenData) (string, time.Time, error) {
|
||||
if m.generateTokenFunc != nil {
|
||||
token, err := m.generateTokenFunc(data)
|
||||
return token, time.Now().Add(24 * time.Hour), err
|
||||
}
|
||||
return "mock-token", time.Now().Add(24 * time.Hour), nil
|
||||
}
|
||||
|
||||
func (m *MockJWTService) GenerateTokenForKubeconfig(data *portainer.TokenData) (string, error) {
|
||||
if m.generateTokenFunc != nil {
|
||||
return m.generateTokenFunc(data)
|
||||
}
|
||||
return "mock-kubeconfig-token", nil
|
||||
}
|
||||
|
||||
func (m *MockJWTService) ParseAndVerifyToken(token string) (*portainer.TokenData, string, time.Time, error) {
|
||||
return &portainer.TokenData{ID: 1, Username: "mock", Role: portainer.AdministratorRole}, "mock-id", time.Now().Add(24 * time.Hour), nil
|
||||
}
|
||||
|
||||
func (m *MockJWTService) SetUserSessionDuration(userSessionDuration time.Duration) {
|
||||
// Mock implementation - not used in tests
|
||||
}
|
||||
|
||||
func TestBaseTransport_AddTokenForExec(t *testing.T) {
|
||||
// Setup test store and JWT service
|
||||
_, store := datastore.MustNewTestStore(t, true, false)
|
||||
|
||||
// Create test users
|
||||
adminUser := &portainer.User{
|
||||
ID: 1,
|
||||
Username: "admin",
|
||||
Role: portainer.AdministratorRole,
|
||||
}
|
||||
err := store.User().Create(adminUser)
|
||||
require.NoError(t, err)
|
||||
|
||||
standardUser := &portainer.User{
|
||||
ID: 2,
|
||||
Username: "standard",
|
||||
Role: portainer.StandardUserRole,
|
||||
}
|
||||
err = store.User().Create(standardUser)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Create JWT service
|
||||
jwtService, err := jwt.NewService("24h", store)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Create base transport
|
||||
transport := &baseTransport{
|
||||
jwtService: jwtService,
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
tokenData *portainer.TokenData
|
||||
setupRequest func(*http.Request) *http.Request
|
||||
expectError bool
|
||||
errorMsg string
|
||||
expectPanic bool
|
||||
verifyResponse func(*testing.T, *http.Request, *portainer.TokenData)
|
||||
}{
|
||||
{
|
||||
name: "admin user - successful token generation",
|
||||
tokenData: &portainer.TokenData{
|
||||
ID: adminUser.ID,
|
||||
Username: adminUser.Username,
|
||||
Role: adminUser.Role,
|
||||
},
|
||||
setupRequest: func(req *http.Request) *http.Request {
|
||||
return req.WithContext(security.StoreTokenData(req, &portainer.TokenData{
|
||||
ID: adminUser.ID,
|
||||
Username: adminUser.Username,
|
||||
Role: adminUser.Role,
|
||||
}))
|
||||
},
|
||||
expectError: false,
|
||||
verifyResponse: func(t *testing.T, req *http.Request, tokenData *portainer.TokenData) {
|
||||
authHeader := req.Header.Get("Authorization")
|
||||
assert.NotEmpty(t, authHeader)
|
||||
assert.True(t, strings.HasPrefix(authHeader, "Bearer "))
|
||||
|
||||
token := authHeader[7:] // Remove "Bearer " prefix
|
||||
parsedTokenData, _, _, err := jwtService.ParseAndVerifyToken(token)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, tokenData.ID, parsedTokenData.ID)
|
||||
assert.Equal(t, tokenData.Username, parsedTokenData.Username)
|
||||
assert.Equal(t, tokenData.Role, parsedTokenData.Role)
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "standard user - successful token generation",
|
||||
tokenData: &portainer.TokenData{
|
||||
ID: standardUser.ID,
|
||||
Username: standardUser.Username,
|
||||
Role: standardUser.Role,
|
||||
},
|
||||
setupRequest: func(req *http.Request) *http.Request {
|
||||
return req.WithContext(security.StoreTokenData(req, &portainer.TokenData{
|
||||
ID: standardUser.ID,
|
||||
Username: standardUser.Username,
|
||||
Role: standardUser.Role,
|
||||
}))
|
||||
},
|
||||
expectError: false,
|
||||
verifyResponse: func(t *testing.T, req *http.Request, tokenData *portainer.TokenData) {
|
||||
authHeader := req.Header.Get("Authorization")
|
||||
assert.NotEmpty(t, authHeader)
|
||||
assert.True(t, strings.HasPrefix(authHeader, "Bearer "))
|
||||
|
||||
token := authHeader[7:] // Remove "Bearer " prefix
|
||||
parsedTokenData, _, _, err := jwtService.ParseAndVerifyToken(token)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, tokenData.ID, parsedTokenData.ID)
|
||||
assert.Equal(t, tokenData.Username, parsedTokenData.Username)
|
||||
assert.Equal(t, tokenData.Role, parsedTokenData.Role)
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "request without token data in context",
|
||||
tokenData: nil,
|
||||
setupRequest: func(req *http.Request) *http.Request {
|
||||
return req // Don't add token data to context
|
||||
},
|
||||
expectError: true,
|
||||
errorMsg: "Unable to find JWT data in request context",
|
||||
},
|
||||
{
|
||||
name: "request with nil token data",
|
||||
tokenData: nil,
|
||||
setupRequest: func(req *http.Request) *http.Request {
|
||||
return req.WithContext(security.StoreTokenData(req, nil))
|
||||
},
|
||||
expectPanic: true,
|
||||
},
|
||||
{
|
||||
name: "JWT service failure",
|
||||
tokenData: &portainer.TokenData{
|
||||
ID: 1,
|
||||
Username: "test",
|
||||
Role: portainer.AdministratorRole,
|
||||
},
|
||||
setupRequest: func(req *http.Request) *http.Request {
|
||||
return req.WithContext(security.StoreTokenData(req, &portainer.TokenData{
|
||||
ID: 1,
|
||||
Username: "test",
|
||||
Role: portainer.AdministratorRole,
|
||||
}))
|
||||
},
|
||||
expectPanic: true,
|
||||
},
|
||||
{
|
||||
name: "verify authorization header format",
|
||||
tokenData: &portainer.TokenData{
|
||||
ID: adminUser.ID,
|
||||
Username: adminUser.Username,
|
||||
Role: adminUser.Role,
|
||||
},
|
||||
setupRequest: func(req *http.Request) *http.Request {
|
||||
return req.WithContext(security.StoreTokenData(req, &portainer.TokenData{
|
||||
ID: adminUser.ID,
|
||||
Username: adminUser.Username,
|
||||
Role: adminUser.Role,
|
||||
}))
|
||||
},
|
||||
expectError: false,
|
||||
verifyResponse: func(t *testing.T, req *http.Request, tokenData *portainer.TokenData) {
|
||||
authHeader := req.Header.Get("Authorization")
|
||||
assert.NotEmpty(t, authHeader)
|
||||
assert.True(t, strings.HasPrefix(authHeader, "Bearer "))
|
||||
|
||||
token := authHeader[7:] // Remove "Bearer " prefix
|
||||
assert.NotEmpty(t, token)
|
||||
assert.Greater(t, len(token), 0, "Token should not be empty")
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "verify header is overwritten on subsequent calls",
|
||||
tokenData: &portainer.TokenData{
|
||||
ID: adminUser.ID,
|
||||
Username: adminUser.Username,
|
||||
Role: adminUser.Role,
|
||||
},
|
||||
setupRequest: func(req *http.Request) *http.Request {
|
||||
req = req.WithContext(security.StoreTokenData(req, &portainer.TokenData{
|
||||
ID: adminUser.ID,
|
||||
Username: adminUser.Username,
|
||||
Role: adminUser.Role,
|
||||
}))
|
||||
// Set an existing Authorization header
|
||||
req.Header.Set("Authorization", "Bearer old-token")
|
||||
return req
|
||||
},
|
||||
expectError: false,
|
||||
verifyResponse: func(t *testing.T, req *http.Request, tokenData *portainer.TokenData) {
|
||||
authHeader := req.Header.Get("Authorization")
|
||||
assert.NotEqual(t, "Bearer old-token", authHeader)
|
||||
assert.True(t, strings.HasPrefix(authHeader, "Bearer "))
|
||||
|
||||
token := authHeader[7:] // Remove "Bearer " prefix
|
||||
parsedTokenData, _, _, err := jwtService.ParseAndVerifyToken(token)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, tokenData.ID, parsedTokenData.ID)
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
// Create request
|
||||
request := httptest.NewRequest("GET", "/", nil)
|
||||
request = tt.setupRequest(request)
|
||||
|
||||
// Determine which transport to use based on test case
|
||||
var testTransport *baseTransport
|
||||
if tt.name == "JWT service failure" {
|
||||
testTransport = &baseTransport{
|
||||
jwtService: nil,
|
||||
}
|
||||
} else {
|
||||
testTransport = transport
|
||||
}
|
||||
|
||||
// Call the function
|
||||
if tt.expectPanic {
|
||||
assert.Panics(t, func() {
|
||||
_ = testTransport.addTokenForExec(request)
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
err := testTransport.addTokenForExec(request)
|
||||
|
||||
// Check results
|
||||
if tt.expectError {
|
||||
assert.Error(t, err)
|
||||
if tt.errorMsg != "" {
|
||||
assert.Contains(t, err.Error(), tt.errorMsg)
|
||||
}
|
||||
} else {
|
||||
assert.NoError(t, err)
|
||||
if tt.verifyResponse != nil {
|
||||
tt.verifyResponse(t, request, tt.tokenData)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestBaseTransport_AddTokenForExec_Integration(t *testing.T) {
|
||||
// Create a test HTTP server to capture requests
|
||||
var capturedRequest *http.Request
|
||||
var capturedHeaders http.Header
|
||||
|
||||
testServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
capturedRequest = r
|
||||
capturedHeaders = r.Header.Clone()
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Write([]byte("success"))
|
||||
}))
|
||||
defer testServer.Close()
|
||||
|
||||
// Create mock JWT service
|
||||
mockJWTService := &MockJWTService{
|
||||
generateTokenFunc: func(data *portainer.TokenData) (string, error) {
|
||||
return "mock-token-" + data.Username, nil
|
||||
},
|
||||
}
|
||||
|
||||
// Create base transport
|
||||
transport := &baseTransport{
|
||||
httpTransport: &http.Transport{},
|
||||
jwtService: mockJWTService,
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
tokenData *portainer.TokenData
|
||||
requestPath string
|
||||
expectedToken string
|
||||
}{
|
||||
{
|
||||
name: "admin user exec request",
|
||||
tokenData: &portainer.TokenData{
|
||||
ID: 1,
|
||||
Username: "admin",
|
||||
Role: portainer.AdministratorRole,
|
||||
},
|
||||
requestPath: "/api/endpoints/1/kubernetes/api/v1/namespaces/default/pods/test-pod/exec",
|
||||
expectedToken: "mock-token-admin",
|
||||
},
|
||||
{
|
||||
name: "standard user exec request",
|
||||
tokenData: &portainer.TokenData{
|
||||
ID: 2,
|
||||
Username: "standard",
|
||||
Role: portainer.StandardUserRole,
|
||||
},
|
||||
requestPath: "/api/endpoints/1/kubernetes/api/v1/namespaces/default/pods/test-pod/exec",
|
||||
expectedToken: "mock-token-standard",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
// Reset captured data
|
||||
capturedRequest = nil
|
||||
capturedHeaders = nil
|
||||
|
||||
// Create request to the test server
|
||||
request, err := http.NewRequest("POST", testServer.URL+tt.requestPath, strings.NewReader(""))
|
||||
require.NoError(t, err)
|
||||
|
||||
// Add token data to request context
|
||||
request = request.WithContext(security.StoreTokenData(request, tt.tokenData))
|
||||
|
||||
// Call proxyPodsRequest which triggers addTokenForExec for POST /exec requests
|
||||
resp, err := transport.proxyPodsRequest(request, "default")
|
||||
require.NoError(t, err)
|
||||
defer resp.Body.Close()
|
||||
|
||||
// Verify the response
|
||||
assert.Equal(t, http.StatusOK, resp.StatusCode)
|
||||
|
||||
// Verify the request was captured
|
||||
assert.NotNil(t, capturedRequest)
|
||||
assert.Equal(t, "POST", capturedRequest.Method)
|
||||
assert.Equal(t, tt.requestPath, capturedRequest.URL.Path)
|
||||
|
||||
// Verify the authorization header was set correctly
|
||||
capturedAuthHeader := capturedHeaders.Get("Authorization")
|
||||
assert.NotEmpty(t, capturedAuthHeader)
|
||||
assert.True(t, strings.HasPrefix(capturedAuthHeader, "Bearer "))
|
||||
assert.Equal(t, "Bearer "+tt.expectedToken, capturedAuthHeader)
|
||||
})
|
||||
}
|
||||
}
|
|
@ -13,13 +13,16 @@ var allowedHeaders = map[string]struct{}{
|
|||
"Accept-Encoding": {},
|
||||
"Accept-Language": {},
|
||||
"Cache-Control": {},
|
||||
"Connection": {},
|
||||
"Content-Length": {},
|
||||
"Content-Type": {},
|
||||
"Private-Token": {},
|
||||
"Upgrade": {},
|
||||
"User-Agent": {},
|
||||
"X-Portaineragent-Target": {},
|
||||
"X-Portainer-Volumename": {},
|
||||
"X-Registry-Auth": {},
|
||||
"X-Stream-Protocol-Version": {},
|
||||
}
|
||||
|
||||
// newSingleHostReverseProxyWithHostHeader is based on NewSingleHostReverseProxy
|
||||
|
|
|
@ -32,8 +32,8 @@ func NewManager(kubernetesClientFactory *cli.ClientFactory) *Manager {
|
|||
}
|
||||
}
|
||||
|
||||
func (manager *Manager) NewProxyFactory(dataStore dataservices.DataStore, signatureService portainer.DigitalSignatureService, tunnelService portainer.ReverseTunnelService, clientFactory *dockerclient.ClientFactory, kubernetesClientFactory *cli.ClientFactory, kubernetesTokenCacheManager *kubernetes.TokenCacheManager, gitService portainer.GitService, snapshotService portainer.SnapshotService) {
|
||||
manager.proxyFactory = factory.NewProxyFactory(dataStore, signatureService, tunnelService, clientFactory, kubernetesClientFactory, kubernetesTokenCacheManager, gitService, snapshotService)
|
||||
func (manager *Manager) NewProxyFactory(dataStore dataservices.DataStore, signatureService portainer.DigitalSignatureService, tunnelService portainer.ReverseTunnelService, clientFactory *dockerclient.ClientFactory, kubernetesClientFactory *cli.ClientFactory, kubernetesTokenCacheManager *kubernetes.TokenCacheManager, gitService portainer.GitService, snapshotService portainer.SnapshotService, jwtService portainer.JWTService) {
|
||||
manager.proxyFactory = factory.NewProxyFactory(dataStore, signatureService, tunnelService, clientFactory, kubernetesClientFactory, kubernetesTokenCacheManager, gitService, snapshotService, jwtService)
|
||||
}
|
||||
|
||||
// CreateAndRegisterEndpointProxy creates a new HTTP reverse proxy based on environment(endpoint) properties and adds it to the registered proxies.
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue