mirror of
https://github.com/gravitational/teleport.git
synced 2026-11-02 14:59:53 +08:00
This PR introduces a gRPC service to retrieve the pods available using the `search_as_roles` configured in the user's roles.
This gRPC service receives a TLS identity and manipulates it to access the Teleport Kubernetes Proxy and retrieve the pods available for a hypothetical user that has the `search_as_roles` defined for its roles.
The response does not leak the certificate nor the pod details other than `name`, `namespace` and `labels`.
```mermaid
sequenceDiagram
participant User as User
participant gRPC as grpc Server
participant Auth as Auth Server
participant KubeProxy as Kube Proxy
participant KubeSVC as Kube Service
User->>+gRPC: ListKubeResources(cluster,namespace)
Note left of gRPC: Includes search_as_roles <br/> and preview_as_roles <br/>into identity!
gRPC-->>+Auth: Sign cert with ProcessKubeCSR
Auth-->>-gRPC: Signed certificate
gRPC->>+KubeProxy: list pods "v1.../namespaces/{namespace}/pods"
KubeProxy-->>+KubeSVC: list pods "v1.../namespaces/{namespace}/pods"
KubeSVC-->>-KubeProxy: filtered response
Note left of KubeSVC: Applies Pod RBAC filtering!
KubeProxy-->>-gRPC: response
gRPC->>-User:Return name, namespace and labels
```
Part of #21107 and #19573
234 lines
7.2 KiB
Go
234 lines
7.2 KiB
Go
/*
|
|
Copyright 2022 Gravitational, Inc.
|
|
|
|
Licensed under the Apache License, Version 2.0 (the "License");
|
|
you may not use this file except in compliance with the License.
|
|
You may obtain a copy of the License at
|
|
|
|
http://www.apache.org/licenses/LICENSE-2.0
|
|
|
|
Unless required by applicable law or agreed to in writing, software
|
|
distributed under the License is distributed on an "AS IS" BASIS,
|
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
See the License for the specific language governing permissions and
|
|
limitations under the License.
|
|
*/
|
|
|
|
package proxy
|
|
|
|
import (
|
|
"context"
|
|
"sort"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/google/go-cmp/cmp"
|
|
"github.com/google/go-cmp/cmp/cmpopts"
|
|
"github.com/stretchr/testify/require"
|
|
"k8s.io/client-go/tools/clientcmd"
|
|
clientcmdapi "k8s.io/client-go/tools/clientcmd/api"
|
|
|
|
"github.com/gravitational/teleport/api/types"
|
|
testingkubemock "github.com/gravitational/teleport/lib/kube/proxy/testing/kube_server"
|
|
"github.com/gravitational/teleport/lib/services"
|
|
)
|
|
|
|
// TestWatcher verifies that kubernetes agent properly detects and applies
|
|
// changes to kube_cluster resources.
|
|
func TestWatcher(t *testing.T) {
|
|
kubeMock, err := testingkubemock.NewKubeAPIMock()
|
|
require.NoError(t, err)
|
|
t.Cleanup(func() { kubeMock.Close() })
|
|
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
t.Cleanup(cancel)
|
|
reconcileCh := make(chan types.KubeClusters)
|
|
// Setup kubernetes server that proxies one static kube cluster and
|
|
// watches for kube_clusters with label group=a.
|
|
testCtx := SetupTestContext(ctx, t, TestConfig{
|
|
Clusters: []KubeClusterConfig{{"kube0", kubeMock.URL}},
|
|
ResourceMatchers: []services.ResourceMatcher{
|
|
{Labels: types.Labels{
|
|
"group": []string{"a"},
|
|
}},
|
|
},
|
|
OnReconcile: func(kcs types.KubeClusters) {
|
|
select {
|
|
case reconcileCh <- kcs:
|
|
case <-ctx.Done():
|
|
return
|
|
}
|
|
},
|
|
})
|
|
|
|
require.Len(t, testCtx.KubeServer.fwd.kubeClusters(), 1)
|
|
kube0 := testCtx.KubeServer.fwd.kubeClusters()[0]
|
|
|
|
// Only kube0 should be registered initially.
|
|
select {
|
|
case a := <-reconcileCh:
|
|
sort.Sort(a)
|
|
require.Empty(t, cmp.Diff(types.KubeClusters{kube0}, a,
|
|
cmpopts.IgnoreFields(types.Metadata{}, "ID"),
|
|
))
|
|
case <-time.After(time.Second):
|
|
t.Fatal("Didn't receive reconcile event after 1s.")
|
|
}
|
|
|
|
// Create kube_cluster with label group=a.
|
|
kube1, err := makeDynamicKubeCluster(t, "kube1", kubeMock.URL, map[string]string{"group": "a"})
|
|
require.NoError(t, err)
|
|
err = testCtx.AuthServer.CreateKubernetesCluster(ctx, kube1)
|
|
require.NoError(t, err)
|
|
|
|
// It should be registered.
|
|
select {
|
|
case a := <-reconcileCh:
|
|
sort.Sort(a)
|
|
require.Empty(t, cmp.Diff(types.KubeClusters{kube0, kube1}, a,
|
|
cmpopts.IgnoreFields(types.Metadata{}, "ID"),
|
|
))
|
|
case <-time.After(time.Second):
|
|
t.Fatal("Didn't receive reconcile event after 1s.")
|
|
}
|
|
|
|
// Try to update kube0 which is registered statically.
|
|
kube0Updated, err := makeDynamicKubeCluster(t, "kube0", kubeMock.URL, map[string]string{"group": "a", types.OriginLabel: types.OriginDynamic})
|
|
require.NoError(t, err)
|
|
err = testCtx.AuthServer.CreateKubernetesCluster(ctx, kube0Updated)
|
|
require.NoError(t, err)
|
|
|
|
// It should not be registered, old kube0 should remain.
|
|
select {
|
|
case a := <-reconcileCh:
|
|
sort.Sort(a)
|
|
require.Empty(t, cmp.Diff(types.KubeClusters{kube0, kube1}, a,
|
|
cmpopts.IgnoreFields(types.Metadata{}, "ID"),
|
|
))
|
|
case <-time.After(time.Second):
|
|
t.Fatal("Didn't receive reconcile event after 1s.")
|
|
}
|
|
|
|
// Create kube_cluster with label group=b.
|
|
kube2, err := makeDynamicKubeCluster(t, "kube2", kubeMock.URL, map[string]string{"group": "b"})
|
|
require.NoError(t, err)
|
|
err = testCtx.AuthServer.CreateKubernetesCluster(ctx, kube2)
|
|
require.NoError(t, err)
|
|
|
|
// It shouldn't be registered.
|
|
select {
|
|
case a := <-reconcileCh:
|
|
sort.Sort(a)
|
|
require.Empty(t, cmp.Diff(types.KubeClusters{kube0, kube1}, a,
|
|
cmpopts.IgnoreFields(types.Metadata{}, "ID"),
|
|
))
|
|
case <-time.After(time.Second):
|
|
t.Fatal("Didn't receive reconcile event after 1s.")
|
|
}
|
|
|
|
// Update kube2 labels so it matches.
|
|
kube2.SetStaticLabels(map[string]string{"group": "a", types.OriginLabel: types.OriginDynamic})
|
|
err = testCtx.AuthServer.UpdateKubernetesCluster(ctx, kube2)
|
|
require.NoError(t, err)
|
|
|
|
// Both should be registered now.
|
|
select {
|
|
case a := <-reconcileCh:
|
|
sort.Sort(a)
|
|
require.Empty(t, cmp.Diff(types.KubeClusters{kube0, kube1, kube2}, a,
|
|
cmpopts.IgnoreFields(types.Metadata{}, "ID"),
|
|
))
|
|
case <-time.After(time.Second):
|
|
t.Fatal("Didn't receive reconcile event after 1s.")
|
|
}
|
|
|
|
// Update kube2 expiry so it gets re-registered.
|
|
kube2.SetExpiry(time.Now().Add(1 * time.Hour))
|
|
kube2.SetKubeconfig(newKubeConfig(t, "random", "https://api.cluster.com"))
|
|
err = testCtx.AuthServer.UpdateKubernetesCluster(ctx, kube2)
|
|
require.NoError(t, err)
|
|
|
|
// kube2 should get updated.
|
|
select {
|
|
case a := <-reconcileCh:
|
|
sort.Sort(a)
|
|
require.Empty(t, cmp.Diff(types.KubeClusters{kube0, kube1, kube2}, a,
|
|
cmpopts.IgnoreFields(types.Metadata{}, "ID"),
|
|
))
|
|
// make sure credentials were updated as well.
|
|
require.Equal(t, "api.cluster.com:443", testCtx.KubeServer.fwd.clusterDetails["kube2"].kubeCreds.getTargetAddr())
|
|
case <-time.After(time.Second):
|
|
t.Fatal("Didn't receive reconcile event after 1s.")
|
|
}
|
|
|
|
// Update kube1 labels so it doesn't match.
|
|
kube1.SetStaticLabels(map[string]string{"group": "c", types.OriginLabel: types.OriginDynamic})
|
|
err = testCtx.AuthServer.UpdateKubernetesCluster(ctx, kube1)
|
|
require.NoError(t, err)
|
|
|
|
// Only kube0 and kube2 should remain registered.
|
|
select {
|
|
case a := <-reconcileCh:
|
|
sort.Sort(a)
|
|
require.Empty(t, cmp.Diff(types.KubeClusters{kube0, kube2}, a,
|
|
cmpopts.IgnoreFields(types.Metadata{}, "ID"),
|
|
))
|
|
case <-time.After(time.Second):
|
|
t.Fatal("Didn't receive reconcile event after 1s.")
|
|
}
|
|
|
|
// Remove kube2.
|
|
err = testCtx.AuthServer.DeleteKubernetesCluster(ctx, kube2.GetName())
|
|
require.NoError(t, err)
|
|
|
|
// Only static kube_cluster should remain.
|
|
select {
|
|
case a := <-reconcileCh:
|
|
require.Empty(t, cmp.Diff(types.KubeClusters{kube0}, a,
|
|
cmpopts.IgnoreFields(types.Metadata{}, "ID"),
|
|
))
|
|
case <-time.After(time.Second):
|
|
t.Fatal("Didn't receive reconcile event after 1s.")
|
|
}
|
|
}
|
|
|
|
func makeDynamicKubeCluster(t *testing.T, name, url string, labels map[string]string) (*types.KubernetesClusterV3, error) {
|
|
return makeKubeCluster(t, name, url, labels, map[string]string{
|
|
types.OriginLabel: types.OriginDynamic,
|
|
})
|
|
}
|
|
|
|
func makeKubeCluster(t *testing.T, name string, url string, labels map[string]string, additionalLabels map[string]string) (*types.KubernetesClusterV3, error) {
|
|
if labels == nil {
|
|
labels = make(map[string]string)
|
|
}
|
|
for k, v := range additionalLabels {
|
|
labels[k] = v
|
|
}
|
|
return types.NewKubernetesClusterV3(types.Metadata{
|
|
Name: name,
|
|
Labels: labels,
|
|
}, types.KubernetesClusterSpecV3{
|
|
Kubeconfig: newKubeConfig(t, name, url),
|
|
})
|
|
}
|
|
|
|
func newKubeConfig(t *testing.T, name, url string) []byte {
|
|
kubeConf := clientcmdapi.NewConfig()
|
|
|
|
kubeConf.Clusters[name] = &clientcmdapi.Cluster{
|
|
Server: url,
|
|
InsecureSkipTLSVerify: true,
|
|
}
|
|
kubeConf.AuthInfos[name] = &clientcmdapi.AuthInfo{}
|
|
|
|
kubeConf.Contexts[name] = &clientcmdapi.Context{
|
|
Cluster: name,
|
|
AuthInfo: name,
|
|
}
|
|
|
|
buf, err := clientcmd.Write(*kubeConf)
|
|
require.NoError(t, err)
|
|
return buf
|
|
}
|