Skip to content

Commit 07c9785

Browse files
manaswinidasppadti
andauthored
fix(model-registry): scope in-cluster K8s client to SAR-only interface in MCP deployment auth (#7259)
* fix(model-registry): scope in-cluster K8s client to SAR-only interface in MCP deployment auth Move the MCP server SubjectAccessReview logic into the shared KubernetesClientInterface (CanVerbMcpServersInNamespace) so requireMcpDeploymentAccess uses the same K8s client infrastructure as every other handler. This removes the separate InClusterConfig path, the cached authorizationv1client.SubjectAccessReviewInterface, and the package-level sync.Once from mcp_deployment_auth.go. - InternalKubernetesClient: SAR with explicit user/groups - TokenKubernetesClient: SelfSubjectAccessReview via user token - requireMcpDeploymentAccess: delegates to factory client https: //issues.redhat.com/browse/RHOAIENG-57223 Made-with: Cursor * Update packages/model-registry/upstream/bff/internal/redhat/handlers/mcp_deployment_auth.go Co-authored-by: Pushpa Padti <99261071+ppadti@users.noreply.github.com> * Update packages/model-registry/upstream/bff/internal/redhat/handlers/mcp_deployment_auth.go Co-authored-by: Pushpa Padti <99261071+ppadti@users.noreply.github.com> * restore auth method guard in requireMcpDeploymentAccess --------- Co-authored-by: Pushpa Padti <99261071+ppadti@users.noreply.github.com>
1 parent bb045de commit 07c9785

5 files changed

Lines changed: 84 additions & 57 deletions

File tree

packages/model-registry/upstream/bff/internal/integrations/kubernetes/client.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,9 @@ const CatalogSourceKey = "sources.yaml"
1515
const CatalogSourceDefaultConfigMapName = "default-catalog-sources"
1616
const CatalogSourceUserConfigMapName = "model-catalog-sources"
1717

18+
const McpServerAPIGroup = "mcp.x-k8s.io"
19+
const McpServerResource = "mcpservers"
20+
1821
type KubernetesClientInterface interface {
1922
// Service discovery
2023
GetServiceNames(ctx context.Context, namespace string) ([]string, error)
@@ -31,6 +34,7 @@ type KubernetesClientInterface interface {
3134
CanListServicesInNamespace(ctx context.Context, identity *RequestIdentity, namespace string) (bool, error)
3235
CanAccessServiceInNamespace(ctx context.Context, identity *RequestIdentity, namespace, serviceName string) (bool, error)
3336
CanNamespaceAccessRegistry(ctx context.Context, identity *RequestIdentity, jobNamespace, registryName, registryNamespace string) (bool, error)
37+
CanVerbMcpServersInNamespace(ctx context.Context, identity *RequestIdentity, namespace, verb string) (bool, error)
3438
GetSelfSubjectRulesReview(ctx context.Context, identity *RequestIdentity, namespace string) ([]string, error)
3539

3640
// Meta

packages/model-registry/upstream/bff/internal/integrations/kubernetes/internal_k8s_client.go

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,37 @@ func (kc *InternalKubernetesClient) CanNamespaceAccessRegistry(ctx context.Conte
149149
return CanNamespaceAccessRegistry(ctx, kc.Client, kc.Logger, jobNamespace, registryName, registryNamespace)
150150
}
151151

152+
func (kc *InternalKubernetesClient) CanVerbMcpServersInNamespace(ctx context.Context, identity *RequestIdentity, namespace, verb string) (bool, error) {
153+
ctx, cancel := context.WithTimeout(ctx, 30*time.Second)
154+
defer cancel()
155+
156+
sar := &authv1.SubjectAccessReview{
157+
Spec: authv1.SubjectAccessReviewSpec{
158+
User: identity.UserID,
159+
Groups: identity.Groups,
160+
ResourceAttributes: &authv1.ResourceAttributes{
161+
Verb: verb,
162+
Group: McpServerAPIGroup,
163+
Resource: McpServerResource,
164+
Namespace: namespace,
165+
},
166+
},
167+
}
168+
169+
resp, err := kc.Client.AuthorizationV1().SubjectAccessReviews().Create(ctx, sar, metav1.CreateOptions{})
170+
if err != nil {
171+
kc.Logger.Error("SAR failed for MCP server access", "user", identity.UserID, "verb", verb, "namespace", namespace, "error", err)
172+
return false, err
173+
}
174+
175+
if !resp.Status.Allowed {
176+
kc.Logger.Warn("MCP server access denied", "user", identity.UserID, "verb", verb, "namespace", namespace)
177+
return false, nil
178+
}
179+
180+
return true, nil
181+
}
182+
152183
// GetSelfSubjectRulesReview gets the rules for what a user can access in a namespace
153184
func (kc *InternalKubernetesClient) GetSelfSubjectRulesReview(ctx context.Context, identity *RequestIdentity, namespace string) ([]string, error) {
154185
kc.Logger.Warn("GetSelfSubjectRulesReview not fully implemented for internal client",

packages/model-registry/upstream/bff/internal/integrations/kubernetes/token_k8s_client.go

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -234,6 +234,35 @@ func (kc *TokenKubernetesClient) CanNamespaceAccessRegistry(ctx context.Context,
234234
return CanNamespaceAccessRegistry(ctx, kc.Client, kc.Logger, jobNamespace, registryName, registryNamespace)
235235
}
236236

237+
func (kc *TokenKubernetesClient) CanVerbMcpServersInNamespace(ctx context.Context, _ *RequestIdentity, namespace, verb string) (bool, error) {
238+
ctx, cancel := context.WithTimeout(ctx, 30*time.Second)
239+
defer cancel()
240+
241+
sar := &authv1.SelfSubjectAccessReview{
242+
Spec: authv1.SelfSubjectAccessReviewSpec{
243+
ResourceAttributes: &authv1.ResourceAttributes{
244+
Verb: verb,
245+
Group: McpServerAPIGroup,
246+
Resource: McpServerResource,
247+
Namespace: namespace,
248+
},
249+
},
250+
}
251+
252+
resp, err := kc.Client.AuthorizationV1().SelfSubjectAccessReviews().Create(ctx, sar, metav1.CreateOptions{})
253+
if err != nil {
254+
kc.Logger.Error("self-SAR failed for MCP server access", "verb", verb, "namespace", namespace, "error", err)
255+
return false, err
256+
}
257+
258+
if !resp.Status.Allowed {
259+
kc.Logger.Warn("MCP server access denied", "verb", verb, "namespace", namespace)
260+
return false, nil
261+
}
262+
263+
return true, nil
264+
}
265+
237266
// RequestIdentity is unused because the token already represents the user identity.
238267
// This endpoint is used only on dev mode that is why is safe to ignore permissions errors
239268
func (kc *TokenKubernetesClient) GetNamespaces(ctx context.Context, _ *RequestIdentity) ([]corev1.Namespace, error) {

packages/model-registry/upstream/bff/internal/redhat/handlers/mcp_deployment_auth.go

Lines changed: 16 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -1,59 +1,34 @@
11
package handlers
22

33
import (
4-
"context"
54
"fmt"
65
"log/slog"
76
"net/http"
8-
"sync"
9-
"time"
10-
11-
authv1 "k8s.io/api/authorization/v1"
12-
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
13-
"k8s.io/client-go/kubernetes"
14-
"k8s.io/client-go/rest"
157

168
"github.com/kubeflow/model-registry/ui/bff/internal/api"
179
"github.com/kubeflow/model-registry/ui/bff/internal/config"
1810
"github.com/kubeflow/model-registry/ui/bff/internal/constants"
1911
k8s "github.com/kubeflow/model-registry/ui/bff/internal/integrations/kubernetes"
2012
)
2113

22-
const (
23-
mcpServerSARAPIGroup = "mcp.x-k8s.io"
24-
mcpServerSARResource = "mcpservers"
25-
sarRequestTimeout = 30 * time.Second
26-
)
27-
28-
var (
29-
sarClientOnce sync.Once
30-
sarClient kubernetes.Interface
31-
)
32-
33-
// initSARClient lazily initializes a typed K8s clientset using in-cluster config.
34-
// Returns nil when not running in a cluster (e.g. federated/dev mode).
35-
func initSARClient() kubernetes.Interface {
36-
sarClientOnce.Do(func() {
37-
cfg, err := rest.InClusterConfig()
38-
if err != nil {
39-
return
40-
}
41-
sarClient, _ = kubernetes.NewForConfig(cfg)
42-
})
43-
return sarClient
44-
}
45-
46-
// requireMcpDeploymentAccess performs a SubjectAccessReview to verify that
47-
// the requesting user has the given verb on mcpservers in the target namespace.
14+
// requireMcpDeploymentAccess performs a permission check to verify that the
15+
// requesting user has the given verb on mcpservers in the target namespace.
16+
// SAR is only performed when the auth method is "internal" (in-cluster service
17+
// account). For user-token auth the K8s API server enforces authorization via
18+
// the user's own bearer token, so no server-side SAR is needed.
4819
func requireMcpDeploymentAccess(app *api.App, w http.ResponseWriter, r *http.Request, namespace, verb string) bool {
4920
if app.Config().AuthMethod != config.AuthMethodInternal {
5021
return true
5122
}
5223

53-
client := initSARClient()
24+
client, err := app.KubernetesClientFactory().GetClient(r.Context())
25+
if err != nil {
26+
app.ServerError(w, r, fmt.Errorf("failed to get Kubernetes client for MCP access check: %w", err))
27+
return false
28+
}
5429
if client == nil {
55-
app.Logger().Warn("SAR client unavailable in internal auth mode, skipping access check")
56-
return true
30+
app.ServerError(w, r, fmt.Errorf("kubernetes client factory returned nil client without error"))
31+
return false
5732
}
5833

5934
identity, ok := r.Context().Value(constants.RequestIdentityKey).(*k8s.RequestIdentity)
@@ -62,25 +37,9 @@ func requireMcpDeploymentAccess(app *api.App, w http.ResponseWriter, r *http.Req
6237
return false
6338
}
6439

65-
ctx, cancel := context.WithTimeout(r.Context(), sarRequestTimeout)
66-
defer cancel()
67-
68-
sar := &authv1.SubjectAccessReview{
69-
Spec: authv1.SubjectAccessReviewSpec{
70-
User: identity.UserID,
71-
Groups: identity.Groups,
72-
ResourceAttributes: &authv1.ResourceAttributes{
73-
Verb: verb,
74-
Group: mcpServerSARAPIGroup,
75-
Resource: mcpServerSARResource,
76-
Namespace: namespace,
77-
},
78-
},
79-
}
80-
81-
response, err := client.AuthorizationV1().SubjectAccessReviews().Create(ctx, sar, metav1.CreateOptions{})
40+
allowed, err := client.CanVerbMcpServersInNamespace(r.Context(), identity, namespace, verb)
8241
if err != nil {
83-
app.Logger().Error("SAR check failed",
42+
app.Logger().Error("MCP server access check failed",
8443
slog.String("user", identity.UserID),
8544
slog.String("verb", verb),
8645
slog.String("namespace", namespace),
@@ -90,8 +49,8 @@ func requireMcpDeploymentAccess(app *api.App, w http.ResponseWriter, r *http.Req
9049
return false
9150
}
9251

93-
if !response.Status.Allowed {
94-
app.Logger().Warn("SAR denied",
52+
if !allowed {
53+
app.Logger().Warn("MCP server access denied",
9554
slog.String("user", identity.UserID),
9655
slog.String("verb", verb),
9756
slog.String("namespace", namespace),

packages/model-registry/upstream/bff/internal/repositories/model_transfer_jobs_test.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,10 @@ func (f *fakeKubernetesClient) CanNamespaceAccessRegistry(ctx context.Context, i
140140
return false, nil
141141
}
142142

143+
func (f *fakeKubernetesClient) CanVerbMcpServersInNamespace(ctx context.Context, identity *k8s.RequestIdentity, namespace, verb string) (bool, error) {
144+
return false, nil
145+
}
146+
143147
func (f *fakeKubernetesClient) GetSelfSubjectRulesReview(ctx context.Context, identity *k8s.RequestIdentity, namespace string) ([]string, error) {
144148
return nil, nil
145149
}

0 commit comments

Comments
 (0)