diff --git a/Makefile b/Makefile index 3525c67a164..b1a3fbb1ce8 100644 --- a/Makefile +++ b/Makefile @@ -1593,7 +1593,7 @@ protos-up-to-date/host: must-start-clean/host grpc/host .PHONY: must-start-clean/host must-start-clean/host: @if ! git diff --quiet; then \ - @echo 'This must be run from a repo with no unstaged commits.'; \ + echo 'This must be run from a repo with no unstaged commits.'; \ git diff; \ exit 1; \ fi diff --git a/api/types/role.go b/api/types/role.go index 384f61a69fb..63ff20f6a1d 100644 --- a/api/types/role.go +++ b/api/types/role.go @@ -1196,6 +1196,7 @@ func (r *RoleV6) CheckAndSetDefaults() error { Namespace: Wildcard, Name: Wildcard, Verbs: []string{Wildcard}, + APIGroup: Wildcard, }, } } diff --git a/e2e/aws/eks_test.go b/e2e/aws/eks_test.go index ebcb44181aa..939bd79cf14 100644 --- a/e2e/aws/eks_test.go +++ b/e2e/aws/eks_test.go @@ -197,6 +197,7 @@ func withFullKubeAccessUserRole(t *testing.T) testOptionsFunc { Name: types.Wildcard, Namespace: types.Wildcard, Verbs: []string{types.Wildcard}, + APIGroup: types.Wildcard, }, }, }, diff --git a/gen/preset-roles.json b/gen/preset-roles.json index d0c576c9ea0..3290103e324 100755 --- a/gen/preset-roles.json +++ b/gen/preset-roles.json @@ -1,7 +1,7 @@ { "access": { "kind": "role", - "version": "v8", + "version": "v7", "metadata": { "name": "access", "description": "Access cluster resources", @@ -25,6 +25,11 @@ "desktop_directory_sharing": true, "pin_source_ip": false, "ssh_file_copy": true, + "idp": { + "saml": { + "enabled": true + } + }, "create_desktop_user": false, "create_db_user": false, "ssh_port_forwarding": { @@ -147,7 +152,7 @@ }, "auditor": { "kind": "role", - "version": "v8", + "version": "v7", "metadata": { "name": "auditor", "description": "Review cluster events and replay sessions", @@ -171,6 +176,11 @@ "desktop_directory_sharing": true, "pin_source_ip": false, "ssh_file_copy": true, + "idp": { + "saml": { + "enabled": true + } + }, "create_desktop_user": false, "create_db_user": false }, @@ -266,7 +276,7 @@ }, "editor": { "kind": "role", - "version": "v8", + "version": "v7", "metadata": { "name": "editor", "description": "Edit cluster configuration", @@ -290,6 +300,11 @@ "desktop_directory_sharing": true, "pin_source_ip": false, "ssh_file_copy": true, + "idp": { + "saml": { + "enabled": true + } + }, "create_desktop_user": false, "create_db_user": false, "ssh_port_forwarding": { diff --git a/integration/kube_integration_test.go b/integration/kube_integration_test.go index 6ccdebf8f65..70456c93abc 100644 --- a/integration/kube_integration_test.go +++ b/integration/kube_integration_test.go @@ -222,7 +222,7 @@ func testExec(t *testing.T, suite *KubeSuite, pinnedIP string, clientError strin }, KubernetesResources: []types.KubernetesResource{ { - Kind: types.KindKubePod, Name: types.Wildcard, Namespace: types.Wildcard, Verbs: []string{types.Wildcard}, + Kind: types.KindKubePod, Name: types.Wildcard, Namespace: types.Wildcard, Verbs: []string{types.Wildcard}, APIGroup: types.Wildcard, }, }, }, @@ -433,7 +433,7 @@ func testKubeDeny(t *testing.T, suite *KubeSuite) { KubeUsers: kubeUsers, KubernetesResources: []types.KubernetesResource{ { - Kind: types.KindKubePod, Name: types.Wildcard, Namespace: types.Wildcard, Verbs: []string{types.Wildcard}, + Kind: types.KindKubePod, Name: types.Wildcard, Namespace: types.Wildcard, Verbs: []string{types.Wildcard}, APIGroup: types.Wildcard, }, }, }, @@ -491,7 +491,7 @@ func testKubePortForward(t *testing.T, suite *KubeSuite) { }, KubernetesResources: []types.KubernetesResource{ { - Kind: types.KindKubePod, Name: types.Wildcard, Namespace: types.Wildcard, Verbs: []string{types.Wildcard}, + Kind: types.KindKubePod, Name: types.Wildcard, Namespace: types.Wildcard, Verbs: []string{types.Wildcard}, APIGroup: types.Wildcard, }, }, }, @@ -621,7 +621,7 @@ func testKubeTrustedClustersClientCert(t *testing.T, suite *KubeSuite) { }, KubernetesResources: []types.KubernetesResource{ { - Kind: types.KindKubePod, Name: types.Wildcard, Namespace: types.Wildcard, Verbs: []string{types.Wildcard}, + Kind: types.KindKubePod, Name: types.Wildcard, Namespace: types.Wildcard, Verbs: []string{types.Wildcard}, APIGroup: types.Wildcard, }, }, }, @@ -667,7 +667,7 @@ func testKubeTrustedClustersClientCert(t *testing.T, suite *KubeSuite) { }, KubernetesResources: []types.KubernetesResource{ { - Kind: types.KindKubePod, Name: types.Wildcard, Namespace: types.Wildcard, Verbs: []string{types.Wildcard}, + Kind: types.KindKubePod, Name: types.Wildcard, Namespace: types.Wildcard, Verbs: []string{types.Wildcard}, APIGroup: types.Wildcard, }, }, }, @@ -891,7 +891,7 @@ func testKubeTrustedClustersSNI(t *testing.T, suite *KubeSuite) { }, KubernetesResources: []types.KubernetesResource{ { - Kind: types.KindKubePod, Name: types.Wildcard, Namespace: types.Wildcard, Verbs: []string{types.Wildcard}, + Kind: types.KindKubePod, Name: types.Wildcard, Namespace: types.Wildcard, Verbs: []string{types.Wildcard}, APIGroup: types.Wildcard, }, }, }, @@ -941,7 +941,7 @@ func testKubeTrustedClustersSNI(t *testing.T, suite *KubeSuite) { KubeGroups: auxKubeGroups, KubernetesResources: []types.KubernetesResource{ { - Kind: types.KindKubePod, Name: types.Wildcard, Namespace: types.Wildcard, Verbs: []string{types.Wildcard}, + Kind: types.KindKubePod, Name: types.Wildcard, Namespace: types.Wildcard, Verbs: []string{types.Wildcard}, APIGroup: types.Wildcard, }, }, }, @@ -1191,7 +1191,7 @@ func runKubeDisconnectTest(t *testing.T, suite *KubeSuite, tc disconnectTestCase }, KubernetesResources: []types.KubernetesResource{ { - Kind: types.KindKubePod, Name: types.Wildcard, Namespace: types.Wildcard, Verbs: []string{types.Wildcard}, + Kind: types.KindKubePod, Name: types.Wildcard, Namespace: types.Wildcard, Verbs: []string{types.Wildcard}, APIGroup: types.Wildcard, }, }, }, @@ -1290,7 +1290,7 @@ func testKubeTransportProtocol(t *testing.T, suite *KubeSuite) { }, KubernetesResources: []types.KubernetesResource{ { - Kind: types.KindKubePod, Name: types.Wildcard, Namespace: types.Wildcard, Verbs: []string{types.Wildcard}, + Kind: types.KindKubePod, Name: types.Wildcard, Namespace: types.Wildcard, Verbs: []string{types.Wildcard}, APIGroup: types.Wildcard, }, }, }, @@ -1403,6 +1403,7 @@ func testKubeEphemeralContainers(t *testing.T, suite *KubeSuite) { Name: types.Wildcard, Namespace: types.Wildcard, Verbs: []string{types.Wildcard}, + APIGroup: types.Wildcard, }, }, }, @@ -1682,7 +1683,7 @@ func testKubeExecWeb(t *testing.T, suite *KubeSuite) { }, KubernetesResources: []types.KubernetesResource{ { - Kind: types.Wildcard, Name: types.Wildcard, Namespace: types.Wildcard, Verbs: []string{types.Wildcard}, + Kind: types.Wildcard, Name: types.Wildcard, Namespace: types.Wildcard, Verbs: []string{types.Wildcard}, APIGroup: types.Wildcard, }, }, }, @@ -2179,7 +2180,7 @@ func testKubeJoin(t *testing.T, suite *KubeSuite) { }, KubernetesResources: []types.KubernetesResource{ { - Kind: types.KindKubePod, Name: types.Wildcard, Namespace: types.Wildcard, Verbs: []string{types.Wildcard}, + Kind: types.KindKubePod, Name: types.Wildcard, Namespace: types.Wildcard, Verbs: []string{types.Wildcard}, APIGroup: types.Wildcard, }, }, }, @@ -2401,7 +2402,7 @@ func testKubeJoinWeb(t *testing.T, suite *KubeSuite) { }, KubernetesResources: []types.KubernetesResource{ { - Kind: types.KindKubePod, Name: types.Wildcard, Namespace: types.Wildcard, Verbs: []string{types.Wildcard}, + Kind: types.KindKubePod, Name: types.Wildcard, Namespace: types.Wildcard, Verbs: []string{types.Wildcard}, APIGroup: types.Wildcard, }, }, }, @@ -2696,7 +2697,7 @@ func testExecNoAuth(t *testing.T, suite *KubeSuite) { }, KubernetesResources: []types.KubernetesResource{ { - Kind: types.KindKubePod, Name: types.Wildcard, Namespace: types.Wildcard, Verbs: []string{types.Wildcard}, + Kind: types.KindKubePod, Name: types.Wildcard, Namespace: types.Wildcard, Verbs: []string{types.Wildcard}, APIGroup: types.Wildcard, }, }, }, @@ -2715,7 +2716,7 @@ func testExecNoAuth(t *testing.T, suite *KubeSuite) { }, KubernetesResources: []types.KubernetesResource{ { - Kind: types.KindKubePod, Name: types.Wildcard, Namespace: types.Wildcard, Verbs: []string{types.Wildcard}, + Kind: types.KindKubePod, Name: types.Wildcard, Namespace: types.Wildcard, Verbs: []string{types.Wildcard}, APIGroup: types.Wildcard, }, }, RequireSessionJoin: []*types.SessionRequirePolicy{ diff --git a/integration/proxy/proxy_test.go b/integration/proxy/proxy_test.go index b230a04c0e5..77ce17d4cab 100644 --- a/integration/proxy/proxy_test.go +++ b/integration/proxy/proxy_test.go @@ -349,7 +349,7 @@ func TestALPNSNIProxyKube(t *testing.T) { KubeUsers: []string{k8User}, KubernetesResources: []types.KubernetesResource{ { - Kind: types.KindKubePod, Name: types.Wildcard, Namespace: types.Wildcard, Verbs: []string{types.Wildcard}, + Kind: types.KindKubePod, Name: types.Wildcard, Namespace: types.Wildcard, Verbs: []string{types.Wildcard}, APIGroup: types.Wildcard, }, }, }, @@ -427,7 +427,7 @@ func TestALPNSNIProxyKubeV2Leaf(t *testing.T) { KubeUsers: []string{k8User}, KubernetesResources: []types.KubernetesResource{ { - Kind: types.KindKubePod, Name: types.Wildcard, Namespace: types.Wildcard, Verbs: []string{types.Wildcard}, + Kind: types.KindKubePod, Name: types.Wildcard, Namespace: types.Wildcard, Verbs: []string{types.Wildcard}, APIGroup: types.Wildcard, }, }, }, @@ -544,7 +544,7 @@ func TestKubePROXYProtocol(t *testing.T) { KubeUsers: []string{k8User}, KubernetesResources: []types.KubernetesResource{ { - Kind: types.KindKubePod, Name: types.Wildcard, Namespace: types.Wildcard, Verbs: []string{types.Wildcard}, + Kind: types.KindKubePod, Name: types.Wildcard, Namespace: types.Wildcard, Verbs: []string{types.Wildcard}, APIGroup: types.Wildcard, }, }, }, @@ -726,7 +726,7 @@ func TestKubeIPPinning(t *testing.T) { KubeUsers: []string{k8User}, KubernetesResources: []types.KubernetesResource{ { - Kind: types.KindKubePod, Name: types.Wildcard, Namespace: types.Wildcard, Verbs: []string{types.Wildcard}, + Kind: types.KindKubePod, Name: types.Wildcard, Namespace: types.Wildcard, Verbs: []string{types.Wildcard}, APIGroup: types.Wildcard, }, }, }, @@ -1791,7 +1791,7 @@ func TestALPNSNIProxyGRPCSecure(t *testing.T) { KubeUsers: []string{k8User}, KubernetesResources: []types.KubernetesResource{ { - Kind: types.KindKubePod, Name: types.Wildcard, Namespace: types.Wildcard, Verbs: []string{types.Wildcard}, + Kind: types.KindKubePod, Name: types.Wildcard, Namespace: types.Wildcard, Verbs: []string{types.Wildcard}, APIGroup: types.Wildcard, }, }, }, diff --git a/integration/proxy/teleterm_test.go b/integration/proxy/teleterm_test.go index 0e80106c865..6bfbcb01881 100644 --- a/integration/proxy/teleterm_test.go +++ b/integration/proxy/teleterm_test.go @@ -415,7 +415,7 @@ func TestTeletermKubeGateway(t *testing.T) { KubeUsers: []string{k8User}, KubernetesResources: []types.KubernetesResource{ { - Kind: types.KindKubePod, Name: types.Wildcard, Namespace: types.Wildcard, Verbs: []string{types.Wildcard}, + Kind: types.KindKubePod, Name: types.Wildcard, Namespace: types.Wildcard, Verbs: []string{types.Wildcard}, APIGroup: types.Wildcard, }, }, }, diff --git a/lib/kube/grpc/grpc_test.go b/lib/kube/grpc/grpc_test.go index 877edbb05c3..af1ff431165 100644 --- a/lib/kube/grpc/grpc_test.go +++ b/lib/kube/grpc/grpc_test.go @@ -90,7 +90,7 @@ func TestListKubernetesResources(t *testing.T) { // override the role to allow access to all kube resources. r.SetKubeResources( types.Allow, - []types.KubernetesResource{{Kind: types.Wildcard, Name: types.Wildcard, Namespace: types.Wildcard, Verbs: []string{types.Wildcard}}}, + []types.KubernetesResource{{Kind: types.Wildcard, Name: types.Wildcard, Namespace: types.Wildcard, Verbs: []string{types.Wildcard}, APIGroup: types.Wildcard}}, ) }, }, diff --git a/lib/kube/proxy/auth_test.go b/lib/kube/proxy/auth_test.go index 272f4c36db6..08ac8d9e838 100644 --- a/lib/kube/proxy/auth_test.go +++ b/lib/kube/proxy/auth_test.go @@ -338,7 +338,6 @@ current-context: foo }, } for _, tt := range tests { - tt := tt t.Run(tt.desc, func(t *testing.T) { t.Parallel() fwd := &Forwarder{ diff --git a/lib/kube/proxy/resource_deletecollection.go b/lib/kube/proxy/resource_deletecollection.go index 1792c189d13..11f56d66aa9 100644 --- a/lib/kube/proxy/resource_deletecollection.go +++ b/lib/kube/proxy/resource_deletecollection.go @@ -185,6 +185,7 @@ func (f *Forwarder) handleDeleteCollectionReq(req *http.Request, sess *clusterSe items, err := deleteResources( params, types.KindKubePod, + sess.apiResource.apiGroup, slices.ToPointers(o.Items), func(ctx context.Context, client kubernetes.Interface, name, namespace string) error { return trace.Wrap(client.CoreV1().Pods(namespace).Delete(ctx, name, deleteOptions)) @@ -198,6 +199,7 @@ func (f *Forwarder) handleDeleteCollectionReq(req *http.Request, sess *clusterSe items, err := deleteResources( params, types.KindKubeSecret, + sess.apiResource.apiGroup, slices.ToPointers(o.Items), func(ctx context.Context, client kubernetes.Interface, name, namespace string) error { return trace.Wrap(client.CoreV1().Secrets(namespace).Delete(ctx, name, deleteOptions)) @@ -211,6 +213,7 @@ func (f *Forwarder) handleDeleteCollectionReq(req *http.Request, sess *clusterSe items, err := deleteResources( params, types.KindKubeConfigmap, + sess.apiResource.apiGroup, slices.ToPointers(o.Items), func(ctx context.Context, client kubernetes.Interface, name, namespace string) error { return trace.Wrap(client.CoreV1().ConfigMaps(namespace).Delete(ctx, name, deleteOptions)) @@ -224,6 +227,7 @@ func (f *Forwarder) handleDeleteCollectionReq(req *http.Request, sess *clusterSe items, err := deleteResources( params, types.KindKubeNamespace, + sess.apiResource.apiGroup, slices.ToPointers(o.Items), func(ctx context.Context, client kubernetes.Interface, name, _ string) error { return trace.Wrap(client.CoreV1().Namespaces().Delete(ctx, name, deleteOptions)) @@ -237,6 +241,7 @@ func (f *Forwarder) handleDeleteCollectionReq(req *http.Request, sess *clusterSe items, err := deleteResources( params, types.KindKubeServiceAccount, + sess.apiResource.apiGroup, slices.ToPointers(o.Items), func(ctx context.Context, client kubernetes.Interface, name, namespace string) error { return trace.Wrap(client.CoreV1().ServiceAccounts(namespace).Delete(ctx, name, deleteOptions)) @@ -250,6 +255,7 @@ func (f *Forwarder) handleDeleteCollectionReq(req *http.Request, sess *clusterSe items, err := deleteResources( params, types.KindKubePersistentVolume, + sess.apiResource.apiGroup, slices.ToPointers(o.Items), func(ctx context.Context, client kubernetes.Interface, name, _ string) error { return trace.Wrap(client.CoreV1().PersistentVolumes().Delete(ctx, name, deleteOptions)) @@ -264,6 +270,7 @@ func (f *Forwarder) handleDeleteCollectionReq(req *http.Request, sess *clusterSe items, err := deleteResources( params, types.KindKubePersistentVolumeClaim, + sess.apiResource.apiGroup, slices.ToPointers(o.Items), func(ctx context.Context, client kubernetes.Interface, name, namespace string) error { return trace.Wrap(client.CoreV1().PersistentVolumeClaims(namespace).Delete(ctx, name, deleteOptions)) @@ -277,6 +284,7 @@ func (f *Forwarder) handleDeleteCollectionReq(req *http.Request, sess *clusterSe items, err := deleteResources( params, types.KindKubeDeployment, + sess.apiResource.apiGroup, slices.ToPointers(o.Items), func(ctx context.Context, client kubernetes.Interface, name, namespace string) error { return trace.Wrap(client.AppsV1().Deployments(namespace).Delete(ctx, name, deleteOptions)) @@ -290,6 +298,7 @@ func (f *Forwarder) handleDeleteCollectionReq(req *http.Request, sess *clusterSe items, err := deleteResources( params, types.KindKubeReplicaSet, + sess.apiResource.apiGroup, slices.ToPointers(o.Items), func(ctx context.Context, client kubernetes.Interface, name, namespace string) error { return trace.Wrap(client.AppsV1().ReplicaSets(namespace).Delete(ctx, name, deleteOptions)) @@ -304,6 +313,7 @@ func (f *Forwarder) handleDeleteCollectionReq(req *http.Request, sess *clusterSe items, err := deleteResources( params, types.KindKubeStatefulset, + sess.apiResource.apiGroup, slices.ToPointers(o.Items), func(ctx context.Context, client kubernetes.Interface, name, namespace string) error { return trace.Wrap(client.AppsV1().StatefulSets(namespace).Delete(ctx, name, deleteOptions)) @@ -317,6 +327,7 @@ func (f *Forwarder) handleDeleteCollectionReq(req *http.Request, sess *clusterSe items, err := deleteResources( params, types.KindKubeDaemonSet, + sess.apiResource.apiGroup, slices.ToPointers(o.Items), func(ctx context.Context, client kubernetes.Interface, name, namespace string) error { return trace.Wrap(client.AppsV1().DaemonSets(namespace).Delete(ctx, name, deleteOptions)) @@ -331,6 +342,7 @@ func (f *Forwarder) handleDeleteCollectionReq(req *http.Request, sess *clusterSe items, err := deleteResources( params, types.KindKubeClusterRole, + sess.apiResource.apiGroup, slices.ToPointers(o.Items), func(ctx context.Context, client kubernetes.Interface, name, _ string) error { return trace.Wrap(client.RbacV1().ClusterRoles().Delete(ctx, name, deleteOptions)) @@ -344,6 +356,7 @@ func (f *Forwarder) handleDeleteCollectionReq(req *http.Request, sess *clusterSe items, err := deleteResources( params, types.KindKubeRole, + sess.apiResource.apiGroup, slices.ToPointers(o.Items), func(ctx context.Context, client kubernetes.Interface, name, namespace string) error { return trace.Wrap(client.RbacV1().Roles(namespace).Delete(ctx, name, deleteOptions)) @@ -357,6 +370,7 @@ func (f *Forwarder) handleDeleteCollectionReq(req *http.Request, sess *clusterSe items, err := deleteResources( params, types.KindKubeClusterRoleBinding, + sess.apiResource.apiGroup, slices.ToPointers(o.Items), func(ctx context.Context, client kubernetes.Interface, name, _ string) error { return trace.Wrap(client.RbacV1().ClusterRoleBindings().Delete(ctx, name, deleteOptions)) @@ -370,6 +384,7 @@ func (f *Forwarder) handleDeleteCollectionReq(req *http.Request, sess *clusterSe items, err := deleteResources( params, types.KindKubeRoleBinding, + sess.apiResource.apiGroup, slices.ToPointers(o.Items), func(ctx context.Context, client kubernetes.Interface, name, namespace string) error { return trace.Wrap(client.RbacV1().RoleBindings(namespace).Delete(ctx, name, deleteOptions)) @@ -383,6 +398,7 @@ func (f *Forwarder) handleDeleteCollectionReq(req *http.Request, sess *clusterSe items, err := deleteResources( params, types.KindKubeCronjob, + sess.apiResource.apiGroup, slices.ToPointers(o.Items), func(ctx context.Context, client kubernetes.Interface, name, namespace string) error { return trace.Wrap(client.BatchV1().CronJobs(namespace).Delete(ctx, name, deleteOptions)) @@ -396,6 +412,7 @@ func (f *Forwarder) handleDeleteCollectionReq(req *http.Request, sess *clusterSe items, err := deleteResources( params, types.KindKubeJob, + sess.apiResource.apiGroup, slices.ToPointers(o.Items), func(ctx context.Context, client kubernetes.Interface, name, namespace string) error { return trace.Wrap(client.BatchV1().Jobs(namespace).Delete(ctx, name, deleteOptions)) @@ -409,6 +426,7 @@ func (f *Forwarder) handleDeleteCollectionReq(req *http.Request, sess *clusterSe items, err := deleteResources( params, types.KindKubeCertificateSigningRequest, + sess.apiResource.apiGroup, slices.ToPointers(o.Items), func(ctx context.Context, client kubernetes.Interface, name, _ string) error { return trace.Wrap(client.CertificatesV1().CertificateSigningRequests().Delete(ctx, name, deleteOptions)) @@ -422,6 +440,7 @@ func (f *Forwarder) handleDeleteCollectionReq(req *http.Request, sess *clusterSe items, err := deleteResources( params, types.KindKubeIngress, + sess.apiResource.apiGroup, slices.ToPointers(o.Items), func(ctx context.Context, client kubernetes.Interface, name, namespace string) error { return trace.Wrap(client.NetworkingV1().Ingresses(namespace).Delete(ctx, name, deleteOptions)) @@ -435,6 +454,7 @@ func (f *Forwarder) handleDeleteCollectionReq(req *http.Request, sess *clusterSe items, err := deleteResources( params, types.KindKubeIngress, + sess.apiResource.apiGroup, slices.ToPointers(o.Items), func(ctx context.Context, client kubernetes.Interface, name, namespace string) error { return trace.Wrap(client.ExtensionsV1beta1().Ingresses(namespace).Delete(ctx, name, deleteOptions)) @@ -448,6 +468,7 @@ func (f *Forwarder) handleDeleteCollectionReq(req *http.Request, sess *clusterSe items, err := deleteResources( params, types.KindKubeDaemonSet, + sess.apiResource.apiGroup, slices.ToPointers(o.Items), func(ctx context.Context, client kubernetes.Interface, name, namespace string) error { return trace.Wrap(client.ExtensionsV1beta1().DaemonSets(namespace).Delete(ctx, name, deleteOptions)) @@ -461,6 +482,7 @@ func (f *Forwarder) handleDeleteCollectionReq(req *http.Request, sess *clusterSe items, err := deleteResources( params, types.KindKubeDeployment, + sess.apiResource.apiGroup, slices.ToPointers(o.Items), func(ctx context.Context, client kubernetes.Interface, name, namespace string) error { return trace.Wrap(client.ExtensionsV1beta1().Deployments(namespace).Delete(ctx, name, deleteOptions)) @@ -474,6 +496,7 @@ func (f *Forwarder) handleDeleteCollectionReq(req *http.Request, sess *clusterSe items, err := deleteResources( params, types.KindKubeReplicaSet, + sess.apiResource.apiGroup, slices.ToPointers(o.Items), func(ctx context.Context, client kubernetes.Interface, name, namespace string) error { return trace.Wrap(client.ExtensionsV1beta1().ReplicaSets(namespace).Delete(ctx, name, deleteOptions)) @@ -591,7 +614,7 @@ type deleteResourcesCommonParams struct { func deleteResources[T kubeObjectInterface]( params deleteResourcesCommonParams, - kind string, + kind, group string, items []T, deleteOP func(ctx context.Context, client kubernetes.Interface, name, namespace string) error, ) ([]T, error) { @@ -607,7 +630,7 @@ func deleteResources[T kubeObjectInterface]( params.authCtx.Checker.Traits(), ), services.NewKubernetesResourceMatcher( - getKubeResource(kind, types.KubeVerbDeleteCollection, item), + getKubeResource(kind, group, types.KubeVerbDeleteCollection, item), ), ) // no match was found, we ignore the request. diff --git a/lib/kube/proxy/resource_filters.go b/lib/kube/proxy/resource_filters.go index 15236911b30..2e87927c5c8 100644 --- a/lib/kube/proxy/resource_filters.go +++ b/lib/kube/proxy/resource_filters.go @@ -52,7 +52,7 @@ import ( // - deniedResources: excluded if (namespace,name) matches an entry even if it matches // the allowedResources's list. // - allowedResources: excluded if (namespace,name) not match a single entry. -func newResourceFilterer(kind, verb string, codecs *serializer.CodecFactory, allowedResources, deniedResources []types.KubernetesResource, log *slog.Logger) responsewriters.FilterWrapper { +func newResourceFilterer(kind, group, verb string, codecs *serializer.CodecFactory, allowedResources, deniedResources []types.KubernetesResource, log *slog.Logger) responsewriters.FilterWrapper { // If the list of allowed resources contains a wildcard and no deniedResources, then we // don't need to filter anything. if containsWildcard(allowedResources) && len(deniedResources) == 0 { @@ -74,6 +74,7 @@ func newResourceFilterer(kind, verb string, codecs *serializer.CodecFactory, all deniedResources: deniedResources, log: log, kind: kind, + group: group, verb: verb, }, nil } @@ -118,6 +119,8 @@ type resourceFilterer struct { log *slog.Logger // kind is the type of the resource. kind string + // group is the api group of the resource. + group string // verb is the kube API verb based on HTTP verb. verb string } @@ -167,7 +170,7 @@ func (d *resourceFilterer) FilterObj(obj runtime.Object) (isAllowed bool, isList // should be forwarded to the user. return true, false, nil case *corev1.Pod: - result, err := filterResource(d.kind, d.verb, o, d.allowedResources, d.deniedResources) + result, err := filterResource(d.kind, d.group, d.verb, o, d.allowedResources, d.deniedResources) if err != nil { d.log.WarnContext(ctx, "Unable to compile regex expressions within kubernetes_resources", "error", err) } @@ -176,12 +179,12 @@ func (d *resourceFilterer) FilterObj(obj runtime.Object) (isAllowed bool, isList case *corev1.PodList: o.Items = slices.FromPointers( filterResourceList( - d.kind, d.verb, + d.kind, d.group, d.verb, slices.ToPointers(o.Items), d.allowedResources, d.deniedResources, d.log), ) return len(o.Items) > 0, true, nil case *corev1.Secret: - result, err := filterResource(d.kind, d.verb, o, d.allowedResources, d.deniedResources) + result, err := filterResource(d.kind, d.group, d.verb, o, d.allowedResources, d.deniedResources) if err != nil { d.log.WarnContext(ctx, "Unable to compile regex expressions within kubernetes_resources", "error", err) } @@ -190,12 +193,12 @@ func (d *resourceFilterer) FilterObj(obj runtime.Object) (isAllowed bool, isList case *corev1.SecretList: o.Items = slices.FromPointers( filterResourceList( - d.kind, d.verb, + d.kind, d.group, d.verb, slices.ToPointers(o.Items), d.allowedResources, d.deniedResources, d.log), ) return len(o.Items) > 0, true, nil case *corev1.ConfigMap: - result, err := filterResource(d.kind, d.verb, o, d.allowedResources, d.deniedResources) + result, err := filterResource(d.kind, d.group, d.verb, o, d.allowedResources, d.deniedResources) if err != nil { d.log.WarnContext(ctx, "Unable to compile regex expressions within kubernetes_resources", "error", err) } @@ -204,12 +207,12 @@ func (d *resourceFilterer) FilterObj(obj runtime.Object) (isAllowed bool, isList case *corev1.ConfigMapList: o.Items = slices.FromPointers( filterResourceList( - d.kind, d.verb, + d.kind, d.group, d.verb, slices.ToPointers(o.Items), d.allowedResources, d.deniedResources, d.log), ) return len(o.Items) > 0, true, nil case *corev1.Namespace: - result, err := filterResource(d.kind, d.verb, o, d.allowedResources, d.deniedResources) + result, err := filterResource(d.kind, d.group, d.verb, o, d.allowedResources, d.deniedResources) if err != nil { d.log.WarnContext(ctx, "Unable to compile regex expressions within kubernetes_resources", "error", err) } @@ -218,12 +221,12 @@ func (d *resourceFilterer) FilterObj(obj runtime.Object) (isAllowed bool, isList case *corev1.NamespaceList: o.Items = slices.FromPointers( filterResourceList( - d.kind, d.verb, + d.kind, d.group, d.verb, slices.ToPointers(o.Items), d.allowedResources, d.deniedResources, d.log), ) return len(o.Items) > 0, true, nil case *corev1.Service: - result, err := filterResource(d.kind, d.verb, o, d.allowedResources, d.deniedResources) + result, err := filterResource(d.kind, d.group, d.verb, o, d.allowedResources, d.deniedResources) if err != nil { d.log.WarnContext(ctx, "Unable to compile regex expressions within kubernetes_resources", "error", err) } @@ -232,12 +235,12 @@ func (d *resourceFilterer) FilterObj(obj runtime.Object) (isAllowed bool, isList case *corev1.ServiceList: o.Items = slices.FromPointers( filterResourceList( - d.kind, d.verb, + d.kind, d.group, d.verb, slices.ToPointers(o.Items), d.allowedResources, d.deniedResources, d.log), ) return len(o.Items) > 0, true, nil - case *corev1.Endpoints: //nolint:staticcheck // SA1019. Rreserve compatibility until RBAC is tweaked to support discovery endpoints. - result, err := filterResource(d.kind, d.verb, o, d.allowedResources, d.deniedResources) + case *corev1.Endpoints: //nolint:staticcheck // SA1019. Preserve compatibility until RBAC is tweaked to support discovery endpoints. + result, err := filterResource(d.kind, d.group, d.verb, o, d.allowedResources, d.deniedResources) if err != nil { d.log.WarnContext(ctx, "Unable to compile regex expressions within kubernetes_resources", "error", err) } @@ -246,12 +249,12 @@ func (d *resourceFilterer) FilterObj(obj runtime.Object) (isAllowed bool, isList case *corev1.EndpointsList: o.Items = slices.FromPointers( filterResourceList( - d.kind, d.verb, + d.kind, d.group, d.verb, slices.ToPointers(o.Items), d.allowedResources, d.deniedResources, d.log), ) return len(o.Items) > 0, true, nil case *corev1.ServiceAccount: - result, err := filterResource(d.kind, d.verb, o, d.allowedResources, d.deniedResources) + result, err := filterResource(d.kind, d.group, d.verb, o, d.allowedResources, d.deniedResources) if err != nil { d.log.WarnContext(ctx, "Unable to compile regex expressions within kubernetes_resources", "error", err) } @@ -260,12 +263,12 @@ func (d *resourceFilterer) FilterObj(obj runtime.Object) (isAllowed bool, isList case *corev1.ServiceAccountList: o.Items = slices.FromPointers( filterResourceList( - d.kind, d.verb, + d.kind, d.group, d.verb, slices.ToPointers(o.Items), d.allowedResources, d.deniedResources, d.log), ) return len(o.Items) > 0, true, nil case *corev1.Node: - result, err := filterResource(d.kind, d.verb, o, d.allowedResources, d.deniedResources) + result, err := filterResource(d.kind, d.group, d.verb, o, d.allowedResources, d.deniedResources) if err != nil { d.log.WarnContext(ctx, "Unable to compile regex expressions within kubernetes_resources", "error", err) } @@ -274,12 +277,12 @@ func (d *resourceFilterer) FilterObj(obj runtime.Object) (isAllowed bool, isList case *corev1.NodeList: o.Items = slices.FromPointers( filterResourceList( - d.kind, d.verb, + d.kind, d.group, d.verb, slices.ToPointers(o.Items), d.allowedResources, d.deniedResources, d.log), ) return len(o.Items) > 0, true, nil case *corev1.PersistentVolume: - result, err := filterResource(d.kind, d.verb, o, d.allowedResources, d.deniedResources) + result, err := filterResource(d.kind, d.group, d.verb, o, d.allowedResources, d.deniedResources) if err != nil { d.log.WarnContext(ctx, "Unable to compile regex expressions within kubernetes_resources", "error", err) } @@ -288,12 +291,12 @@ func (d *resourceFilterer) FilterObj(obj runtime.Object) (isAllowed bool, isList case *corev1.PersistentVolumeList: o.Items = slices.FromPointers( filterResourceList( - d.kind, d.verb, + d.kind, d.group, d.verb, slices.ToPointers(o.Items), d.allowedResources, d.deniedResources, d.log), ) return len(o.Items) > 0, true, nil case *corev1.PersistentVolumeClaim: - result, err := filterResource(d.kind, d.verb, o, d.allowedResources, d.deniedResources) + result, err := filterResource(d.kind, d.group, d.verb, o, d.allowedResources, d.deniedResources) if err != nil { d.log.WarnContext(ctx, "Unable to compile regex expressions within kubernetes_resources", "error", err) } @@ -302,13 +305,13 @@ func (d *resourceFilterer) FilterObj(obj runtime.Object) (isAllowed bool, isList case *corev1.PersistentVolumeClaimList: o.Items = slices.FromPointers( filterResourceList( - d.kind, d.verb, + d.kind, d.group, d.verb, slices.ToPointers(o.Items), d.allowedResources, d.deniedResources, d.log), ) return len(o.Items) > 0, true, nil case *appsv1.Deployment: - result, err := filterResource(d.kind, d.verb, o, d.allowedResources, d.deniedResources) + result, err := filterResource(d.kind, d.group, d.verb, o, d.allowedResources, d.deniedResources) if err != nil { d.log.WarnContext(ctx, "Unable to compile regex expressions within kubernetes_resources", "error", err) } @@ -317,13 +320,13 @@ func (d *resourceFilterer) FilterObj(obj runtime.Object) (isAllowed bool, isList case *appsv1.DeploymentList: o.Items = slices.FromPointers( filterResourceList( - d.kind, d.verb, + d.kind, d.group, d.verb, slices.ToPointers(o.Items), d.allowedResources, d.deniedResources, d.log), ) return len(o.Items) > 0, true, nil case *appsv1.ReplicaSet: - result, err := filterResource(d.kind, d.verb, o, d.allowedResources, d.deniedResources) + result, err := filterResource(d.kind, d.group, d.verb, o, d.allowedResources, d.deniedResources) if err != nil { d.log.WarnContext(ctx, "Unable to compile regex expressions within kubernetes_resources", "error", err) } @@ -332,12 +335,12 @@ func (d *resourceFilterer) FilterObj(obj runtime.Object) (isAllowed bool, isList case *appsv1.ReplicaSetList: o.Items = slices.FromPointers( filterResourceList( - d.kind, d.verb, + d.kind, d.group, d.verb, slices.ToPointers(o.Items), d.allowedResources, d.deniedResources, d.log), ) return len(o.Items) > 0, true, nil case *appsv1.StatefulSet: - result, err := filterResource(d.kind, d.verb, o, d.allowedResources, d.deniedResources) + result, err := filterResource(d.kind, d.group, d.verb, o, d.allowedResources, d.deniedResources) if err != nil { d.log.WarnContext(ctx, "Unable to compile regex expressions within kubernetes_resources", "error", err) } @@ -346,13 +349,13 @@ func (d *resourceFilterer) FilterObj(obj runtime.Object) (isAllowed bool, isList case *appsv1.StatefulSetList: o.Items = slices.FromPointers( filterResourceList( - d.kind, d.verb, + d.kind, d.group, d.verb, slices.ToPointers(o.Items), d.allowedResources, d.deniedResources, d.log), ) return len(o.Items) > 0, true, nil case *appsv1.DaemonSet: - result, err := filterResource(d.kind, d.verb, o, d.allowedResources, d.deniedResources) + result, err := filterResource(d.kind, d.group, d.verb, o, d.allowedResources, d.deniedResources) if err != nil { d.log.WarnContext(ctx, "Unable to compile regex expressions within kubernetes_resources", "error", err) } @@ -361,12 +364,12 @@ func (d *resourceFilterer) FilterObj(obj runtime.Object) (isAllowed bool, isList case *appsv1.DaemonSetList: o.Items = slices.FromPointers( filterResourceList( - d.kind, d.verb, + d.kind, d.group, d.verb, slices.ToPointers(o.Items), d.allowedResources, d.deniedResources, d.log), ) return len(o.Items) > 0, true, nil case *authv1.ClusterRole: - result, err := filterResource(d.kind, d.verb, o, d.allowedResources, d.deniedResources) + result, err := filterResource(d.kind, d.group, d.verb, o, d.allowedResources, d.deniedResources) if err != nil { d.log.WarnContext(ctx, "Unable to compile regex expressions within kubernetes_resources", "error", err) } @@ -375,12 +378,12 @@ func (d *resourceFilterer) FilterObj(obj runtime.Object) (isAllowed bool, isList case *authv1.ClusterRoleList: o.Items = slices.FromPointers( filterResourceList( - d.kind, d.verb, + d.kind, d.group, d.verb, slices.ToPointers(o.Items), d.allowedResources, d.deniedResources, d.log), ) return len(o.Items) > 0, true, nil case *authv1.Role: - result, err := filterResource(d.kind, d.verb, o, d.allowedResources, d.deniedResources) + result, err := filterResource(d.kind, d.group, d.verb, o, d.allowedResources, d.deniedResources) if err != nil { d.log.WarnContext(ctx, "Unable to compile regex expressions within kubernetes_resources", "error", err) } @@ -389,13 +392,13 @@ func (d *resourceFilterer) FilterObj(obj runtime.Object) (isAllowed bool, isList case *authv1.RoleList: o.Items = slices.FromPointers( filterResourceList( - d.kind, d.verb, + d.kind, d.group, d.verb, slices.ToPointers(o.Items), d.allowedResources, d.deniedResources, d.log), ) return len(o.Items) > 0, true, nil case *authv1.ClusterRoleBinding: - result, err := filterResource(d.kind, d.verb, o, d.allowedResources, d.deniedResources) + result, err := filterResource(d.kind, d.group, d.verb, o, d.allowedResources, d.deniedResources) if err != nil { d.log.WarnContext(ctx, "Unable to compile regex expressions within kubernetes_resources", "error", err) } @@ -404,13 +407,13 @@ func (d *resourceFilterer) FilterObj(obj runtime.Object) (isAllowed bool, isList case *authv1.ClusterRoleBindingList: o.Items = slices.FromPointers( filterResourceList( - d.kind, d.verb, + d.kind, d.group, d.verb, slices.ToPointers(o.Items), d.allowedResources, d.deniedResources, d.log), ) return len(o.Items) > 0, true, nil case *authv1.RoleBinding: - result, err := filterResource(d.kind, d.verb, o, d.allowedResources, d.deniedResources) + result, err := filterResource(d.kind, d.group, d.verb, o, d.allowedResources, d.deniedResources) if err != nil { d.log.WarnContext(ctx, "Unable to compile regex expressions within kubernetes_resources", "error", err) } @@ -419,13 +422,13 @@ func (d *resourceFilterer) FilterObj(obj runtime.Object) (isAllowed bool, isList case *authv1.RoleBindingList: o.Items = slices.FromPointers( filterResourceList( - d.kind, d.verb, + d.kind, d.group, d.verb, slices.ToPointers(o.Items), d.allowedResources, d.deniedResources, d.log), ) return len(o.Items) > 0, true, nil case *batchv1.CronJob: - result, err := filterResource(d.kind, d.verb, o, d.allowedResources, d.deniedResources) + result, err := filterResource(d.kind, d.group, d.verb, o, d.allowedResources, d.deniedResources) if err != nil { d.log.WarnContext(ctx, "Unable to compile regex expressions within kubernetes_resources", "error", err) } @@ -434,13 +437,13 @@ func (d *resourceFilterer) FilterObj(obj runtime.Object) (isAllowed bool, isList case *batchv1.CronJobList: o.Items = slices.FromPointers( filterResourceList( - d.kind, d.verb, + d.kind, d.group, d.verb, slices.ToPointers(o.Items), d.allowedResources, d.deniedResources, d.log), ) return len(o.Items) > 0, true, nil case *batchv1.Job: - result, err := filterResource(d.kind, d.verb, o, d.allowedResources, d.deniedResources) + result, err := filterResource(d.kind, d.group, d.verb, o, d.allowedResources, d.deniedResources) if err != nil { d.log.WarnContext(ctx, "Unable to compile regex expressions within kubernetes_resources", "error", err) } @@ -449,13 +452,13 @@ func (d *resourceFilterer) FilterObj(obj runtime.Object) (isAllowed bool, isList case *batchv1.JobList: o.Items = slices.FromPointers( filterResourceList( - d.kind, d.verb, + d.kind, d.group, d.verb, slices.ToPointers(o.Items), d.allowedResources, d.deniedResources, d.log), ) return len(o.Items) > 0, true, nil case *certificatesv1.CertificateSigningRequest: - result, err := filterResource(d.kind, d.verb, o, d.allowedResources, d.deniedResources) + result, err := filterResource(d.kind, d.group, d.verb, o, d.allowedResources, d.deniedResources) if err != nil { d.log.WarnContext(ctx, "Unable to compile regex expressions within kubernetes_resources", "error", err) } @@ -464,12 +467,12 @@ func (d *resourceFilterer) FilterObj(obj runtime.Object) (isAllowed bool, isList case *certificatesv1.CertificateSigningRequestList: o.Items = slices.FromPointers( filterResourceList( - d.kind, d.verb, + d.kind, d.group, d.verb, slices.ToPointers(o.Items), d.allowedResources, d.deniedResources, d.log), ) return len(o.Items) > 0, true, nil case *networkingv1.Ingress: - result, err := filterResource(d.kind, d.verb, o, d.allowedResources, d.deniedResources) + result, err := filterResource(d.kind, d.group, d.verb, o, d.allowedResources, d.deniedResources) if err != nil { d.log.WarnContext(ctx, "Unable to compile regex expressions within kubernetes_resources", "error", err) } @@ -478,12 +481,12 @@ func (d *resourceFilterer) FilterObj(obj runtime.Object) (isAllowed bool, isList case *networkingv1.IngressList: o.Items = slices.FromPointers( filterResourceList( - d.kind, d.verb, + d.kind, d.group, d.verb, slices.ToPointers(o.Items), d.allowedResources, d.deniedResources, d.log), ) return len(o.Items) > 0, true, nil case *extensionsv1beta1.Ingress: - result, err := filterResource(d.kind, d.verb, o, d.allowedResources, d.deniedResources) + result, err := filterResource(d.kind, d.group, d.verb, o, d.allowedResources, d.deniedResources) if err != nil { d.log.WarnContext(ctx, "Unable to compile regex expressions within kubernetes_resources", "error", err) } @@ -492,13 +495,13 @@ func (d *resourceFilterer) FilterObj(obj runtime.Object) (isAllowed bool, isList case *extensionsv1beta1.IngressList: o.Items = slices.FromPointers( filterResourceList( - d.kind, d.verb, + d.kind, d.group, d.verb, slices.ToPointers(o.Items), d.allowedResources, d.deniedResources, d.log), ) return len(o.Items) > 0, true, nil case *extensionsv1beta1.DaemonSet: - result, err := filterResource(d.kind, d.verb, o, d.allowedResources, d.deniedResources) + result, err := filterResource(d.kind, d.group, d.verb, o, d.allowedResources, d.deniedResources) if err != nil { d.log.WarnContext(ctx, "Unable to compile regex expressions within kubernetes_resources", "error", err) } @@ -507,13 +510,13 @@ func (d *resourceFilterer) FilterObj(obj runtime.Object) (isAllowed bool, isList case *extensionsv1beta1.DaemonSetList: o.Items = slices.FromPointers( filterResourceList( - d.kind, d.verb, + d.kind, d.group, d.verb, slices.ToPointers(o.Items), d.allowedResources, d.deniedResources, d.log), ) return len(o.Items) > 0, true, nil case *extensionsv1beta1.Deployment: - result, err := filterResource(d.kind, d.verb, o, d.allowedResources, d.deniedResources) + result, err := filterResource(d.kind, d.group, d.verb, o, d.allowedResources, d.deniedResources) if err != nil { d.log.WarnContext(ctx, "Unable to compile regex expressions within kubernetes_resources", "error", err) } @@ -522,13 +525,13 @@ func (d *resourceFilterer) FilterObj(obj runtime.Object) (isAllowed bool, isList case *extensionsv1beta1.DeploymentList: o.Items = slices.FromPointers( filterResourceList( - d.kind, d.verb, + d.kind, d.group, d.verb, slices.ToPointers(o.Items), d.allowedResources, d.deniedResources, d.log), ) return len(o.Items) > 0, true, nil case *extensionsv1beta1.ReplicaSet: - result, err := filterResource(d.kind, d.verb, o, d.allowedResources, d.deniedResources) + result, err := filterResource(d.kind, d.group, d.verb, o, d.allowedResources, d.deniedResources) if err != nil { d.log.WarnContext(ctx, "Unable to compile regex expressions within kubernetes_resources", "error", err) } @@ -537,18 +540,18 @@ func (d *resourceFilterer) FilterObj(obj runtime.Object) (isAllowed bool, isList case *extensionsv1beta1.ReplicaSetList: o.Items = slices.FromPointers( filterResourceList( - d.kind, d.verb, + d.kind, d.group, d.verb, slices.ToPointers(o.Items), d.allowedResources, d.deniedResources, d.log), ) return len(o.Items) > 0, true, nil case *unstructured.Unstructured: if o.IsList() { - hasElemts := filterUnstructuredList(d.verb, o, d.allowedResources, d.deniedResources, d.log) + hasElemts := filterUnstructuredList(d.kind, d.group, d.verb, o, d.allowedResources, d.deniedResources, d.log) return hasElemts, true, nil } - r := getKubeResource(utils.KubeCustomResource, d.verb, o) + r := getKubeResource(d.kind, d.group, d.verb, o) result, err := matchKubernetesResource( r, d.allowedResources, d.deniedResources, @@ -617,10 +620,10 @@ func (d *resourceFilterer) encode(obj runtime.Object, w io.Writer) error { } // filterResourceList excludes resources the user should not have access to. -func filterResourceList[T kubeObjectInterface](kind, verb string, originalList []T, allowed, denied []types.KubernetesResource, log *slog.Logger) []T { +func filterResourceList[T kubeObjectInterface](kind, group, verb string, originalList []T, allowed, denied []types.KubernetesResource, log *slog.Logger) []T { filteredList := make([]T, 0, len(originalList)) for _, resource := range originalList { - if result, err := filterResource(kind, verb, resource, allowed, denied); err == nil && result { + if result, err := filterResource(kind, group, verb, resource, allowed, denied); err == nil && result { filteredList = append(filteredList, resource) } else if err != nil { slog.WarnContext(context.Background(), "Unable to compile regex expressions within kubernetes_resources", "error", err) @@ -638,20 +641,21 @@ type kubeObjectInterface interface { } // filterResource validates if the user should access the current resource. -func filterResource(kind, verb string, resource kubeObjectInterface, allowed, denied []types.KubernetesResource) (bool, error) { +func filterResource(kind, group, verb string, resource kubeObjectInterface, allowed, denied []types.KubernetesResource) (bool, error) { result, err := matchKubernetesResource( - getKubeResource(kind, verb, resource), + getKubeResource(kind, group, verb, resource), allowed, denied, ) return result, trace.Wrap(err) } -func getKubeResource(kind, verb string, obj kubeObjectInterface) types.KubernetesResource { +func getKubeResource(kind, group, verb string, obj kubeObjectInterface) types.KubernetesResource { return types.KubernetesResource{ Kind: kind, Namespace: obj.GetNamespace(), Name: obj.GetName(), Verbs: []string{verb}, + APIGroup: group, } } @@ -664,7 +668,7 @@ func (d *resourceFilterer) filterMetaV1Table(table *metav1.Table, allowedResourc if err := d.decodePartialObjectMetadata(row); err != nil { return nil, trace.Wrap(err) } - resource, err := getKubeResourcePartialMetadataObject(d.kind, d.verb, row.Object.Object) + resource, err := getKubeResourcePartialMetadataObject(d.kind, d.group, d.verb, row.Object.Object) if err != nil { return nil, trace.Wrap(err) } @@ -680,7 +684,7 @@ func (d *resourceFilterer) filterMetaV1Table(table *metav1.Table, allowedResourc // getKubeResourcePartialMetadataObject checks if obj satisfies namespaceNamer or namer interfaces // otherwise returns an error. -func getKubeResourcePartialMetadataObject(kind, verb string, obj runtime.Object) (types.KubernetesResource, error) { +func getKubeResourcePartialMetadataObject(kind, group, verb string, obj runtime.Object) (types.KubernetesResource, error) { type namer interface { GetName() string } @@ -695,12 +699,14 @@ func getKubeResourcePartialMetadataObject(kind, verb string, obj runtime.Object) Name: o.GetName(), Kind: kind, Verbs: []string{verb}, + APIGroup: group, }, nil case namer: return types.KubernetesResource{ - Name: o.GetName(), - Kind: kind, - Verbs: []string{verb}, + Name: o.GetName(), + Kind: kind, + Verbs: []string{verb}, + APIGroup: group, }, nil default: return types.KubernetesResource{}, trace.BadParameter("unexpected %T type", obj) @@ -784,7 +790,7 @@ func filterBuffer(filterWrapper responsewriters.FilterWrapper, src *responsewrit // filterUnstructuredList filters the unstructured list object to exclude resources // that the user must not have access to. // The filtered list is re-assigned to `obj.Object["items"]`. -func filterUnstructuredList(verb string, obj *unstructured.Unstructured, allowed, denied []types.KubernetesResource, log *slog.Logger) (hasElems bool) { +func filterUnstructuredList(kind, group, verb string, obj *unstructured.Unstructured, allowed, denied []types.KubernetesResource, log *slog.Logger) (hasElems bool) { const ( itemsKey = "items" ) @@ -800,7 +806,7 @@ func filterUnstructuredList(verb string, obj *unstructured.Unstructured, allowed filteredList := make([]any, 0, len(objList.Items)) for _, resource := range objList.Items { - r := getKubeResource(utils.KubeCustomResource, verb, &resource) + r := getKubeResource(utils.KubeCustomResource, group, verb, &resource) if result, err := matchKubernetesResource( r, allowed, denied, diff --git a/lib/kube/proxy/resource_filters_test.go b/lib/kube/proxy/resource_filters_test.go index e00779a2bf6..7d71bcaea7e 100644 --- a/lib/kube/proxy/resource_filters_test.go +++ b/lib/kube/proxy/resource_filters_test.go @@ -155,6 +155,7 @@ func Test_filterBuffer(t *testing.T) { allowedResources := []types.KubernetesResource{ { Kind: r, + APIGroup: "*", Namespace: "default", Name: "*", Verbs: []string{types.KubeVerbList}, @@ -175,7 +176,7 @@ func Test_filterBuffer(t *testing.T) { buf, decompress := newMemoryResponseWriter(t, data.Bytes(), tt.args.contentEncoding) - err = filterBuffer(newResourceFilterer(r, types.KubeVerbList, &globalKubeCodecs, allowedResources, nil, utils.NewSlogLoggerForTests()), buf) + err = filterBuffer(newResourceFilterer(r, "", types.KubeVerbList, &globalKubeCodecs, allowedResources, nil, utils.NewSlogLoggerForTests()), buf) require.NoError(t, err) // Decompress the buffer to compare the result. @@ -235,7 +236,7 @@ func Test_filterBuffer(t *testing.T) { require.NoError(t, err) } - resource, err := getKubeResourcePartialMetadataObject(r, "list", row.Object.Object) + resource, err := getKubeResourcePartialMetadataObject(r, "", "list", row.Object.Object) require.NoError(t, err) resources = append(resources, resource.Namespace+"/"+resource.Name) } diff --git a/lib/kube/proxy/resource_list.go b/lib/kube/proxy/resource_list.go index 02d761cbb29..df169fc5cd0 100644 --- a/lib/kube/proxy/resource_list.go +++ b/lib/kube/proxy/resource_list.go @@ -71,7 +71,7 @@ func (f *Forwarder) listResources(sess *clusterSession, w http.ResponseWriter, r status = rw.Status() } else { allowedResources, deniedResources := sess.Checker.GetKubeResources(sess.kubeCluster) - shouldBeAllowed, err := matchListRequestShouldBeAllowed(sess, resourceKind, allowedResources, deniedResources) + shouldBeAllowed, err := matchListRequestShouldBeAllowed(sess, resourceKind, sess.apiResource.apiGroup, allowedResources, deniedResources) if err != nil { return nil, trace.Wrap(err) } @@ -123,7 +123,7 @@ func (f *Forwarder) listResourcesList(req *http.Request, w http.ResponseWriter, // filterBuffer filters the response to exclude resources the user doesn't have access to. // The filtered payload will be written into memBuffer again. if err := filterBuffer( - newResourceFilterer(resourceKind, verb, sess.codecFactory, allowedResources, deniedResources, f.log), + newResourceFilterer(resourceKind, sess.apiResource.apiGroup, verb, sess.codecFactory, allowedResources, deniedResources, f.log), memBuffer, ); err != nil { return memBuffer.Status(), trace.Wrap(err) @@ -140,12 +140,14 @@ func (f *Forwarder) listResourcesList(req *http.Request, w http.ResponseWriter, // has no access and present then a more user-friendly error message instead of returning // an empty list. // This function is not responsible for enforcing access rules. -func matchListRequestShouldBeAllowed(sess *clusterSession, resourceKind string, allowedResources, deniedResources []types.KubernetesResource) (bool, error) { +func matchListRequestShouldBeAllowed(sess *clusterSession, resourceKind, resourceGroup string, allowedResources, deniedResources []types.KubernetesResource) (bool, error) { resource := types.KubernetesResource{ Kind: resourceKind, Namespace: sess.apiResource.namespace, Verbs: []string{sess.requestVerb}, + APIGroup: resourceGroup, } + result, err := utils.KubeResourceCouldMatchRules(resource, deniedResources, types.Deny) if err != nil { return false, trace.Wrap(err) @@ -182,6 +184,7 @@ func (f *Forwarder) listResourcesWatcher(req *http.Request, w http.ResponseWrite negotiator, newResourceFilterer( resourceKind, + sess.apiResource.resourceKind, verb, sess.codecFactory, allowedResources, diff --git a/lib/kube/proxy/resource_rbac_test.go b/lib/kube/proxy/resource_rbac_test.go index faebea64668..eba9d5cd9d1 100644 --- a/lib/kube/proxy/resource_rbac_test.go +++ b/lib/kube/proxy/resource_rbac_test.go @@ -100,6 +100,7 @@ func TestListPodRBAC(t *testing.T) { Name: types.Wildcard, Namespace: types.Wildcard, Verbs: []string{types.Wildcard}, + APIGroup: types.Wildcard, }, }) }, @@ -123,6 +124,7 @@ func TestListPodRBAC(t *testing.T) { Name: types.Wildcard, Namespace: metav1.NamespaceDefault, Verbs: []string{types.Wildcard}, + APIGroup: types.Wildcard, }, }) }, @@ -148,6 +150,7 @@ func TestListPodRBAC(t *testing.T) { Name: types.Wildcard, Namespace: "{{external.namespaces}}", Verbs: []string{types.Wildcard}, + APIGroup: types.Wildcard, }, }) }, @@ -172,6 +175,7 @@ func TestListPodRBAC(t *testing.T) { Name: "nginx-*", Namespace: metav1.NamespaceDefault, Verbs: []string{types.Wildcard}, + APIGroup: types.Wildcard, }, }, ) @@ -196,6 +200,7 @@ func TestListPodRBAC(t *testing.T) { Name: "*", Namespace: metav1.NamespaceDefault, Verbs: []string{"get"}, + APIGroup: types.Wildcard, }, }, ) @@ -221,6 +226,7 @@ func TestListPodRBAC(t *testing.T) { Name: types.Wildcard, Namespace: types.Wildcard, Verbs: []string{types.Wildcard}, + APIGroup: types.Wildcard, }, }, ) @@ -231,6 +237,7 @@ func TestListPodRBAC(t *testing.T) { Name: types.Wildcard, Namespace: metav1.NamespaceDefault, Verbs: []string{types.Wildcard}, + APIGroup: types.Wildcard, }, }, ) @@ -476,7 +483,6 @@ func TestListPodRBAC(t *testing.T) { return pods } for _, tt := range tests { - tt := tt t.Run(tt.name, func(t *testing.T) { t.Parallel() // generate a kube client with user certs for auth @@ -562,6 +568,7 @@ func TestWatcherResponseWriter(t *testing.T) { Namespace: "*", Name: "*", Verbs: []string{types.Wildcard}, + APIGroup: "core", }, }, }, @@ -576,6 +583,7 @@ func TestWatcherResponseWriter(t *testing.T) { Namespace: defaultNamespace, Name: "*", Verbs: []string{types.Wildcard}, + APIGroup: "core", }, }, }, @@ -590,6 +598,7 @@ func TestWatcherResponseWriter(t *testing.T) { Namespace: defaultNamespace, Name: "*", Verbs: []string{types.Wildcard}, + APIGroup: "core", }, }, denied: []types.KubernetesResource{ @@ -598,6 +607,7 @@ func TestWatcherResponseWriter(t *testing.T) { Namespace: defaultNamespace, Name: "otherPod", Verbs: []string{types.Wildcard}, + APIGroup: "core", }, }, }, @@ -612,6 +622,7 @@ func TestWatcherResponseWriter(t *testing.T) { Namespace: defaultNamespace, Name: "rand*", Verbs: []string{types.Wildcard}, + APIGroup: "core", }, }, }, @@ -627,11 +638,10 @@ func TestWatcherResponseWriter(t *testing.T) { }, } for _, tt := range tests { - tt := tt t.Run(tt.name, func(t *testing.T) { userReader, userWriter := io.Pipe() negotiator := newClientNegotiator(&globalKubeCodecs) - filterWrapper := newResourceFilterer(types.KindKubePod, types.KubeVerbWatch, &globalKubeCodecs, tt.args.allowed, tt.args.denied, utils.NewSlogLoggerForTests()) + filterWrapper := newResourceFilterer(types.KindKubePod, "core", types.KubeVerbWatch, &globalKubeCodecs, tt.args.allowed, tt.args.denied, utils.NewSlogLoggerForTests()) // watcher parses the data written into itself and if the user is allowed to // receive the update, it writes the event into target. watcher, err := responsewriters.NewWatcherResponseWriter(newFakeResponseWriter(userWriter) /*target*/, negotiator, filterWrapper) @@ -867,6 +877,7 @@ func TestDeletePodCollectionRBAC(t *testing.T) { Name: types.Wildcard, Namespace: types.Wildcard, Verbs: []string{types.Wildcard}, + APIGroup: types.Wildcard, }, }) }, @@ -890,6 +901,7 @@ func TestDeletePodCollectionRBAC(t *testing.T) { Name: types.Wildcard, Namespace: metav1.NamespaceDefault, Verbs: []string{types.Wildcard}, + APIGroup: types.Wildcard, }, }) }, @@ -914,6 +926,7 @@ func TestDeletePodCollectionRBAC(t *testing.T) { Name: "nginx-*", Namespace: metav1.NamespaceDefault, Verbs: []string{types.Wildcard}, + APIGroup: types.Wildcard, }, }, ) @@ -1052,9 +1065,10 @@ func TestListClusterRoleRBAC(t *testing.T) { SetupRoleFunc: func(r types.Role) { r.SetKubeResources(types.Allow, []types.KubernetesResource{ { - Kind: types.KindKubeClusterRole, - Name: types.Wildcard, - Verbs: []string{types.Wildcard}, + Kind: types.KindKubeClusterRole, + Name: types.Wildcard, + Verbs: []string{types.Wildcard}, + APIGroup: types.Wildcard, }, }) }, @@ -1075,9 +1089,10 @@ func TestListClusterRoleRBAC(t *testing.T) { r.SetKubeResources(types.Allow, []types.KubernetesResource{ { - Kind: types.KindKubeClusterRole, - Name: "nginx-*", - Verbs: []string{types.Wildcard}, + Kind: types.KindKubeClusterRole, + Name: "nginx-*", + Verbs: []string{types.Wildcard}, + APIGroup: types.Wildcard, }, }, ) @@ -1276,9 +1291,10 @@ func TestCustomResourcesRBAC(t *testing.T) { SetupRoleFunc: func(r types.Role) { r.SetKubeResources(types.Allow, []types.KubernetesResource{ { - Kind: types.KindKubeNamespace, - Name: types.Wildcard, - Verbs: []string{types.Wildcard}, + Kind: types.KindKubeNamespace, + Name: types.Wildcard, + Verbs: []string{types.Wildcard}, + APIGroup: types.Wildcard, }, }) }, @@ -1298,9 +1314,10 @@ func TestCustomResourcesRBAC(t *testing.T) { r.SetKubeResources(types.Allow, []types.KubernetesResource{ { - Kind: types.KindKubeNamespace, - Name: "dev", - Verbs: []string{types.Wildcard}, + Kind: types.KindKubeNamespace, + Name: "dev", + Verbs: []string{types.Wildcard}, + APIGroup: types.Wildcard, }, }, ) diff --git a/lib/kube/proxy/self_subject_reviews_test.go b/lib/kube/proxy/self_subject_reviews_test.go index b1557e50c40..523d7f2eb4d 100644 --- a/lib/kube/proxy/self_subject_reviews_test.go +++ b/lib/kube/proxy/self_subject_reviews_test.go @@ -77,6 +77,7 @@ func TestSelfSubjectAccessReviewsRBAC(t *testing.T) { Namespace: types.Wildcard, Name: types.Wildcard, Verbs: []string{types.Wildcard}, + APIGroup: types.Wildcard, }, }, }, @@ -94,6 +95,7 @@ func TestSelfSubjectAccessReviewsRBAC(t *testing.T) { Namespace: types.Wildcard, Name: types.Wildcard, Verbs: []string{types.Wildcard}, + APIGroup: types.Wildcard, }, }, }, @@ -111,6 +113,7 @@ func TestSelfSubjectAccessReviewsRBAC(t *testing.T) { Namespace: types.Wildcard, Name: types.Wildcard, Verbs: []string{types.Wildcard}, + APIGroup: types.Wildcard, }, }, }, @@ -128,6 +131,7 @@ func TestSelfSubjectAccessReviewsRBAC(t *testing.T) { Name: "pod-2", Namespace: "namespace-1", Verbs: []string{types.Wildcard}, + APIGroup: types.Wildcard, }, }, }, @@ -145,12 +149,14 @@ func TestSelfSubjectAccessReviewsRBAC(t *testing.T) { Name: "pod-2", Namespace: "namespace-1", Verbs: []string{types.Wildcard}, + APIGroup: types.Wildcard, }, { Kind: types.KindKubePod, Name: "pod-1", Namespace: "namespace-1", Verbs: []string{types.Wildcard}, + APIGroup: types.Wildcard, }, }, }, @@ -168,6 +174,7 @@ func TestSelfSubjectAccessReviewsRBAC(t *testing.T) { Name: "pod-2", Namespace: "namespace-1", Verbs: []string{types.Wildcard}, + APIGroup: types.Wildcard, }, }, }, @@ -185,6 +192,7 @@ func TestSelfSubjectAccessReviewsRBAC(t *testing.T) { Name: "pod-2", Namespace: "namespace-1", Verbs: []string{types.Wildcard}, + APIGroup: types.Wildcard, }, }, }, @@ -197,9 +205,10 @@ func TestSelfSubjectAccessReviewsRBAC(t *testing.T) { kind: "namespaces", resources: []types.KubernetesResource{ { - Kind: types.KindKubeNamespace, - Name: "namespace-2", - Verbs: []string{types.Wildcard}, + Kind: types.KindKubeNamespace, + Name: "namespace-2", + Verbs: []string{types.Wildcard}, + APIGroup: types.Wildcard, }, }, }, @@ -212,9 +221,10 @@ func TestSelfSubjectAccessReviewsRBAC(t *testing.T) { kind: "namespaces", resources: []types.KubernetesResource{ { - Kind: types.KindKubeNamespace, - Name: "namespace", - Verbs: []string{types.Wildcard}, + Kind: types.KindKubeNamespace, + Name: "namespace", + Verbs: []string{types.Wildcard}, + APIGroup: types.Wildcard, }, }, }, @@ -227,9 +237,10 @@ func TestSelfSubjectAccessReviewsRBAC(t *testing.T) { kind: "pods", resources: []types.KubernetesResource{ { - Kind: types.KindKubeNamespace, - Name: "namespace-2", - Verbs: []string{types.Wildcard}, + Kind: types.KindKubeNamespace, + Name: "namespace-2", + Verbs: []string{types.Wildcard}, + APIGroup: types.Wildcard, }, }, }, @@ -259,9 +270,10 @@ func TestSelfSubjectAccessReviewsRBAC(t *testing.T) { apiGroup: "resources.teleport.dev", resources: []types.KubernetesResource{ { - Kind: types.KindKubeNamespace, - Name: "namespace-2", - Verbs: []string{types.Wildcard}, + Kind: types.KindKubeNamespace, + Name: "namespace-2", + Verbs: []string{types.Wildcard}, + APIGroup: types.Wildcard, }, }, }, @@ -275,9 +287,10 @@ func TestSelfSubjectAccessReviewsRBAC(t *testing.T) { apiGroup: "rbac.authorization.k8s.io", resources: []types.KubernetesResource{ { - Kind: types.KindKubeNamespace, - Name: "namespace-2", - Verbs: []string{types.Wildcard}, + Kind: types.KindKubeNamespace, + Name: "namespace-2", + Verbs: []string{types.Wildcard}, + APIGroup: types.Wildcard, }, }, }, @@ -291,9 +304,10 @@ func TestSelfSubjectAccessReviewsRBAC(t *testing.T) { apiGroup: "rbac.authorization.k8s.io", resources: []types.KubernetesResource{ { - Kind: types.KindKubeClusterRole, - Name: "role", - Verbs: []string{types.Wildcard}, + Kind: types.KindKubeClusterRole, + Name: "role", + Verbs: []string{types.Wildcard}, + APIGroup: types.Wildcard, }, }, }, @@ -307,9 +321,10 @@ func TestSelfSubjectAccessReviewsRBAC(t *testing.T) { apiGroup: "rbac.authorization.k8s.io", resources: []types.KubernetesResource{ { - Kind: types.KindKubeClusterRole, - Name: "role", - Verbs: []string{types.Wildcard}, + Kind: types.KindKubeClusterRole, + Name: "role", + Verbs: []string{types.Wildcard}, + APIGroup: types.Wildcard, }, }, }, @@ -323,9 +338,10 @@ func TestSelfSubjectAccessReviewsRBAC(t *testing.T) { apiGroup: "rbac.authorization.k8s.io", resources: []types.KubernetesResource{ { - Kind: types.KindKubeClusterRole, - Name: "role", - Verbs: []string{types.Wildcard}, + Kind: types.KindKubeClusterRole, + Name: "role", + Verbs: []string{types.Wildcard}, + APIGroup: types.Wildcard, }, }, }, @@ -339,9 +355,10 @@ func TestSelfSubjectAccessReviewsRBAC(t *testing.T) { apiGroup: "rbac.authorization.k8s.io", resources: []types.KubernetesResource{ { - Kind: types.KindKubeClusterRole, - Name: "role", - Verbs: []string{"get"}, + Kind: types.KindKubeClusterRole, + Name: "role", + Verbs: []string{"get"}, + APIGroup: types.Wildcard, }, }, }, @@ -355,9 +372,10 @@ func TestSelfSubjectAccessReviewsRBAC(t *testing.T) { namespace: "namespace-1", resources: []types.KubernetesResource{ { - Kind: types.KindKubeNamespace, - Name: "namespace-1", - Verbs: []string{types.Wildcard}, + Kind: types.KindKubeNamespace, + Name: "namespace-1", + Verbs: []string{types.Wildcard}, + APIGroup: types.Wildcard, }, }, }, @@ -366,7 +384,6 @@ func TestSelfSubjectAccessReviewsRBAC(t *testing.T) { } for _, tt := range tests { - tt := tt t.Run(tt.name, func(t *testing.T) { t.Parallel() // create a user with full access to kubernetes Pods. diff --git a/lib/kube/proxy/single_cert_handler_test.go b/lib/kube/proxy/single_cert_handler_test.go index 19fda6c63bb..ad5aec6691c 100644 --- a/lib/kube/proxy/single_cert_handler_test.go +++ b/lib/kube/proxy/single_cert_handler_test.go @@ -115,7 +115,7 @@ func TestSingleCertRouting(t *testing.T) { KubeUsers: roleKubeUsers, KubeGroups: roleKubeGroups, SetupRoleFunc: func(r types.Role) { - r.SetKubeResources(types.Deny, []types.KubernetesResource{{Kind: types.KindKubePod, Name: types.Wildcard, Namespace: types.Wildcard, Verbs: []string{types.Wildcard}}}) + r.SetKubeResources(types.Deny, []types.KubernetesResource{{Kind: types.KindKubePod, Name: types.Wildcard, Namespace: types.Wildcard, Verbs: []string{types.Wildcard}, APIGroup: types.Wildcard}}) }, }, assert: func(t *testing.T, restConfig *rest.Config) { @@ -143,7 +143,7 @@ func TestSingleCertRouting(t *testing.T) { KubeUsers: roleKubeUsers, KubeGroups: roleKubeGroups, SetupRoleFunc: func(r types.Role) { - r.SetKubeResources(types.Allow, []types.KubernetesResource{{Kind: types.KindKubePod, Name: types.Wildcard, Namespace: types.Wildcard, Verbs: []string{types.Wildcard}}}) + r.SetKubeResources(types.Allow, []types.KubernetesResource{{Kind: types.KindKubePod, Name: types.Wildcard, Namespace: types.Wildcard, Verbs: []string{types.Wildcard}, APIGroup: types.Wildcard}}) r.SetOptions(types.RoleOptions{ RequireMFAType: types.RequireMFAType_SESSION, }) @@ -165,7 +165,7 @@ func TestSingleCertRouting(t *testing.T) { KubeUsers: roleKubeUsers, KubeGroups: roleKubeGroups, SetupRoleFunc: func(r types.Role) { - r.SetKubeResources(types.Allow, []types.KubernetesResource{{Kind: types.KindKubePod, Name: types.Wildcard, Namespace: types.Wildcard, Verbs: []string{types.Wildcard}}}) + r.SetKubeResources(types.Allow, []types.KubernetesResource{{Kind: types.KindKubePod, Name: types.Wildcard, Namespace: types.Wildcard, Verbs: []string{types.Wildcard}, APIGroup: types.Wildcard}}) r.SetOptions(types.RoleOptions{ RequireMFAType: types.RequireMFAType_SESSION, }) diff --git a/lib/kube/proxy/url.go b/lib/kube/proxy/url.go index 6b50c7427fc..6498b318559 100644 --- a/lib/kube/proxy/url.go +++ b/lib/kube/proxy/url.go @@ -249,6 +249,7 @@ func getResourceFromRequest(req *http.Request, kubeDetails *kubeDetails) (*types Namespace: apiResource.namespace, Name: apiResource.resourceName, Verbs: []string{verb}, + APIGroup: apiResource.apiGroup, }, apiResource, nil } diff --git a/lib/kube/proxy/url_test.go b/lib/kube/proxy/url_test.go index 64bd96ed723..d54e43ee6c4 100644 --- a/lib/kube/proxy/url_test.go +++ b/lib/kube/proxy/url_test.go @@ -102,120 +102,120 @@ func Test_getResourceFromRequest(t *testing.T) { {path: "/apis/apps/v1/", want: nil}, {path: "/api/v1/pods", want: nil}, {path: "/api/v1/watch/pods", want: nil}, - {path: "/api/v1/namespaces/kube-system", want: &types.KubernetesResource{Kind: types.KindKubeNamespace, Name: "kube-system", Verbs: []string{"get"}}}, - {path: "/api/v1/watch/namespaces/kube-system", want: &types.KubernetesResource{Kind: types.KindKubeNamespace, Name: "kube-system", Verbs: []string{"watch"}}}, + {path: "/api/v1/namespaces/kube-system", want: &types.KubernetesResource{Kind: types.KindKubeNamespace, Name: "kube-system", Verbs: []string{"get"}, APIGroup: "core"}}, + {path: "/api/v1/watch/namespaces/kube-system", want: &types.KubernetesResource{Kind: types.KindKubeNamespace, Name: "kube-system", Verbs: []string{"watch"}, APIGroup: "core"}}, {path: "/api/v1/namespaces/kube-system/pods", want: nil}, {path: "/api/v1/watch/namespaces/kube-system/pods", want: nil}, - {path: "/api/v1/namespaces/kube-system/pods/foo", want: &types.KubernetesResource{Kind: types.KindKubePod, Namespace: "kube-system", Name: "foo", Verbs: []string{"get"}}}, - {path: "/api/v1/watch/namespaces/kube-system/pods/foo", want: &types.KubernetesResource{Kind: types.KindKubePod, Namespace: "kube-system", Name: "foo", Verbs: []string{"watch"}}}, + {path: "/api/v1/namespaces/kube-system/pods/foo", want: &types.KubernetesResource{Kind: types.KindKubePod, Namespace: "kube-system", Name: "foo", Verbs: []string{"get"}, APIGroup: "core"}}, + {path: "/api/v1/watch/namespaces/kube-system/pods/foo", want: &types.KubernetesResource{Kind: types.KindKubePod, Namespace: "kube-system", Name: "foo", Verbs: []string{"watch"}, APIGroup: "core"}}, {path: "/apis/apiregistration.k8s.io/v1/apiservices/foo/status", want: nil}, // core // Pods {path: "/api/v1/pods", want: nil}, {path: "/api/v1/namespaces/default/pods", want: nil}, - {path: "/api/v1/namespaces/default/pods/foo", want: &types.KubernetesResource{Kind: types.KindKubePod, Namespace: "default", Name: "foo", Verbs: []string{"get"}}}, - {path: "/api/v1/watch/namespaces/default/pods/foo", want: &types.KubernetesResource{Kind: types.KindKubePod, Namespace: "default", Name: "foo", Verbs: []string{"watch"}}}, - {path: "/api/v1/namespaces/kube-system/pods/foo/exec", want: &types.KubernetesResource{Kind: types.KindKubePod, Namespace: "kube-system", Name: "foo", Verbs: []string{"exec"}}}, - {path: "/api/v1/namespaces/kube-system/pods/foo/attach", want: &types.KubernetesResource{Kind: types.KindKubePod, Namespace: "kube-system", Name: "foo", Verbs: []string{"exec"}}}, - {path: "/api/v1/namespaces/kube-system/pods/foo/portforward", want: &types.KubernetesResource{Kind: types.KindKubePod, Namespace: "kube-system", Name: "foo", Verbs: []string{"portforward"}}}, - {path: "/api/v1/namespaces/default/pods", body: bodyFunc("Pod", "v1"), want: &types.KubernetesResource{Kind: types.KindKubePod, Namespace: "default", Name: "foo-create", Verbs: []string{"create"}}}, - {path: "/api/v1/namespaces/default/pods", body: bodyFuncWithoutGVK(), want: &types.KubernetesResource{Kind: types.KindKubePod, Namespace: "default", Name: "foo-create", Verbs: []string{"create"}}}, + {path: "/api/v1/namespaces/default/pods/foo", want: &types.KubernetesResource{Kind: types.KindKubePod, Namespace: "default", Name: "foo", Verbs: []string{"get"}, APIGroup: "core"}}, + {path: "/api/v1/watch/namespaces/default/pods/foo", want: &types.KubernetesResource{Kind: types.KindKubePod, Namespace: "default", Name: "foo", Verbs: []string{"watch"}, APIGroup: "core"}}, + {path: "/api/v1/namespaces/kube-system/pods/foo/exec", want: &types.KubernetesResource{Kind: types.KindKubePod, Namespace: "kube-system", Name: "foo", Verbs: []string{"exec"}, APIGroup: "core"}}, + {path: "/api/v1/namespaces/kube-system/pods/foo/attach", want: &types.KubernetesResource{Kind: types.KindKubePod, Namespace: "kube-system", Name: "foo", Verbs: []string{"exec"}, APIGroup: "core"}}, + {path: "/api/v1/namespaces/kube-system/pods/foo/portforward", want: &types.KubernetesResource{Kind: types.KindKubePod, Namespace: "kube-system", Name: "foo", Verbs: []string{"portforward"}, APIGroup: "core"}}, + {path: "/api/v1/namespaces/default/pods", body: bodyFunc("Pod", "v1"), want: &types.KubernetesResource{Kind: types.KindKubePod, Namespace: "default", Name: "foo-create", Verbs: []string{"create"}, APIGroup: "core"}}, + {path: "/api/v1/namespaces/default/pods", body: bodyFuncWithoutGVK(), want: &types.KubernetesResource{Kind: types.KindKubePod, Namespace: "default", Name: "foo-create", Verbs: []string{"create"}, APIGroup: "core"}}, // Secrets {path: "/api/v1/secrets", want: nil}, {path: "/api/v1/namespaces/default/secrets", want: nil}, - {path: "/api/v1/namespaces/default/secrets/foo", want: &types.KubernetesResource{Kind: types.KindKubeSecret, Namespace: "default", Name: "foo", Verbs: []string{"get"}}}, - {path: "/api/v1/watch/namespaces/default/secrets/foo", want: &types.KubernetesResource{Kind: types.KindKubeSecret, Namespace: "default", Name: "foo", Verbs: []string{"watch"}}}, - {path: "/api/v1/namespaces/default/secrets", body: bodyFunc("Secret", "v1"), want: &types.KubernetesResource{Kind: types.KindKubeSecret, Namespace: "default", Name: "foo-create", Verbs: []string{"create"}}}, - {path: "/api/v1/namespaces/default/secrets", body: bodyFuncWithoutGVK(), want: &types.KubernetesResource{Kind: types.KindKubeSecret, Namespace: "default", Name: "foo-create", Verbs: []string{"create"}}}, + {path: "/api/v1/namespaces/default/secrets/foo", want: &types.KubernetesResource{Kind: types.KindKubeSecret, Namespace: "default", Name: "foo", Verbs: []string{"get"}, APIGroup: "core"}}, + {path: "/api/v1/watch/namespaces/default/secrets/foo", want: &types.KubernetesResource{Kind: types.KindKubeSecret, Namespace: "default", Name: "foo", Verbs: []string{"watch"}, APIGroup: "core"}}, + {path: "/api/v1/namespaces/default/secrets", body: bodyFunc("Secret", "v1"), want: &types.KubernetesResource{Kind: types.KindKubeSecret, Namespace: "default", Name: "foo-create", Verbs: []string{"create"}, APIGroup: "core"}}, + {path: "/api/v1/namespaces/default/secrets", body: bodyFuncWithoutGVK(), want: &types.KubernetesResource{Kind: types.KindKubeSecret, Namespace: "default", Name: "foo-create", Verbs: []string{"create"}, APIGroup: "core"}}, // Configmaps {path: "/api/v1/configmaps", want: nil}, {path: "/api/v1/namespaces/default/configmaps", want: nil}, - {path: "/api/v1/namespaces/default/configmaps/foo", want: &types.KubernetesResource{Kind: types.KindKubeConfigmap, Namespace: "default", Name: "foo", Verbs: []string{"get"}}}, - {path: "/api/v1/watch/namespaces/default/configmaps/foo", want: &types.KubernetesResource{Kind: types.KindKubeConfigmap, Namespace: "default", Name: "foo", Verbs: []string{"watch"}}}, - {path: "/api/v1/namespaces/default/configmaps", body: bodyFunc("ConfigMap", "v1"), want: &types.KubernetesResource{Kind: types.KindKubeConfigmap, Namespace: "default", Name: "foo-create", Verbs: []string{"create"}}}, - {path: "/api/v1/namespaces/default/configmaps", body: bodyFuncWithoutGVK(), want: &types.KubernetesResource{Kind: types.KindKubeConfigmap, Namespace: "default", Name: "foo-create", Verbs: []string{"create"}}}, + {path: "/api/v1/namespaces/default/configmaps/foo", want: &types.KubernetesResource{Kind: types.KindKubeConfigmap, Namespace: "default", Name: "foo", Verbs: []string{"get"}, APIGroup: "core"}}, + {path: "/api/v1/watch/namespaces/default/configmaps/foo", want: &types.KubernetesResource{Kind: types.KindKubeConfigmap, Namespace: "default", Name: "foo", Verbs: []string{"watch"}, APIGroup: "core"}}, + {path: "/api/v1/namespaces/default/configmaps", body: bodyFunc("ConfigMap", "v1"), want: &types.KubernetesResource{Kind: types.KindKubeConfigmap, Namespace: "default", Name: "foo-create", Verbs: []string{"create"}, APIGroup: "core"}}, + {path: "/api/v1/namespaces/default/configmaps", body: bodyFuncWithoutGVK(), want: &types.KubernetesResource{Kind: types.KindKubeConfigmap, Namespace: "default", Name: "foo-create", Verbs: []string{"create"}, APIGroup: "core"}}, // Namespaces {path: "/api/v1/namespaces", want: nil}, - {path: "/api/v1/namespaces/default", want: &types.KubernetesResource{Kind: types.KindKubeNamespace, Name: "default", Verbs: []string{"get"}}}, - {path: "/api/v1/watch/namespaces/default", want: &types.KubernetesResource{Kind: types.KindKubeNamespace, Name: "default", Verbs: []string{"watch"}}}, - {path: "/api/v1/namespaces", body: bodyFunc("Namespace", "v1"), want: &types.KubernetesResource{Kind: types.KindKubeNamespace, Name: "foo-create", Verbs: []string{"create"}}}, - {path: "/api/v1/namespaces", body: bodyFuncWithoutGVK(), want: &types.KubernetesResource{Kind: types.KindKubeNamespace, Name: "foo-create", Verbs: []string{"create"}}}, + {path: "/api/v1/namespaces/default", want: &types.KubernetesResource{Kind: types.KindKubeNamespace, Name: "default", Verbs: []string{"get"}, APIGroup: "core"}}, + {path: "/api/v1/watch/namespaces/default", want: &types.KubernetesResource{Kind: types.KindKubeNamespace, Name: "default", Verbs: []string{"watch"}, APIGroup: "core"}}, + {path: "/api/v1/namespaces", body: bodyFunc("Namespace", "v1"), want: &types.KubernetesResource{Kind: types.KindKubeNamespace, Name: "foo-create", Verbs: []string{"create"}, APIGroup: "core"}}, + {path: "/api/v1/namespaces", body: bodyFuncWithoutGVK(), want: &types.KubernetesResource{Kind: types.KindKubeNamespace, Name: "foo-create", Verbs: []string{"create"}, APIGroup: "core"}}, // Nodes {path: "/api/v1/nodes", want: nil}, - {path: "/api/v1/nodes/foo/proxy/bar", want: &types.KubernetesResource{Kind: types.KindKubeNode, Name: "foo", Verbs: []string{"get"}}}, + {path: "/api/v1/nodes/foo/proxy/bar", want: &types.KubernetesResource{Kind: types.KindKubeNode, Name: "foo", Verbs: []string{"get"}, APIGroup: "core"}}, // Services {path: "/api/v1/services", want: nil}, {path: "/api/v1/namespaces/default/services", want: nil}, - {path: "/api/v1/namespaces/default/services/foo", want: &types.KubernetesResource{Kind: types.KindKubeService, Namespace: "default", Name: "foo", Verbs: []string{"get"}}}, - {path: "/api/v1/watch/namespaces/default/services/foo", want: &types.KubernetesResource{Kind: types.KindKubeService, Namespace: "default", Name: "foo", Verbs: []string{"watch"}}}, - {path: "/api/v1/namespaces/default/services", body: bodyFunc("Service", "v1"), want: &types.KubernetesResource{Kind: types.KindKubeService, Namespace: "default", Name: "foo-create", Verbs: []string{"create"}}}, + {path: "/api/v1/namespaces/default/services/foo", want: &types.KubernetesResource{Kind: types.KindKubeService, Namespace: "default", Name: "foo", Verbs: []string{"get"}, APIGroup: "core"}}, + {path: "/api/v1/watch/namespaces/default/services/foo", want: &types.KubernetesResource{Kind: types.KindKubeService, Namespace: "default", Name: "foo", Verbs: []string{"watch"}, APIGroup: "core"}}, + {path: "/api/v1/namespaces/default/services", body: bodyFunc("Service", "v1"), want: &types.KubernetesResource{Kind: types.KindKubeService, Namespace: "default", Name: "foo-create", Verbs: []string{"create"}, APIGroup: "core"}}, // ServiceAccounts {path: "/api/v1/serviceaccounts", want: nil}, {path: "/api/v1/namespaces/default/serviceaccounts", want: nil}, - {path: "/api/v1/namespaces/default/serviceaccounts/foo", want: &types.KubernetesResource{Kind: types.KindKubeServiceAccount, Namespace: "default", Name: "foo", Verbs: []string{"get"}}}, - {path: "/api/v1/watch/namespaces/default/serviceaccounts/foo", want: &types.KubernetesResource{Kind: types.KindKubeServiceAccount, Namespace: "default", Name: "foo", Verbs: []string{"watch"}}}, + {path: "/api/v1/namespaces/default/serviceaccounts/foo", want: &types.KubernetesResource{Kind: types.KindKubeServiceAccount, Namespace: "default", Name: "foo", Verbs: []string{"get"}, APIGroup: "core"}}, + {path: "/api/v1/watch/namespaces/default/serviceaccounts/foo", want: &types.KubernetesResource{Kind: types.KindKubeServiceAccount, Namespace: "default", Name: "foo", Verbs: []string{"watch"}, APIGroup: "core"}}, // PersistentVolumes {path: "/api/v1/persistentvolumes", want: nil}, {path: "/api/v1/namespaces/default/persistentvolumes", want: nil}, - {path: "/api/v1/namespaces/default/persistentvolumes/foo", want: &types.KubernetesResource{Kind: types.KindKubePersistentVolume, Namespace: "default", Name: "foo", Verbs: []string{"get"}}}, - {path: "/api/v1/watch/namespaces/default/persistentvolumes/foo", want: &types.KubernetesResource{Kind: types.KindKubePersistentVolume, Namespace: "default", Name: "foo", Verbs: []string{"watch"}}}, + {path: "/api/v1/namespaces/default/persistentvolumes/foo", want: &types.KubernetesResource{Kind: types.KindKubePersistentVolume, Namespace: "default", Name: "foo", Verbs: []string{"get"}, APIGroup: "core"}}, + {path: "/api/v1/watch/namespaces/default/persistentvolumes/foo", want: &types.KubernetesResource{Kind: types.KindKubePersistentVolume, Namespace: "default", Name: "foo", Verbs: []string{"watch"}, APIGroup: "core"}}, // PersistentVolumeClaims {path: "/api/v1/persistentvolumeclaims", want: nil}, {path: "/api/v1/namespaces/default/persistentvolumeclaims", want: nil}, - {path: "/api/v1/namespaces/default/persistentvolumeclaims/foo", want: &types.KubernetesResource{Kind: types.KindKubePersistentVolumeClaim, Namespace: "default", Name: "foo", Verbs: []string{"get"}}}, - {path: "/api/v1/watch/namespaces/default/persistentvolumeclaims/foo", want: &types.KubernetesResource{Kind: types.KindKubePersistentVolumeClaim, Namespace: "default", Name: "foo", Verbs: []string{"watch"}}}, + {path: "/api/v1/namespaces/default/persistentvolumeclaims/foo", want: &types.KubernetesResource{Kind: types.KindKubePersistentVolumeClaim, Namespace: "default", Name: "foo", Verbs: []string{"get"}, APIGroup: "core"}}, + {path: "/api/v1/watch/namespaces/default/persistentvolumeclaims/foo", want: &types.KubernetesResource{Kind: types.KindKubePersistentVolumeClaim, Namespace: "default", Name: "foo", Verbs: []string{"watch"}, APIGroup: "core"}}, // apis/apps // Deployments {path: "/apis/apps/v1/deployments", want: nil}, {path: "/apis/apps/v1/namespaces/default/deployments", want: nil}, - {path: "/apis/apps/v1/namespaces/default/deployments/foo", want: &types.KubernetesResource{Kind: types.KindKubeDeployment, Namespace: "default", Name: "foo", Verbs: []string{"get"}}}, - {path: "/apis/apps/v1/watch/namespaces/default/deployments/foo", want: &types.KubernetesResource{Kind: types.KindKubeDeployment, Namespace: "default", Name: "foo", Verbs: []string{"watch"}}}, - {path: "/apis/apps/v1/namespaces/default/deployments", body: bodyFunc("Deployment", "apps/v1"), want: &types.KubernetesResource{Kind: types.KindKubeDeployment, Namespace: "default", Name: "foo-create", Verbs: []string{"create"}}}, - {path: "/apis/apps/v1beta2/namespaces/default/deployments", body: bodyFunc("Deployment", "apps/v1beta2"), want: &types.KubernetesResource{Kind: types.KindKubeDeployment, Namespace: "default", Name: "foo-create", Verbs: []string{"create"}}}, - {path: "/apis/apps/v1/namespaces/default/deployments", body: bodyFuncWithoutGVK(), want: &types.KubernetesResource{Kind: types.KindKubeDeployment, Namespace: "default", Name: "foo-create", Verbs: []string{"create"}}}, + {path: "/apis/apps/v1/namespaces/default/deployments/foo", want: &types.KubernetesResource{Kind: types.KindKubeDeployment, Namespace: "default", Name: "foo", Verbs: []string{"get"}, APIGroup: "apps"}}, + {path: "/apis/apps/v1/watch/namespaces/default/deployments/foo", want: &types.KubernetesResource{Kind: types.KindKubeDeployment, Namespace: "default", Name: "foo", Verbs: []string{"watch"}, APIGroup: "apps"}}, + {path: "/apis/apps/v1/namespaces/default/deployments", body: bodyFunc("Deployment", "apps/v1"), want: &types.KubernetesResource{Kind: types.KindKubeDeployment, Namespace: "default", Name: "foo-create", Verbs: []string{"create"}, APIGroup: "apps"}}, + {path: "/apis/apps/v1beta2/namespaces/default/deployments", body: bodyFunc("Deployment", "apps/v1beta2"), want: &types.KubernetesResource{Kind: types.KindKubeDeployment, Namespace: "default", Name: "foo-create", Verbs: []string{"create"}, APIGroup: "apps"}}, + {path: "/apis/apps/v1/namespaces/default/deployments", body: bodyFuncWithoutGVK(), want: &types.KubernetesResource{Kind: types.KindKubeDeployment, Namespace: "default", Name: "foo-create", Verbs: []string{"create"}, APIGroup: "apps"}}, // Statefulsets {path: "/apis/apps/v1/statefulsets", want: nil}, {path: "/apis/apps/v1/namespaces/default/statefulsets", want: nil}, - {path: "/apis/apps/v1/namespaces/default/statefulsets/foo", want: &types.KubernetesResource{Kind: types.KindKubeStatefulset, Namespace: "default", Name: "foo", Verbs: []string{"get"}}}, - {path: "/apis/apps/v1/watch/namespaces/default/statefulsets/foo", want: &types.KubernetesResource{Kind: types.KindKubeStatefulset, Namespace: "default", Name: "foo", Verbs: []string{"watch"}}}, + {path: "/apis/apps/v1/namespaces/default/statefulsets/foo", want: &types.KubernetesResource{Kind: types.KindKubeStatefulset, Namespace: "default", Name: "foo", Verbs: []string{"get"}, APIGroup: "apps"}}, + {path: "/apis/apps/v1/watch/namespaces/default/statefulsets/foo", want: &types.KubernetesResource{Kind: types.KindKubeStatefulset, Namespace: "default", Name: "foo", Verbs: []string{"watch"}, APIGroup: "apps"}}, // Replicasets {path: "/apis/apps/v1/replicasets", want: nil}, {path: "/apis/apps/v1/namespaces/default/replicasets", want: nil}, - {path: "/apis/apps/v1/namespaces/default/replicasets/foo", want: &types.KubernetesResource{Kind: types.KindKubeReplicaSet, Namespace: "default", Name: "foo", Verbs: []string{"get"}}}, - {path: "/apis/apps/v1/watch/namespaces/default/replicasets/foo", want: &types.KubernetesResource{Kind: types.KindKubeReplicaSet, Namespace: "default", Name: "foo", Verbs: []string{"watch"}}}, + {path: "/apis/apps/v1/namespaces/default/replicasets/foo", want: &types.KubernetesResource{Kind: types.KindKubeReplicaSet, Namespace: "default", Name: "foo", Verbs: []string{"get"}, APIGroup: "apps"}}, + {path: "/apis/apps/v1/watch/namespaces/default/replicasets/foo", want: &types.KubernetesResource{Kind: types.KindKubeReplicaSet, Namespace: "default", Name: "foo", Verbs: []string{"watch"}, APIGroup: "apps"}}, // Daemonsets {path: "/apis/apps/v1/daemonsets", want: nil}, {path: "/apis/apps/v1/namespaces/default/daemonsets", want: nil}, - {path: "/apis/apps/v1/namespaces/default/daemonsets/foo", want: &types.KubernetesResource{Kind: types.KindKubeDaemonSet, Namespace: "default", Name: "foo", Verbs: []string{"get"}}}, - {path: "/apis/apps/v1/watch/namespaces/default/daemonsets/foo", want: &types.KubernetesResource{Kind: types.KindKubeDaemonSet, Namespace: "default", Name: "foo", Verbs: []string{"watch"}}}, + {path: "/apis/apps/v1/namespaces/default/daemonsets/foo", want: &types.KubernetesResource{Kind: types.KindKubeDaemonSet, Namespace: "default", Name: "foo", Verbs: []string{"get"}, APIGroup: "apps"}}, + {path: "/apis/apps/v1/watch/namespaces/default/daemonsets/foo", want: &types.KubernetesResource{Kind: types.KindKubeDaemonSet, Namespace: "default", Name: "foo", Verbs: []string{"watch"}, APIGroup: "apps"}}, // apis/batch // Job {path: "/apis/batch/v1/jobs", want: nil}, {path: "/apis/batch/v1/namespaces/default/jobs", want: nil}, - {path: "/apis/batch/v1/namespaces/default/jobs/foo", want: &types.KubernetesResource{Kind: types.KindKubeJob, Namespace: "default", Name: "foo", Verbs: []string{"get"}}}, - {path: "/apis/batch/v1/watch/namespaces/default/jobs/foo", want: &types.KubernetesResource{Kind: types.KindKubeJob, Namespace: "default", Name: "foo", Verbs: []string{"watch"}}}, + {path: "/apis/batch/v1/namespaces/default/jobs/foo", want: &types.KubernetesResource{Kind: types.KindKubeJob, Namespace: "default", Name: "foo", Verbs: []string{"get"}, APIGroup: "batch"}}, + {path: "/apis/batch/v1/watch/namespaces/default/jobs/foo", want: &types.KubernetesResource{Kind: types.KindKubeJob, Namespace: "default", Name: "foo", Verbs: []string{"watch"}, APIGroup: "batch"}}, // CronJob {path: "/apis/batch/v1/cronjobs", want: nil}, {path: "/apis/batch/v1/namespaces/default/cronjobs", want: nil}, - {path: "/apis/batch/v1/namespaces/default/cronjobs/foo", want: &types.KubernetesResource{Kind: types.KindKubeCronjob, Namespace: "default", Name: "foo", Verbs: []string{"get"}}}, - {path: "/apis/batch/v1/watch/namespaces/default/cronjobs/foo", want: &types.KubernetesResource{Kind: types.KindKubeCronjob, Namespace: "default", Name: "foo", Verbs: []string{"watch"}}}, + {path: "/apis/batch/v1/namespaces/default/cronjobs/foo", want: &types.KubernetesResource{Kind: types.KindKubeCronjob, Namespace: "default", Name: "foo", Verbs: []string{"get"}, APIGroup: "batch"}}, + {path: "/apis/batch/v1/watch/namespaces/default/cronjobs/foo", want: &types.KubernetesResource{Kind: types.KindKubeCronjob, Namespace: "default", Name: "foo", Verbs: []string{"watch"}, APIGroup: "batch"}}, // apis/certificates.k8s.io {path: "/apis/certificates.k8s.io/v1/certificatesigningrequests", want: nil}, - {path: "/apis/certificates.k8s.io/v1/certificatesigningrequests/foo", want: &types.KubernetesResource{Kind: types.KindKubeCertificateSigningRequest, Name: "foo", Verbs: []string{"get"}}}, + {path: "/apis/certificates.k8s.io/v1/certificatesigningrequests/foo", want: &types.KubernetesResource{Kind: types.KindKubeCertificateSigningRequest, Name: "foo", Verbs: []string{"get"}, APIGroup: "certificates.k8s.io"}}, // apis/networking.k8s.io {path: "/apis/networking.k8s.io/v1/ingresses", want: nil}, - {path: "/apis/networking.k8s.io/v1/ingresses/foo", want: &types.KubernetesResource{Kind: types.KindKubeIngress, Name: "foo", Verbs: []string{"get"}}}, + {path: "/apis/networking.k8s.io/v1/ingresses/foo", want: &types.KubernetesResource{Kind: types.KindKubeIngress, Name: "foo", Verbs: []string{"get"}, APIGroup: "networking.k8s.io"}}, } for _, tt := range tests { diff --git a/lib/kube/proxy/utils_test.go b/lib/kube/proxy/utils_test.go index ff648100fd0..b4922ef5774 100644 --- a/lib/kube/proxy/utils_test.go +++ b/lib/kube/proxy/utils_test.go @@ -465,7 +465,7 @@ func (c *TestContext) CreateUserWithTraitsAndRole(ctx context.Context, t *testin role.SetSessionRequirePolicies(roleSpec.SessionRequire) role.SetSessionJoinPolicies(roleSpec.SessionJoin) if roleSpec.SetupRoleFunc == nil { - role.SetKubeResources(types.Allow, []types.KubernetesResource{{Kind: types.KindKubePod, Name: types.Wildcard, Namespace: types.Wildcard, Verbs: []string{types.Wildcard}}}) + role.SetKubeResources(types.Allow, []types.KubernetesResource{{Kind: types.KindKubePod, Name: types.Wildcard, Namespace: types.Wildcard, Verbs: []string{types.Wildcard}, APIGroup: "core"}}) } else { roleSpec.SetupRoleFunc(role) } diff --git a/lib/services/access_checker.go b/lib/services/access_checker.go index 8c59769fccf..34f1a64c6b1 100644 --- a/lib/services/access_checker.go +++ b/lib/services/access_checker.go @@ -538,6 +538,8 @@ func (a *accessChecker) GetKubeResources(cluster types.KubeCluster) (allowed, de Kind: r.Kind, Namespace: namespace, Name: name, + // TODO(@creack): Add support for Groups in AccessRequests. + APIGroup: types.Wildcard, } // matchKubernetesResource checks if the Kubernetes Resource matches the tuple // (kind, namespace, kame) from the allowed/denied list and does not match the resource @@ -554,7 +556,7 @@ func (a *accessChecker) GetKubeResources(cluster types.KubeCluster) (allowed, de return rolesAllowed, rolesDenied } } - return + return allowed, denied } // matchKubernetesResource checks if the Kubernetes Resource does not match any diff --git a/lib/services/access_checker_test.go b/lib/services/access_checker_test.go index 27938e9a7f3..d4671f93045 100644 --- a/lib/services/access_checker_test.go +++ b/lib/services/access_checker_test.go @@ -45,6 +45,7 @@ func TestAccessCheckerKubeResources(t *testing.T) { Name: "dev", Namespace: "dev", Verbs: []string{types.Wildcard}, + APIGroup: types.Wildcard, }, }) rv.SetKubernetesLabels(types.Allow, kubeDevLabels) @@ -58,12 +59,14 @@ func TestAccessCheckerKubeResources(t *testing.T) { Name: "any1", Namespace: "any1", Verbs: []string{types.Wildcard}, + APIGroup: types.Wildcard, }, { Kind: types.KindKubePod, Name: "any1", Namespace: "any2", Verbs: []string{types.Wildcard}, + APIGroup: types.Wildcard, }, }) rv.SetKubernetesLabels(types.Allow, kubeAnyLabels) @@ -79,12 +82,14 @@ func TestAccessCheckerKubeResources(t *testing.T) { Name: "any1", Namespace: "any1", Verbs: []string{types.KubeVerbList}, + APIGroup: types.Wildcard, }, { Kind: types.KindKubePod, Name: "any1", Namespace: "any2", Verbs: []string{types.KubeVerbList}, + APIGroup: types.Wildcard, }, }) rv.SetKubernetesLabels(types.Allow, kubeAnyLabels) @@ -126,12 +131,14 @@ func TestAccessCheckerKubeResources(t *testing.T) { Name: "any1", Namespace: "any1", Verbs: []string{types.Wildcard}, + APIGroup: types.Wildcard, }, { Kind: types.KindKubePod, Name: "any1", Namespace: "any2", Verbs: []string{types.Wildcard}, + APIGroup: types.Wildcard, }, }, wantDenied: emptySet, @@ -158,18 +165,21 @@ func TestAccessCheckerKubeResources(t *testing.T) { Name: "any1", Namespace: "any1", Verbs: []string{types.Wildcard}, + APIGroup: types.Wildcard, }, { Kind: types.KindKubePod, Name: "any1", Namespace: "any2", Verbs: []string{types.Wildcard}, + APIGroup: types.Wildcard, }, { Kind: types.KindKubePod, Name: "dev", Namespace: "dev", Verbs: []string{types.Wildcard}, + APIGroup: types.Wildcard, }, }, wantDenied: emptySet, @@ -221,6 +231,7 @@ func TestAccessCheckerKubeResources(t *testing.T) { Name: "dev", Namespace: "dev", Verbs: []string{types.Wildcard}, + APIGroup: types.Wildcard, }, }, wantDenied: emptySet, @@ -302,18 +313,21 @@ func TestAccessCheckerKubeResources(t *testing.T) { Name: "any1", Namespace: "any1", Verbs: []string{types.Wildcard}, + APIGroup: types.Wildcard, }, { Kind: types.KindKubePod, Name: "any1", Namespace: "any2", Verbs: []string{types.Wildcard}, + APIGroup: types.Wildcard, }, { Kind: types.KindKubePod, Name: "dev", Namespace: "dev", Verbs: []string{types.Wildcard}, + APIGroup: types.Wildcard, }, }, wantDenied: emptySet, @@ -353,6 +367,7 @@ func TestAccessCheckerKubeResources(t *testing.T) { Name: "dev", Namespace: "dev", Verbs: []string{types.Wildcard}, + APIGroup: types.Wildcard, }, }, wantDenied: emptySet, @@ -392,12 +407,14 @@ func TestAccessCheckerKubeResources(t *testing.T) { Name: "any1", Namespace: "any1", Verbs: []string{types.Wildcard}, + APIGroup: types.Wildcard, }, { Kind: types.KindKubePod, Name: "any1", Namespace: "any2", Verbs: []string{types.Wildcard}, + APIGroup: types.Wildcard, }, }, wantDenied: emptySet, @@ -451,12 +468,14 @@ func TestAccessCheckerKubeResources(t *testing.T) { Name: "any1", Namespace: "any1", Verbs: []string{types.KubeVerbList}, + APIGroup: types.Wildcard, }, { Kind: types.KindKubePod, Name: "any1", Namespace: "any2", Verbs: []string{types.KubeVerbList}, + APIGroup: types.Wildcard, }, }, wantDenied: emptySet, @@ -483,12 +502,14 @@ func TestAccessCheckerKubeResources(t *testing.T) { Name: "any1", Namespace: "any1", Verbs: []string{types.KubeVerbList}, + APIGroup: types.Wildcard, }, { Kind: types.KindKubePod, Name: "any1", Namespace: "any2", Verbs: []string{types.KubeVerbList}, + APIGroup: types.Wildcard, }, }, wantDenied: emptySet, diff --git a/lib/services/access_request_test.go b/lib/services/access_request_test.go index 1b6b47fadd8..f8d8d6c7dfb 100644 --- a/lib/services/access_request_test.go +++ b/lib/services/access_request_test.go @@ -2928,7 +2928,7 @@ func TestValidate_WithAllowRequestKubernetesResources(t *testing.T) { "*": {"*"}, }, KubernetesResources: []types.KubernetesResource{ - {Kind: "*", Namespace: "*", Name: "*", Verbs: []string{"*"}}, + {Kind: "*", Namespace: "*", Name: "*", Verbs: []string{"*"}, APIGroup: "*"}, }, }, }, @@ -2941,7 +2941,7 @@ func TestValidate_WithAllowRequestKubernetesResources(t *testing.T) { "*": {"*"}, }, KubernetesResources: []types.KubernetesResource{ - {Kind: types.KindNamespace, Namespace: "*", Name: "*", Verbs: []string{"*"}}, + {Kind: types.KindNamespace, Namespace: "*", Name: "*", Verbs: []string{"*"}, APIGroup: "*"}, }, }, }, @@ -2951,7 +2951,7 @@ func TestValidate_WithAllowRequestKubernetesResources(t *testing.T) { "*": {"*"}, }, KubernetesResources: []types.KubernetesResource{ - {Kind: types.KindKubePod, Namespace: "*", Name: "*", Verbs: []string{"*"}}, + {Kind: types.KindKubePod, Namespace: "*", Name: "*", Verbs: []string{"*"}, APIGroup: "*"}, }, }, }, @@ -2961,7 +2961,7 @@ func TestValidate_WithAllowRequestKubernetesResources(t *testing.T) { "*": {"*"}, }, KubernetesResources: []types.KubernetesResource{ - {Kind: types.KindKubeDeployment, Namespace: "*", Name: "*", Verbs: []string{"*"}}, + {Kind: types.KindKubeDeployment, Namespace: "*", Name: "*", Verbs: []string{"*"}, APIGroup: "*"}, }, }, }, diff --git a/lib/services/presets.go b/lib/services/presets.go index ed45e405bbe..902dc297143 100644 --- a/lib/services/presets.go +++ b/lib/services/presets.go @@ -47,7 +47,7 @@ func NewSystemAutomaticAccessApproverRole() types.Role { } role := &types.RoleV6{ Kind: types.KindRole, - Version: types.V8, + Version: types.V7, Metadata: types.Metadata{ Name: teleport.SystemAutomaticAccessApprovalRoleName, Namespace: apidefaults.Namespace, @@ -113,7 +113,7 @@ func NewPresetEditorRole() types.Role { // YAML. role := &types.RoleV6{ Kind: types.KindRole, - Version: types.V8, + Version: types.V7, Metadata: types.Metadata{ Name: teleport.PresetEditorRoleName, Namespace: apidefaults.Namespace, @@ -234,7 +234,7 @@ func NewPresetAccessRole() types.Role { // YAML. role := &types.RoleV6{ Kind: types.KindRole, - Version: types.V8, + Version: types.V7, Metadata: types.Metadata{ Name: teleport.PresetAccessRoleName, Namespace: apidefaults.Namespace, @@ -284,6 +284,7 @@ func NewPresetAccessRole() types.Role { Namespace: types.Wildcard, Name: types.Wildcard, Verbs: []string{types.Wildcard}, + APIGroup: "", }, }, GitHubPermissions: []types.GitHubPermission{{ @@ -326,7 +327,7 @@ func NewPresetAuditorRole() types.Role { // YAML. role := &types.RoleV6{ Kind: types.KindRole, - Version: types.V8, + Version: types.V7, Metadata: types.Metadata{ Name: teleport.PresetAuditorRoleName, Namespace: apidefaults.Namespace, @@ -371,7 +372,7 @@ func NewPresetReviewerRole() types.Role { role := &types.RoleV6{ Kind: types.KindRole, - Version: types.V8, + Version: types.V7, Metadata: types.Metadata{ Name: teleport.PresetReviewerRoleName, Namespace: apidefaults.Namespace, @@ -398,7 +399,7 @@ func NewPresetRequesterRole() types.Role { role := &types.RoleV6{ Kind: types.KindRole, - Version: types.V8, + Version: types.V7, Metadata: types.Metadata{ Name: teleport.PresetRequesterRoleName, Namespace: apidefaults.Namespace, @@ -425,7 +426,7 @@ func NewPresetGroupAccessRole() types.Role { role := &types.RoleV6{ Kind: types.KindRole, - Version: types.V8, + Version: types.V7, Metadata: types.Metadata{ Name: teleport.PresetGroupAccessRoleName, Namespace: apidefaults.Namespace, @@ -459,7 +460,7 @@ func NewPresetDeviceAdminRole() types.Role { return &types.RoleV6{ Kind: types.KindRole, - Version: types.V8, + Version: types.V7, Metadata: types.Metadata{ Name: teleport.PresetDeviceAdminRoleName, Namespace: apidefaults.Namespace, @@ -488,7 +489,7 @@ func NewPresetDeviceEnrollRole() types.Role { return &types.RoleV6{ Kind: types.KindRole, - Version: types.V8, + Version: types.V7, Metadata: types.Metadata{ Name: teleport.PresetDeviceEnrollRoleName, Namespace: apidefaults.Namespace, @@ -518,7 +519,7 @@ func NewPresetRequireTrustedDeviceRole() types.Role { return &types.RoleV6{ Kind: types.KindRole, - Version: types.V8, + Version: types.V7, Metadata: types.Metadata{ Name: teleport.PresetRequireTrustedDeviceRoleName, Namespace: apidefaults.Namespace, @@ -566,7 +567,7 @@ func NewPresetRequireTrustedDeviceRole() types.Role { func NewPresetWildcardWorkloadIdentityIssuerRole() types.Role { role := &types.RoleV6{ Kind: types.KindRole, - Version: types.V8, + Version: types.V7, Metadata: types.Metadata{ Name: teleport.PresetWildcardWorkloadIdentityIssuerRoleName, Namespace: apidefaults.Namespace, @@ -599,7 +600,7 @@ func NewSystemOktaAccessRole() types.Role { role := &types.RoleV6{ Kind: types.KindRole, - Version: types.V8, + Version: types.V7, Metadata: types.Metadata{ Name: teleport.SystemOktaAccessRoleName, Namespace: apidefaults.Namespace, @@ -635,7 +636,7 @@ func NewSystemOktaRequesterRole() types.Role { role := &types.RoleV6{ Kind: types.KindRole, - Version: types.V8, + Version: types.V7, Metadata: types.Metadata{ Name: teleport.SystemOktaRequesterRoleName, Namespace: apidefaults.Namespace, @@ -662,7 +663,7 @@ func NewSystemIdentityCenterAccessRole() types.Role { } return &types.RoleV6{ Kind: types.KindRole, - Version: types.V8, + Version: types.V7, Metadata: types.Metadata{ Name: teleport.SystemIdentityCenterAccessRoleName, Namespace: apidefaults.Namespace, @@ -689,7 +690,7 @@ func NewSystemIdentityCenterAccessRole() types.Role { func NewPresetTerraformProviderRole() types.Role { role := &types.RoleV6{ Kind: types.KindRole, - Version: types.V8, + Version: types.V7, Metadata: types.Metadata{ Name: teleport.PresetTerraformProviderRoleName, Namespace: apidefaults.Namespace, diff --git a/lib/services/role.go b/lib/services/role.go index f9b11f6359f..f74b0c20ff5 100644 --- a/lib/services/role.go +++ b/lib/services/role.go @@ -540,6 +540,7 @@ func ApplyTraits(r types.Role, traits map[string][]string) (types.Role, error) { Namespace: namespace, Name: name, Verbs: verbs, + APIGroup: rec.APIGroup, }) } } diff --git a/lib/services/role_test.go b/lib/services/role_test.go index d9746b02498..c53ca9a851a 100644 --- a/lib/services/role_test.go +++ b/lib/services/role_test.go @@ -1077,6 +1077,7 @@ func TestValidateRole(t *testing.T) { Namespace: "{{external.namespace", Name: "{{email.localz(external.email)}}", Verbs: []string{"{{external.verbs"}, + APIGroup: types.Wildcard, }, }, }, @@ -1087,6 +1088,7 @@ func TestValidateRole(t *testing.T) { Namespace: "{{external.namespace", Name: "{{email.localz(external.email)}}", Verbs: []string{"{{external.verbs"}, + APIGroup: types.Wildcard, }, }, }, @@ -9289,11 +9291,13 @@ func TestKubeResourcesMatcher(t *testing.T) { Kind: types.KindKubePod, Namespace: "dev", Name: types.Wildcard, + APIGroup: types.Wildcard, }, { Kind: types.KindKubePod, Namespace: "default", Name: "nginx-*", + APIGroup: types.Wildcard, }, }, }, @@ -9303,6 +9307,7 @@ func TestKubeResourcesMatcher(t *testing.T) { Kind: types.KindKubePod, Namespace: "default", Name: "restricted", + APIGroup: types.Wildcard, }, }, }, @@ -9318,6 +9323,7 @@ func TestKubeResourcesMatcher(t *testing.T) { Kind: types.KindKubePod, Namespace: "prod", Name: "pod", + APIGroup: types.Wildcard, }, }, }, @@ -9333,6 +9339,7 @@ func TestKubeResourcesMatcher(t *testing.T) { Kind: types.KindKubePod, Namespace: `^[($`, Name: `^[($`, + APIGroup: types.Wildcard, }, }, }, diff --git a/lib/utils/replace_test.go b/lib/utils/replace_test.go index 7e8b3e2f156..c7fb8440e78 100644 --- a/lib/utils/replace_test.go +++ b/lib/utils/replace_test.go @@ -608,12 +608,14 @@ func TestKubeResourceMatchesRegex(t *testing.T) { Namespace: "default", Name: "name", Verbs: []string{types.KubeVerbGet}, + APIGroup: "stable.example.com", }, resources: []types.KubernetesResource{ { - Kind: types.KindKubeNamespace, - Name: "default", - Verbs: []string{types.KubeVerbGet}, + Kind: types.KindKubeNamespace, + Name: "default", + Verbs: []string{types.KubeVerbGet}, + APIGroup: "*", }, }, assert: require.NoError, @@ -627,12 +629,14 @@ func TestKubeResourceMatchesRegex(t *testing.T) { Namespace: "default", Name: "name", Verbs: []string{types.KubeVerbUpdate}, + APIGroup: "stable.example.com", }, resources: []types.KubernetesResource{ { - Kind: types.KindKubeNamespace, - Name: "default", - Verbs: []string{types.KubeVerbGet}, + Kind: types.KindKubeNamespace, + Name: "default", + Verbs: []string{types.KubeVerbGet}, + APIGroup: "*", }, }, assert: require.NoError, diff --git a/lib/web/apiserver_test.go b/lib/web/apiserver_test.go index a9bd79b037f..fd55adb5d8f 100644 --- a/lib/web/apiserver_test.go +++ b/lib/web/apiserver_test.go @@ -4361,10 +4361,12 @@ func TestClusterKubeResourcesGet(t *testing.T) { Kind: types.KindKubePod, Namespace: types.Wildcard, Name: types.Wildcard, + APIGroup: types.Wildcard, }, { - Kind: types.KindKubeNamespace, - Name: types.Wildcard, + Kind: types.KindKubeNamespace, + Name: types.Wildcard, + APIGroup: types.Wildcard, }, }, }, @@ -4455,7 +4457,6 @@ func TestClusterKubeResourcesGet(t *testing.T) { pack := proxy.authPack(t, user, roleWithFullAccess(user)) for _, tc := range tt { - tc := tc t.Run(tc.name, func(t *testing.T) { endpoint := pack.clt.Endpoint("webapi", "sites", env.server.ClusterName(), "kubernetes", "resources") params := url.Values{} @@ -6886,6 +6887,7 @@ func TestDiagnoseKubeConnection(t *testing.T) { Namespace: types.Wildcard, Name: types.Wildcard, Verbs: []string{types.Wildcard}, + APIGroup: types.Wildcard, }, }, }, @@ -6911,6 +6913,7 @@ func TestDiagnoseKubeConnection(t *testing.T) { Namespace: types.Wildcard, Name: types.Wildcard, Verbs: []string{types.Wildcard}, + APIGroup: types.Wildcard, }, }, }, diff --git a/lib/web/resources_test.go b/lib/web/resources_test.go index f384c715b68..27424f83171 100644 --- a/lib/web/resources_test.go +++ b/lib/web/resources_test.go @@ -218,7 +218,8 @@ spec: kubernetes_labels: '*': '*' kubernetes_resources: - - kind: pod + - api_group: '*' + kind: pod name: '*' namespace: '*' logins: @@ -253,7 +254,7 @@ version: v8 KubernetesLabels: types.Labels{types.Wildcard: []string{types.Wildcard}}, KubernetesResources: []types.KubernetesResource{ { - Kind: types.KindKubePod, Name: types.Wildcard, Namespace: types.Wildcard, + Kind: types.KindKubePod, Name: types.Wildcard, Namespace: types.Wildcard, APIGroup: types.Wildcard, }, }, }, diff --git a/tool/tsh/common/tsh_test.go b/tool/tsh/common/tsh_test.go index 5ca01a7c4ad..ebfe195a01e 100644 --- a/tool/tsh/common/tsh_test.go +++ b/tool/tsh/common/tsh_test.go @@ -2612,7 +2612,7 @@ func TestKubeCredentialsLock(t *testing.T) { KubeUsers: []string{alice.GetName()}, KubernetesResources: []types.KubernetesResource{ { - Kind: types.KindKubePod, Name: types.Wildcard, Namespace: types.Wildcard, + Kind: types.KindKubePod, Name: types.Wildcard, Namespace: types.Wildcard, APIGroup: types.Wildcard, }, }, },