Skip to content

Commit aec49be

Browse files
committed
refactor(kubevirt): Enforce access control in toolset API
Remove direct RESTConfig() exposure from Kubernetes type and add RESTConfigForGVK method that validates resource access through AccessControlRESTMapper before allowing operations. Update all kubevirt tools to use GVK instead of GVR with ResourcesList and use controlled RESTConfigForGVK method instead of creating their own uncontrolled dynamic clients. This prevents toolsets from bypassing the denied resources configuration and ensures all API access goes through the access control layer. Assisted-By: Claude <[email protected]> Signed-off-by: Lee Yarwood <[email protected]>
1 parent 860de99 commit aec49be

File tree

7 files changed

+173
-178
lines changed

7 files changed

+173
-178
lines changed

pkg/kubernetes/kubernetes.go

Lines changed: 0 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ package kubernetes
22

33
import (
44
"k8s.io/apimachinery/pkg/runtime"
5-
"k8s.io/client-go/rest"
65

76
"github.com/containers/kubernetes-mcp-server/pkg/helm"
87
"k8s.io/client-go/kubernetes/scheme"
@@ -31,14 +30,6 @@ func (k *Kubernetes) AccessControlClientset() *AccessControlClientset {
3130
return k.manager.accessControlClientSet
3231
}
3332

34-
// RESTConfig returns the Kubernetes REST configuration
35-
func (k *Kubernetes) RESTConfig() *rest.Config {
36-
if k.manager == nil {
37-
return nil
38-
}
39-
return k.manager.cfg
40-
}
41-
4233
var Scheme = scheme.Scheme
4334
var ParameterCodec = runtime.NewParameterCodec(Scheme)
4435

pkg/kubernetes/resources.go

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import (
1414
metav1beta1 "k8s.io/apimachinery/pkg/apis/meta/v1beta1"
1515
"k8s.io/apimachinery/pkg/runtime/schema"
1616
"k8s.io/apimachinery/pkg/util/yaml"
17+
"k8s.io/client-go/rest"
1718
)
1819

1920
const (
@@ -45,7 +46,33 @@ func (k *Kubernetes) ResourcesList(ctx context.Context, gvk *schema.GroupVersion
4546
return k.manager.dynamicClient.Resource(*gvr).Namespace(namespace).List(ctx, options.ListOptions)
4647
}
4748

49+
// RESTConfigForGVK returns a copy of the REST config for creating custom clients.
50+
// Access control is enforced by validating the provided GVK.
51+
// This is useful for accessing subresources that aren't supported by the dynamic client.
52+
// The caller is responsible for configuring GroupVersion, APIPath, and NegotiatedSerializer
53+
// as needed for their specific use case.
54+
func (k *Kubernetes) RESTConfigForGVK(gvk *schema.GroupVersionKind) (*rest.Config, error) {
55+
if k.manager == nil {
56+
return nil, fmt.Errorf("kubernetes manager not initialized")
57+
}
58+
59+
// Validate access control for the resource kind
60+
if gvk != nil {
61+
_, err := k.resourceFor(gvk)
62+
if err != nil {
63+
return nil, err // Access denied or resource not found
64+
}
65+
}
66+
67+
// Return a copy of the REST config to avoid modifying the original
68+
return rest.CopyConfig(k.manager.cfg), nil
69+
}
70+
4871
func (k *Kubernetes) ResourcesGet(ctx context.Context, gvk *schema.GroupVersionKind, namespace, name string) (*unstructured.Unstructured, error) {
72+
if k.manager == nil {
73+
return nil, fmt.Errorf("kubernetes manager not initialized")
74+
}
75+
4976
gvr, err := k.resourceFor(gvk)
5077
if err != nil {
5178
return nil, err
@@ -73,6 +100,10 @@ func (k *Kubernetes) ResourcesCreateOrUpdate(ctx context.Context, resource strin
73100
}
74101

75102
func (k *Kubernetes) ResourcesDelete(ctx context.Context, gvk *schema.GroupVersionKind, namespace, name string) error {
103+
if k.manager == nil {
104+
return fmt.Errorf("kubernetes manager not initialized")
105+
}
106+
76107
gvr, err := k.resourceFor(gvk)
77108
if err != nil {
78109
return err

pkg/toolsets/kubevirt/vm/create/tool.go

Lines changed: 80 additions & 83 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,11 @@ import (
77
"text/template"
88

99
"github.com/containers/kubernetes-mcp-server/pkg/api"
10+
internalk8s "github.com/containers/kubernetes-mcp-server/pkg/kubernetes"
1011
"github.com/containers/kubernetes-mcp-server/pkg/output"
1112
"github.com/google/jsonschema-go/jsonschema"
12-
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
1313
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
1414
"k8s.io/apimachinery/pkg/runtime/schema"
15-
"k8s.io/client-go/dynamic"
1615
"k8s.io/utils/ptr"
1716
)
1817

@@ -464,27 +463,15 @@ func getDefaultContainerDisks() []DataSourceInfo {
464463

465464
// searchDataSources searches for DataSource resources in the cluster
466465
func searchDataSources(params api.ToolHandlerParams, query string) ([]DataSourceInfo, error) {
467-
// Get dynamic client for querying DataSources
468-
restConfig := params.RESTConfig()
469-
if restConfig == nil {
470-
return nil, fmt.Errorf("REST config is nil")
471-
}
472-
473-
dynamicClient, err := dynamic.NewForConfig(restConfig)
474-
if err != nil {
475-
// Return just the built-in containerdisk images
476-
return getDefaultContainerDisks(), nil
477-
}
478-
479-
// DataSource GVR for CDI
480-
dataSourceGVR := schema.GroupVersionResource{
481-
Group: "cdi.kubevirt.io",
482-
Version: "v1beta1",
483-
Resource: "datasources",
466+
// DataSource GVK for CDI
467+
dataSourceGVK := schema.GroupVersionKind{
468+
Group: "cdi.kubevirt.io",
469+
Version: "v1beta1",
470+
Kind: "DataSource",
484471
}
485472

486473
// Collect DataSources from well-known namespaces and all namespaces
487-
results := collectDataSources(params, dynamicClient, dataSourceGVR)
474+
results := collectDataSources(params, dataSourceGVK)
488475

489476
// Add common containerdisk images
490477
results = append(results, getDefaultContainerDisks()...)
@@ -504,7 +491,7 @@ func searchDataSources(params api.ToolHandlerParams, query string) ([]DataSource
504491
}
505492

506493
// collectDataSources collects DataSources from well-known namespaces and all namespaces
507-
func collectDataSources(params api.ToolHandlerParams, dynamicClient dynamic.Interface, gvr schema.GroupVersionResource) []DataSourceInfo {
494+
func collectDataSources(params api.ToolHandlerParams, gvk schema.GroupVersionKind) []DataSourceInfo {
508495
var results []DataSourceInfo
509496

510497
// Try to list DataSources from well-known namespaces first
@@ -514,14 +501,14 @@ func collectDataSources(params api.ToolHandlerParams, dynamicClient dynamic.Inte
514501
}
515502

516503
for _, ns := range wellKnownNamespaces {
517-
dsInfos, err := listDataSourcesFromNamespace(params, dynamicClient, gvr, ns)
504+
dsInfos, err := listDataSourcesFromNamespace(params, gvk, ns)
518505
if err == nil {
519506
results = append(results, dsInfos...)
520507
}
521508
}
522509

523510
// List DataSources from all namespaces
524-
list, err := dynamicClient.Resource(gvr).List(params.Context, metav1.ListOptions{})
511+
listResult, err := params.ResourcesList(params.Context, &gvk, "", internalk8s.ResourceListOptions{})
525512
if err != nil {
526513
// If we found DataSources from well-known namespaces but couldn't list all, return what we have
527514
if len(results) > 0 {
@@ -537,6 +524,22 @@ func collectDataSources(params api.ToolHandlerParams, dynamicClient dynamic.Inte
537524
}
538525
}
539526

527+
// Convert to UnstructuredList
528+
list, ok := listResult.(*unstructured.UnstructuredList)
529+
if !ok {
530+
// If we found DataSources from well-known namespaces but couldn't convert the result, return what we have
531+
if len(results) > 0 {
532+
return results
533+
}
534+
return []DataSourceInfo{
535+
{
536+
Name: "No DataSources found",
537+
Namespace: "",
538+
Source: "CDI may not be installed or DataSources are not available in this cluster",
539+
},
540+
}
541+
}
542+
540543
// Deduplicate and add DataSources from all namespaces
541544
results = deduplicateAndMergeDataSources(results, list.Items)
542545

@@ -587,13 +590,19 @@ func deduplicateAndMergeDataSources(existing []DataSourceInfo, items []unstructu
587590
}
588591

589592
// listDataSourcesFromNamespace lists DataSources from a specific namespace
590-
func listDataSourcesFromNamespace(params api.ToolHandlerParams, dynamicClient dynamic.Interface, gvr schema.GroupVersionResource, namespace string) ([]DataSourceInfo, error) {
593+
func listDataSourcesFromNamespace(params api.ToolHandlerParams, gvk schema.GroupVersionKind, namespace string) ([]DataSourceInfo, error) {
591594
var results []DataSourceInfo
592-
list, err := dynamicClient.Resource(gvr).Namespace(namespace).List(params.Context, metav1.ListOptions{})
595+
listResult, err := params.ResourcesList(params.Context, &gvk, namespace, internalk8s.ResourceListOptions{})
593596
if err != nil {
594597
return nil, err
595598
}
596599

600+
// Convert to UnstructuredList
601+
list, ok := listResult.(*unstructured.UnstructuredList)
602+
if !ok {
603+
return nil, fmt.Errorf("unexpected result type: %T", listResult)
604+
}
605+
597606
for _, item := range list.Items {
598607
name := item.GetName()
599608
ns := item.GetNamespace()
@@ -629,47 +638,41 @@ func searchPreferences(params api.ToolHandlerParams, namespace string) []Prefere
629638
return []PreferenceInfo{}
630639
}
631640

632-
restConfig := params.RESTConfig()
633-
if restConfig == nil {
634-
return []PreferenceInfo{}
635-
}
636-
637-
dynamicClient, err := dynamic.NewForConfig(restConfig)
638-
if err != nil {
639-
return []PreferenceInfo{}
640-
}
641-
642641
var results []PreferenceInfo
643642

644643
// Search for cluster-wide VirtualMachineClusterPreferences
645-
clusterPreferenceGVR := schema.GroupVersionResource{
646-
Group: "instancetype.kubevirt.io",
647-
Version: "v1beta1",
648-
Resource: "virtualmachineclusterpreferences",
644+
clusterPreferenceGVK := schema.GroupVersionKind{
645+
Group: "instancetype.kubevirt.io",
646+
Version: "v1beta1",
647+
Kind: "VirtualMachineClusterPreference",
649648
}
650649

651-
clusterList, err := dynamicClient.Resource(clusterPreferenceGVR).List(params.Context, metav1.ListOptions{})
650+
clusterListResult, err := params.ResourcesList(params.Context, &clusterPreferenceGVK, "", internalk8s.ResourceListOptions{})
652651
if err == nil {
653-
for _, item := range clusterList.Items {
654-
results = append(results, PreferenceInfo{
655-
Name: item.GetName(),
656-
})
652+
if clusterList, ok := clusterListResult.(*unstructured.UnstructuredList); ok {
653+
for _, item := range clusterList.Items {
654+
results = append(results, PreferenceInfo{
655+
Name: item.GetName(),
656+
})
657+
}
657658
}
658659
}
659660

660661
// Search for namespaced VirtualMachinePreferences
661-
namespacedPreferenceGVR := schema.GroupVersionResource{
662-
Group: "instancetype.kubevirt.io",
663-
Version: "v1beta1",
664-
Resource: "virtualmachinepreferences",
662+
namespacedPreferenceGVK := schema.GroupVersionKind{
663+
Group: "instancetype.kubevirt.io",
664+
Version: "v1beta1",
665+
Kind: "VirtualMachinePreference",
665666
}
666667

667-
namespacedList, err := dynamicClient.Resource(namespacedPreferenceGVR).Namespace(namespace).List(params.Context, metav1.ListOptions{})
668+
namespacedListResult, err := params.ResourcesList(params.Context, &namespacedPreferenceGVK, namespace, internalk8s.ResourceListOptions{})
668669
if err == nil {
669-
for _, item := range namespacedList.Items {
670-
results = append(results, PreferenceInfo{
671-
Name: item.GetName(),
672-
})
670+
if namespacedList, ok := namespacedListResult.(*unstructured.UnstructuredList); ok {
671+
for _, item := range namespacedList.Items {
672+
results = append(results, PreferenceInfo{
673+
Name: item.GetName(),
674+
})
675+
}
673676
}
674677
}
675678

@@ -683,49 +686,43 @@ func searchInstancetypes(params api.ToolHandlerParams, namespace string) []Insta
683686
return []InstancetypeInfo{}
684687
}
685688

686-
restConfig := params.RESTConfig()
687-
if restConfig == nil {
688-
return []InstancetypeInfo{}
689-
}
690-
691-
dynamicClient, err := dynamic.NewForConfig(restConfig)
692-
if err != nil {
693-
return []InstancetypeInfo{}
694-
}
695-
696689
var results []InstancetypeInfo
697690

698691
// Search for cluster-wide VirtualMachineClusterInstancetypes
699-
clusterInstancetypeGVR := schema.GroupVersionResource{
700-
Group: "instancetype.kubevirt.io",
701-
Version: "v1beta1",
702-
Resource: "virtualmachineclusterinstancetypes",
692+
clusterInstancetypeGVK := schema.GroupVersionKind{
693+
Group: "instancetype.kubevirt.io",
694+
Version: "v1beta1",
695+
Kind: "VirtualMachineClusterInstancetype",
703696
}
704697

705-
clusterList, err := dynamicClient.Resource(clusterInstancetypeGVR).List(params.Context, metav1.ListOptions{})
698+
clusterListResult, err := params.ResourcesList(params.Context, &clusterInstancetypeGVK, "", internalk8s.ResourceListOptions{})
706699
if err == nil {
707-
for _, item := range clusterList.Items {
708-
results = append(results, InstancetypeInfo{
709-
Name: item.GetName(),
710-
Labels: item.GetLabels(),
711-
})
700+
if clusterList, ok := clusterListResult.(*unstructured.UnstructuredList); ok {
701+
for _, item := range clusterList.Items {
702+
results = append(results, InstancetypeInfo{
703+
Name: item.GetName(),
704+
Labels: item.GetLabels(),
705+
})
706+
}
712707
}
713708
}
714709

715710
// Search for namespaced VirtualMachineInstancetypes
716-
namespacedInstancetypeGVR := schema.GroupVersionResource{
717-
Group: "instancetype.kubevirt.io",
718-
Version: "v1beta1",
719-
Resource: "virtualmachineinstancetypes",
711+
namespacedInstancetypeGVK := schema.GroupVersionKind{
712+
Group: "instancetype.kubevirt.io",
713+
Version: "v1beta1",
714+
Kind: "VirtualMachineInstancetype",
720715
}
721716

722-
namespacedList, err := dynamicClient.Resource(namespacedInstancetypeGVR).Namespace(namespace).List(params.Context, metav1.ListOptions{})
717+
namespacedListResult, err := params.ResourcesList(params.Context, &namespacedInstancetypeGVK, namespace, internalk8s.ResourceListOptions{})
723718
if err == nil {
724-
for _, item := range namespacedList.Items {
725-
results = append(results, InstancetypeInfo{
726-
Name: item.GetName(),
727-
Labels: item.GetLabels(),
728-
})
719+
if namespacedList, ok := namespacedListResult.(*unstructured.UnstructuredList); ok {
720+
for _, item := range namespacedList.Items {
721+
results = append(results, InstancetypeInfo{
722+
Name: item.GetName(),
723+
Labels: item.GetLabels(),
724+
})
725+
}
729726
}
730727
}
731728

pkg/toolsets/kubevirt/vm/delete/tool.go

Lines changed: 7 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,7 @@ import (
55

66
"github.com/containers/kubernetes-mcp-server/pkg/api"
77
"github.com/google/jsonschema-go/jsonschema"
8-
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
98
"k8s.io/apimachinery/pkg/runtime/schema"
10-
"k8s.io/client-go/dynamic"
119
"k8s.io/utils/ptr"
1210
)
1311

@@ -56,30 +54,15 @@ func deleteVM(params api.ToolHandlerParams) (*api.ToolCallResult, error) {
5654
return api.NewToolCallResult("", err), nil
5755
}
5856

59-
// Get dynamic client
60-
restConfig := params.RESTConfig()
61-
if restConfig == nil {
62-
return api.NewToolCallResult("", fmt.Errorf("failed to get REST config")), nil
57+
// Define the VirtualMachine GVK
58+
gvk := schema.GroupVersionKind{
59+
Group: "kubevirt.io",
60+
Version: "v1",
61+
Kind: "VirtualMachine",
6362
}
6463

65-
dynamicClient, err := dynamic.NewForConfig(restConfig)
66-
if err != nil {
67-
return api.NewToolCallResult("", fmt.Errorf("failed to create dynamic client: %w", err)), nil
68-
}
69-
70-
// Define the VirtualMachine GVR
71-
gvr := schema.GroupVersionResource{
72-
Group: "kubevirt.io",
73-
Version: "v1",
74-
Resource: "virtualmachines",
75-
}
76-
77-
// Delete the VM
78-
err = dynamicClient.Resource(gvr).Namespace(namespace).Delete(
79-
params.Context,
80-
name,
81-
metav1.DeleteOptions{},
82-
)
64+
// Delete the VM using the access-controlled method
65+
err = params.ResourcesDelete(params.Context, &gvk, namespace, name)
8366
if err != nil {
8467
return api.NewToolCallResult("", fmt.Errorf("failed to delete VirtualMachine: %w", err)), nil
8568
}

0 commit comments

Comments
 (0)