Skip to content

Commit 4f69d09

Browse files
committed
feat(kubernetes): access control round tripper
Signed-off-by: Marc Nuri <[email protected]>
1 parent b2f8a8c commit 4f69d09

12 files changed

+424
-199
lines changed

internal/test/mock_server.go

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -200,7 +200,9 @@ func (h *DiscoveryClientHandler) ServeHTTP(w http.ResponseWriter, req *http.Requ
200200
// Request Performed by DiscoveryClient to Kube API (Get API Groups)
201201
if req.URL.Path == "/apis" {
202202
w.Header().Set("Content-Type", "application/json")
203-
_, _ = w.Write([]byte(`{"kind":"APIGroupList","apiVersion":"v1","groups":[]}`))
203+
_, _ = w.Write([]byte(`{"kind":"APIGroupList","apiVersion":"v1","groups":[
204+
{"name":"apps","versions":[{"groupVersion":"apps/v1","version":"v1"}],"preferredVersion":{"groupVersion":"apps/v1","version":"v1"}}
205+
]}`))
204206
return
205207
}
206208
// Request Performed by DiscoveryClient to Kube API (Get API Resources)
@@ -211,6 +213,13 @@ func (h *DiscoveryClientHandler) ServeHTTP(w http.ResponseWriter, req *http.Requ
211213
]}`))
212214
return
213215
}
216+
if req.URL.Path == "/apis/apps/v1" {
217+
w.Header().Set("Content-Type", "application/json")
218+
_, _ = w.Write([]byte(`{"kind":"APIResourceList","apiVersion":"v1","groupVersion":"apps/v1","resources":[
219+
{"name":"deployments","singularName":"","namespaced":true,"kind":"Deployment","verbs":["get","list","watch","create","update","patch","delete"]}
220+
]}`))
221+
return
222+
}
214223
}
215224

216225
type InOpenShiftHandler struct {

pkg/helm/helm.go

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,15 +3,16 @@ package helm
33
import (
44
"context"
55
"fmt"
6+
"time"
7+
68
"helm.sh/helm/v3/pkg/action"
79
"helm.sh/helm/v3/pkg/chart/loader"
810
"helm.sh/helm/v3/pkg/cli"
911
"helm.sh/helm/v3/pkg/registry"
1012
"helm.sh/helm/v3/pkg/release"
1113
"k8s.io/cli-runtime/pkg/genericclioptions"
12-
"log"
14+
"k8s.io/klog/v2"
1315
"sigs.k8s.io/yaml"
14-
"time"
1516
)
1617

1718
type Kubernetes interface {
@@ -115,7 +116,7 @@ func (h *Helm) newAction(namespace string, allNamespaces bool) (*action.Configur
115116
return nil, err
116117
}
117118
cfg.RegistryClient = registryClient
118-
return cfg, cfg.Init(h.kubernetes, applicableNamespace, "", log.Printf)
119+
return cfg, cfg.Init(h.kubernetes, applicableNamespace, "", klog.V(5).Infof)
119120
}
120121

121122
func simplify(release ...*release.Release) []map[string]interface{} {

pkg/kubernetes/accesscontrol_restclient.go

Lines changed: 0 additions & 61 deletions
This file was deleted.

pkg/kubernetes/accesscontrol_restmapper.go

Lines changed: 0 additions & 80 deletions
This file was deleted.
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
package kubernetes
2+
3+
import (
4+
"fmt"
5+
"net/http"
6+
"strings"
7+
8+
"github.com/containers/kubernetes-mcp-server/pkg/config"
9+
"k8s.io/apimachinery/pkg/api/meta"
10+
"k8s.io/apimachinery/pkg/runtime/schema"
11+
)
12+
13+
type AccessControlRoundTripper struct {
14+
delegate http.RoundTripper
15+
staticConfig *config.StaticConfig
16+
restMapper meta.RESTMapper
17+
}
18+
19+
func (rt *AccessControlRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) {
20+
gvr, ok := parseURLToGVR(req.URL.Path)
21+
// Not an API resource request, just pass through
22+
if !ok {
23+
return rt.delegate.RoundTrip(req)
24+
}
25+
26+
gvk, err := rt.restMapper.KindFor(gvr)
27+
if err != nil {
28+
return nil, fmt.Errorf("failed to make request: AccessControlRoundTripper failed to get kind for gvr %v: %w", gvr, err)
29+
}
30+
if !isAllowed(rt.staticConfig, &gvk) {
31+
return nil, isNotAllowedError(&gvk)
32+
}
33+
34+
return rt.delegate.RoundTrip(req)
35+
}
36+
37+
func parseURLToGVR(path string) (gvr schema.GroupVersionResource, ok bool) {
38+
parts := strings.Split(strings.Trim(path, "/"), "/")
39+
40+
gvr = schema.GroupVersionResource{}
41+
switch parts[0] {
42+
case "api":
43+
// /api or /api/v1 are discovery endpoints
44+
if len(parts) < 3 {
45+
return
46+
}
47+
gvr.Group = ""
48+
gvr.Version = parts[1]
49+
if parts[2] == "namespaces" && len(parts) > 4 {
50+
gvr.Resource = parts[4]
51+
} else {
52+
gvr.Resource = parts[2]
53+
}
54+
case "apis":
55+
// /apis, /apis/apps, or /apis/apps/v1 are discovery endpoints
56+
if len(parts) < 4 {
57+
return
58+
}
59+
gvr.Group = parts[1]
60+
gvr.Version = parts[2]
61+
if parts[3] == "namespaces" && len(parts) > 5 {
62+
gvr.Resource = parts[5]
63+
} else {
64+
gvr.Resource = parts[3]
65+
}
66+
default:
67+
return
68+
}
69+
return gvr, true
70+
}

0 commit comments

Comments
 (0)