-
Notifications
You must be signed in to change notification settings - Fork 178
feat(config): deny resources by using RESTMapper as an interceptor #149
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 1 commit
3872cdb
78d92f2
8a0681a
9a6ebe8
8b39eea
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,40 @@ | ||
| package kubernetes | ||
|
|
||
| import ( | ||
| "fmt" | ||
|
|
||
| "k8s.io/apimachinery/pkg/runtime/schema" | ||
|
|
||
| "github.com/manusa/kubernetes-mcp-server/pkg/config" | ||
| ) | ||
|
|
||
| // isAllowed checks the resource is in denied list or not. | ||
| // If it is in denied list, this function returns false. | ||
| func isAllowed( | ||
| staticConfig *config.StaticConfig, // TODO: maybe just use the denied resource slice | ||
| gvk *schema.GroupVersionKind, | ||
| ) bool { | ||
| if staticConfig == nil { | ||
| return true | ||
| } | ||
|
|
||
| for _, val := range staticConfig.DeniedResources { | ||
| // If kind is empty, that means Group/Version pair is denied entirely | ||
| if val.Kind == "" { | ||
| if gvk.Group == val.Group && gvk.Version == val.Version { | ||
| return false | ||
| } | ||
| } | ||
| if gvk.Group == val.Group && | ||
| gvk.Version == val.Version && | ||
| gvk.Kind == val.Kind { | ||
| return false | ||
| } | ||
| } | ||
|
|
||
| return true | ||
| } | ||
|
|
||
| func isNotAllowedError(gvk *schema.GroupVersionKind) error { | ||
| return fmt.Errorf("resource not allowed: %s", gvk.String()) | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,74 @@ | ||
| package kubernetes | ||
|
|
||
| import ( | ||
| authorizationv1api "k8s.io/api/authorization/v1" | ||
| "k8s.io/apimachinery/pkg/runtime/schema" | ||
| "k8s.io/client-go/discovery" | ||
| "k8s.io/client-go/kubernetes" | ||
| authorizationv1 "k8s.io/client-go/kubernetes/typed/authorization/v1" | ||
| corev1 "k8s.io/client-go/kubernetes/typed/core/v1" | ||
| "k8s.io/client-go/rest" | ||
|
|
||
| "github.com/manusa/kubernetes-mcp-server/pkg/config" | ||
| ) | ||
|
|
||
| // AccessControlClientset is a limited clientset delegating interface to the standard kubernetes.Clientset | ||
| // Only a limited set of functions are implemented with a single point of access to the kubernetes API where | ||
| // apiVersion and kinds are checked for allowed access | ||
| type AccessControlClientset struct { | ||
| delegate kubernetes.Interface | ||
| discoveryClient discovery.DiscoveryInterface | ||
| staticConfig *config.StaticConfig // TODO: maybe just store the denied resource slice | ||
| } | ||
|
|
||
| func (a *AccessControlClientset) DiscoveryClient() discovery.DiscoveryInterface { | ||
| return a.discoveryClient | ||
| } | ||
|
|
||
| func (a *AccessControlClientset) Pods(namespace string) (corev1.PodInterface, error) { | ||
| gvk := &schema.GroupVersionKind{Group: "", Version: "v1", Kind: "Pod"} | ||
| if !isAllowed(a.staticConfig, gvk) { | ||
| return nil, isNotAllowedError(gvk) | ||
| } | ||
| return a.delegate.CoreV1().Pods(namespace), nil | ||
| } | ||
|
|
||
| func (a *AccessControlClientset) PodsExec(namespace, name string) (*rest.Request, error) { | ||
|
||
| gvk := &schema.GroupVersionKind{Group: "", Version: "v1", Kind: "Pod"} | ||
| if !isAllowed(a.staticConfig, gvk) { | ||
| return nil, isNotAllowedError(gvk) | ||
| } | ||
| // https://github.com/kubernetes/kubectl/blob/5366de04e168bcbc11f5e340d131a9ca8b7d0df4/pkg/cmd/exec/exec.go#L382-L397 | ||
| return a.delegate.CoreV1().RESTClient(). | ||
| Post(). | ||
| Resource("pods"). | ||
| Namespace(namespace). | ||
| Name(name). | ||
| SubResource("exec"), nil | ||
| } | ||
|
|
||
| func (a *AccessControlClientset) Services(namespace string) (corev1.ServiceInterface, error) { | ||
| gvk := &schema.GroupVersionKind{Group: "", Version: "v1", Kind: "Service"} | ||
| if !isAllowed(a.staticConfig, gvk) { | ||
| return nil, isNotAllowedError(gvk) | ||
| } | ||
| return a.delegate.CoreV1().Services(namespace), nil | ||
| } | ||
|
|
||
| func (a *AccessControlClientset) SelfSubjectAccessReviews() (authorizationv1.SelfSubjectAccessReviewInterface, error) { | ||
| gvk := &schema.GroupVersionKind{Group: authorizationv1api.GroupName, Version: authorizationv1api.SchemeGroupVersion.Version, Kind: "SelfSubjectAccessReview"} | ||
| if !isAllowed(a.staticConfig, gvk) { | ||
| return nil, isNotAllowedError(gvk) | ||
| } | ||
| return a.delegate.AuthorizationV1().SelfSubjectAccessReviews(), nil | ||
| } | ||
|
|
||
| func NewAccessControlClientset(cfg *rest.Config, staticConfig *config.StaticConfig) (*AccessControlClientset, error) { | ||
| clientSet, err := kubernetes.NewForConfig(cfg) | ||
| if err != nil { | ||
| return nil, err | ||
| } | ||
| return &AccessControlClientset{ | ||
| delegate: clientSet, discoveryClient: clientSet.DiscoveryClient, staticConfig: staticConfig, | ||
| }, nil | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -101,7 +101,7 @@ func (k *Kubernetes) resourcesListAsTable(ctx context.Context, gvk *schema.Group | |
| } | ||
| url = append(url, gvr.Resource) | ||
| var table metav1.Table | ||
| err := k.manager.clientSet.CoreV1().RESTClient(). | ||
| err := k.manager.discoveryClient.RESTClient(). | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think, now we support all the resources
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Not sure what you mean with this.
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. NVM |
||
| Get(). | ||
| SetHeader("Accept", strings.Join([]string{ | ||
| fmt.Sprintf("application/json;as=Table;v=%s;g=%s", metav1.SchemeGroupVersion.Version, metav1.GroupName), | ||
|
|
@@ -188,7 +188,11 @@ func (k *Kubernetes) supportsGroupVersion(groupVersion string) bool { | |
| } | ||
|
|
||
| func (k *Kubernetes) canIUse(ctx context.Context, gvr *schema.GroupVersionResource, namespace, verb string) bool { | ||
| response, err := k.manager.clientSet.AuthorizationV1().SelfSubjectAccessReviews().Create(ctx, &authv1.SelfSubjectAccessReview{ | ||
| accessReviews, err := k.manager.accessControlClientSet.SelfSubjectAccessReviews() | ||
| if err != nil { | ||
| return false | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. very nice! |
||
| } | ||
| response, err := accessReviews.Create(ctx, &authv1.SelfSubjectAccessReview{ | ||
| Spec: authv1.SelfSubjectAccessReviewSpec{ResourceAttributes: &authv1.ResourceAttributes{ | ||
| Namespace: namespace, | ||
| Verb: verb, | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
(not a blocker for this PR):
We have to return 403: Forbidden HTTP Status. But I'm not really sure that mcp library we use support this. If it doesn't support returning http code we have, we'll have major issue in oauth side as well.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It just simply sets http 200 statically https://github.com/mark3labs/mcp-go/blob/1eddde7bd69b760f745a1b4064969cffcf97e935/server/streamable_http.go#L347 :(
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, I don't think the library (or even the protocol) is prepared for fine-grained operation status codes yet.
As I understand it, the protocol negotiates authentication and authorization first, and then enables