diff --git a/pkg/kubernetes/accesscontrol_restclient.go b/pkg/kubernetes/accesscontrol_restclient.go new file mode 100644 index 00000000..79a2390d --- /dev/null +++ b/pkg/kubernetes/accesscontrol_restclient.go @@ -0,0 +1,61 @@ +package kubernetes + +import ( + "fmt" + "net/http" + "strings" + + "k8s.io/apimachinery/pkg/runtime/schema" +) + +type AccessControlRoundTripper struct { + delegate http.RoundTripper + accessControlRESTMapper *AccessControlRESTMapper +} + +func (rt *AccessControlRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) { + gvr, err := parseURLToGVR(req.URL.Path) + if err != nil { + return nil, fmt.Errorf("failed to make request: AccessControlRoundTripper failed to parse url: %w", err) + } + + _, err = rt.accessControlRESTMapper.KindFor(gvr) + if err != nil { + return nil, fmt.Errorf("not allowed to access resource: %v", gvr) + } + + return rt.delegate.RoundTrip(req) +} + +func parseURLToGVR(path string) (schema.GroupVersionResource, error) { + parts := strings.Split(strings.Trim(path, "/"), "/") + + if len(parts) < 3 { + return schema.GroupVersionResource{}, fmt.Errorf("not an api path: %s", path) + } + + gvr := schema.GroupVersionResource{} + + switch parts[0] { + case "api": + gvr.Group = "" + gvr.Version = parts[1] + if parts[2] == "namespaces" && len(parts) > 4 { + gvr.Resource = parts[4] + } else { + gvr.Resource = parts[2] + } + case "apis": + gvr.Group = parts[1] + gvr.Version = parts[2] + if parts[3] == "namespaces" && len(parts) > 5 { + gvr.Resource = parts[5] + } else { + gvr.Resource = parts[3] + } + default: + return schema.GroupVersionResource{}, fmt.Errorf("unknown prefix: %s", parts[0]) + } + + return gvr, nil +} diff --git a/pkg/kubernetes/kubernetes.go b/pkg/kubernetes/kubernetes.go index 7de8d6ff..dfd925d9 100644 --- a/pkg/kubernetes/kubernetes.go +++ b/pkg/kubernetes/kubernetes.go @@ -1,13 +1,16 @@ package kubernetes import ( - "k8s.io/apimachinery/pkg/runtime" + "net/http" - "github.com/containers/kubernetes-mcp-server/pkg/helm" - "github.com/containers/kubernetes-mcp-server/pkg/kiali" + "k8s.io/apimachinery/pkg/runtime" "k8s.io/client-go/kubernetes/scheme" + "k8s.io/client-go/rest" _ "k8s.io/client-go/plugin/pkg/client/auth/oidc" + + "github.com/containers/kubernetes-mcp-server/pkg/helm" + "github.com/containers/kubernetes-mcp-server/pkg/kiali" ) type HeaderKey string @@ -25,6 +28,28 @@ type Kubernetes struct { manager *Manager } +// AccessControlRestClient returns the access-controlled rest.Interface +// This ensures that any denied resources configured in the system are properly enforced +func (k *Kubernetes) AccessControlRestClient() (rest.Interface, error) { + config, err := k.manager.ToRESTConfig() + if err != nil { + return nil, err + } + config.WrapTransport = func(rt http.RoundTripper) http.RoundTripper { + return &AccessControlRoundTripper{ + delegate: rt, + accessControlRESTMapper: k.manager.accessControlRESTMapper, + } + } + + client, err := rest.RESTClientFor(config) + if err != nil { + return nil, err + } + + return client, nil +} + // AccessControlClientset returns the access-controlled clientset // This ensures that any denied resources configured in the system are properly enforced func (k *Kubernetes) AccessControlClientset() *AccessControlClientset {