-
Notifications
You must be signed in to change notification settings - Fork 221
Add MCPAuthzConfig CRD for backend-agnostic authorization #4777
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
base: main
Are you sure you want to change the base?
Changes from all commits
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 |
|---|---|---|
|
|
@@ -126,6 +126,35 @@ type MCPOIDCConfigList struct { | |
| Items []MCPOIDCConfig `json:"items"` | ||
| } | ||
|
|
||
| // ─── MCPAuthzConfig ────────────────────────────────────────────────────────── | ||
|
|
||
| //+kubebuilder:object:root=true | ||
| //+kubebuilder:deprecatedversion:warning="toolhive.stacklok.dev/v1alpha1 is deprecated; use v1beta1" | ||
| //+kubebuilder:subresource:status | ||
| //+kubebuilder:resource:shortName=authzcfg,categories=toolhive | ||
| //+kubebuilder:printcolumn:name="Type",type=string,JSONPath=`.spec.type` | ||
| //+kubebuilder:printcolumn:name="Valid",type=string,JSONPath=`.status.conditions[?(@.type=='Valid')].status` | ||
| //+kubebuilder:printcolumn:name="References",type=integer,JSONPath=`.status.referenceCount` | ||
| //+kubebuilder:printcolumn:name="Age",type=date,JSONPath=`.metadata.creationTimestamp` | ||
|
|
||
| // MCPAuthzConfig is the deprecated v1alpha1 version of the MCPAuthzConfig resource. | ||
|
Collaborator
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. This isn't true? |
||
| type MCPAuthzConfig struct { | ||
| metav1.TypeMeta `json:",inline"` // nolint:revive | ||
| metav1.ObjectMeta `json:"metadata,omitempty"` | ||
|
|
||
| Spec v1beta1.MCPAuthzConfigSpec `json:"spec,omitempty"` | ||
| Status v1beta1.MCPAuthzConfigStatus `json:"status,omitempty"` | ||
| } | ||
|
|
||
| //+kubebuilder:object:root=true | ||
|
|
||
| // MCPAuthzConfigList contains a list of MCPAuthzConfig. | ||
| type MCPAuthzConfigList struct { | ||
| metav1.TypeMeta `json:",inline"` // nolint:revive | ||
| metav1.ListMeta `json:"metadata,omitempty"` | ||
| Items []MCPAuthzConfig `json:"items"` | ||
| } | ||
|
|
||
| // ─── MCPRegistry ───────────────────────────────────────────────────────────── | ||
|
|
||
| //+kubebuilder:object:root=true | ||
|
|
@@ -397,6 +426,7 @@ type VirtualMCPServerList struct { | |
| func init() { | ||
| SchemeBuilder.Register( | ||
| &EmbeddingServer{}, &EmbeddingServerList{}, | ||
| &MCPAuthzConfig{}, &MCPAuthzConfigList{}, | ||
| &MCPExternalAuthConfig{}, &MCPExternalAuthConfigList{}, | ||
| &MCPGroup{}, &MCPGroupList{}, | ||
| &MCPOIDCConfig{}, &MCPOIDCConfigList{}, | ||
|
|
||
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,134 @@ | ||||||||||||||||||||
| // SPDX-FileCopyrightText: Copyright 2025 Stacklok, Inc. | ||||||||||||||||||||
| // SPDX-License-Identifier: Apache-2.0 | ||||||||||||||||||||
|
|
||||||||||||||||||||
| package v1beta1 | ||||||||||||||||||||
|
|
||||||||||||||||||||
| import ( | ||||||||||||||||||||
| "fmt" | ||||||||||||||||||||
|
|
||||||||||||||||||||
| metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" | ||||||||||||||||||||
| runtime "k8s.io/apimachinery/pkg/runtime" | ||||||||||||||||||||
| ) | ||||||||||||||||||||
|
|
||||||||||||||||||||
| // Condition type and reasons for MCPAuthzConfig status (RFC-0023) | ||||||||||||||||||||
| const ( | ||||||||||||||||||||
| // ConditionTypeAuthzConfigValid indicates whether the MCPAuthzConfig configuration is valid | ||||||||||||||||||||
| ConditionTypeAuthzConfigValid = ConditionTypeValid | ||||||||||||||||||||
|
|
||||||||||||||||||||
| // ConditionReasonAuthzConfigValid indicates spec validation passed | ||||||||||||||||||||
| ConditionReasonAuthzConfigValid = "ConfigValid" | ||||||||||||||||||||
|
|
||||||||||||||||||||
| // ConditionReasonAuthzConfigInvalid indicates spec validation failed | ||||||||||||||||||||
| ConditionReasonAuthzConfigInvalid = "ConfigInvalid" | ||||||||||||||||||||
| ) | ||||||||||||||||||||
|
|
||||||||||||||||||||
| // MCPAuthzConfigSpec defines the desired state of MCPAuthzConfig. | ||||||||||||||||||||
| // MCPAuthzConfig resources are namespace-scoped and can only be referenced by | ||||||||||||||||||||
| // MCPServer, MCPRemoteProxy, or VirtualMCPServer resources in the same namespace. | ||||||||||||||||||||
| type MCPAuthzConfigSpec struct { | ||||||||||||||||||||
| // Type identifies the authorizer backend (e.g., "cedarv1", "httpv1"). | ||||||||||||||||||||
| // Must match a registered authorizer type in the factory registry. | ||||||||||||||||||||
| // +kubebuilder:validation:Required | ||||||||||||||||||||
| // +kubebuilder:validation:MinLength=1 | ||||||||||||||||||||
|
Collaborator
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. [LOW] No Validate() method on MCPAuthzConfig type (Consensus: 7/10) Other shared config CRDs (MCPOIDCConfig, MCPTelemetryConfig, MCPExternalAuthConfig) define a The deviation is understandable (validation requires the factory registry, an external dependency), but a comment in the controller explaining why would help maintainability. Raised by: toolhive-expert, code-reviewer |
||||||||||||||||||||
| Type string `json:"type"` | ||||||||||||||||||||
|
|
||||||||||||||||||||
| // Config contains the backend-specific authorization configuration. | ||||||||||||||||||||
| // The structure depends on the Type field: | ||||||||||||||||||||
| // - cedarv1: policies ([]string), entities_json (string), primary_upstream_provider (string), group_claim_name (string) | ||||||||||||||||||||
| // - httpv1: http ({url, timeout, insecure_skip_verify}), context ({include_args, include_operation}), claim_mapping (string) | ||||||||||||||||||||
| // +kubebuilder:pruning:PreserveUnknownFields | ||||||||||||||||||||
| // +kubebuilder:validation:Type=object | ||||||||||||||||||||
| Config runtime.RawExtension `json:"config"` | ||||||||||||||||||||
| } | ||||||||||||||||||||
|
|
||||||||||||||||||||
| // MCPAuthzConfigStatus defines the observed state of MCPAuthzConfig | ||||||||||||||||||||
| type MCPAuthzConfigStatus struct { | ||||||||||||||||||||
| // Conditions represent the latest available observations of the MCPAuthzConfig's state | ||||||||||||||||||||
| // +listType=map | ||||||||||||||||||||
| // +listMapKey=type | ||||||||||||||||||||
| // +optional | ||||||||||||||||||||
| Conditions []metav1.Condition `json:"conditions,omitempty"` | ||||||||||||||||||||
|
|
||||||||||||||||||||
| // ObservedGeneration is the most recent generation observed for this MCPAuthzConfig. | ||||||||||||||||||||
| // +optional | ||||||||||||||||||||
| ObservedGeneration int64 `json:"observedGeneration,omitempty"` | ||||||||||||||||||||
|
|
||||||||||||||||||||
| // ConfigHash is a hash of the current configuration for change detection | ||||||||||||||||||||
| // +optional | ||||||||||||||||||||
| ConfigHash string `json:"configHash,omitempty"` | ||||||||||||||||||||
|
|
||||||||||||||||||||
| // ReferenceCount is the number of workloads referencing this config. | ||||||||||||||||||||
| // +optional | ||||||||||||||||||||
| ReferenceCount int32 `json:"referenceCount,omitempty"` | ||||||||||||||||||||
|
|
||||||||||||||||||||
| // ReferencingWorkloads is a list of workload resources that reference this MCPAuthzConfig. | ||||||||||||||||||||
| // Each entry identifies the workload by kind and name. | ||||||||||||||||||||
| // +listType=map | ||||||||||||||||||||
| // +listMapKey=name | ||||||||||||||||||||
| // +optional | ||||||||||||||||||||
| ReferencingWorkloads []WorkloadReference `json:"referencingWorkloads,omitempty"` | ||||||||||||||||||||
| } | ||||||||||||||||||||
|
|
||||||||||||||||||||
|
Collaborator
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. [LOW] ReferencingWorkloads listMapKey=name may be insufficient (Consensus: 7/10) Using only // +listMapKey=kind
// +listMapKey=nameThis is a pre-existing pattern copied from MCPOIDCConfig, MCPTelemetryConfig, etc. -- consider fixing across all config CRDs in a separate PR. Raised by: kubernetes-expert |
||||||||||||||||||||
| // +kubebuilder:object:root=true | ||||||||||||||||||||
| // +kubebuilder:storageversion | ||||||||||||||||||||
| // +kubebuilder:subresource:status | ||||||||||||||||||||
| // +kubebuilder:metadata:labels=toolhive.stacklok.dev/auto-migrate-storage-version=true | ||||||||||||||||||||
| // +kubebuilder:resource:shortName=authzcfg,categories=toolhive | ||||||||||||||||||||
| // +kubebuilder:printcolumn:name="Type",type=string,JSONPath=`.spec.type` | ||||||||||||||||||||
| // +kubebuilder:printcolumn:name="Valid",type=string,JSONPath=`.status.conditions[?(@.type=='Valid')].status` | ||||||||||||||||||||
| // +kubebuilder:printcolumn:name="References",type=integer,JSONPath=`.status.referenceCount` | ||||||||||||||||||||
| // +kubebuilder:printcolumn:name="Age",type=date,JSONPath=`.metadata.creationTimestamp` | ||||||||||||||||||||
|
|
||||||||||||||||||||
| // MCPAuthzConfig is the Schema for the mcpauthzconfigs API. | ||||||||||||||||||||
| // MCPAuthzConfig resources are namespace-scoped and can only be referenced by | ||||||||||||||||||||
| // MCPServer, MCPRemoteProxy, or VirtualMCPServer resources within the same namespace. | ||||||||||||||||||||
| // Cross-namespace references are not supported for security and isolation reasons. | ||||||||||||||||||||
| type MCPAuthzConfig struct { | ||||||||||||||||||||
| metav1.TypeMeta `json:",inline"` // nolint:revive | ||||||||||||||||||||
| metav1.ObjectMeta `json:"metadata,omitempty"` | ||||||||||||||||||||
|
|
||||||||||||||||||||
| Spec MCPAuthzConfigSpec `json:"spec,omitempty"` | ||||||||||||||||||||
| Status MCPAuthzConfigStatus `json:"status,omitempty"` | ||||||||||||||||||||
| } | ||||||||||||||||||||
|
|
||||||||||||||||||||
| // +kubebuilder:object:root=true | ||||||||||||||||||||
|
|
||||||||||||||||||||
| // MCPAuthzConfigList contains a list of MCPAuthzConfig | ||||||||||||||||||||
| type MCPAuthzConfigList struct { | ||||||||||||||||||||
| metav1.TypeMeta `json:",inline"` // nolint:revive | ||||||||||||||||||||
| metav1.ListMeta `json:"metadata,omitempty"` | ||||||||||||||||||||
| Items []MCPAuthzConfig `json:"items"` | ||||||||||||||||||||
| } | ||||||||||||||||||||
|
|
||||||||||||||||||||
| // MCPAuthzConfigReference references a shared MCPAuthzConfig resource. | ||||||||||||||||||||
| // The referenced MCPAuthzConfig must be in the same namespace as the referencing workload. | ||||||||||||||||||||
| type MCPAuthzConfigReference struct { | ||||||||||||||||||||
| // Name is the name of the MCPAuthzConfig resource in the same namespace. | ||||||||||||||||||||
| // +kubebuilder:validation:Required | ||||||||||||||||||||
| // +kubebuilder:validation:MinLength=1 | ||||||||||||||||||||
| Name string `json:"name"` | ||||||||||||||||||||
| } | ||||||||||||||||||||
|
|
||||||||||||||||||||
| // Validate performs structural validation on the MCPAuthzConfig spec. | ||||||||||||||||||||
| // This method is called by the controller during reconciliation. | ||||||||||||||||||||
| // | ||||||||||||||||||||
| // Note: This provides defense-in-depth alongside CEL validation rules. CEL catches | ||||||||||||||||||||
| // issues at API admission time, but this method also validates stored objects to | ||||||||||||||||||||
| // catch any that bypassed CEL or were stored before CEL rules were added. | ||||||||||||||||||||
| // | ||||||||||||||||||||
| // Backend-specific validation (delegating to the authorizer factory registry) lives | ||||||||||||||||||||
| // in the controller rather than here, because the API types package must not import | ||||||||||||||||||||
| // the authorizer backends. | ||||||||||||||||||||
| func (r *MCPAuthzConfig) Validate() error { | ||||||||||||||||||||
|
Collaborator
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. F13 — LOW: Consider consulting the authorizers registry from
Suggested change
(Adjust the helper name to whatever the registry actually exposes.) |
||||||||||||||||||||
| if r.Spec.Type == "" { | ||||||||||||||||||||
| return fmt.Errorf("type must not be empty") | ||||||||||||||||||||
| } | ||||||||||||||||||||
| if len(r.Spec.Config.Raw) == 0 { | ||||||||||||||||||||
| return fmt.Errorf("config must not be empty") | ||||||||||||||||||||
| } | ||||||||||||||||||||
| return nil | ||||||||||||||||||||
| } | ||||||||||||||||||||
|
|
||||||||||||||||||||
| func init() { | ||||||||||||||||||||
| SchemeBuilder.Register(&MCPAuthzConfig{}, &MCPAuthzConfigList{}) | ||||||||||||||||||||
| } | ||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,60 @@ | ||
| // SPDX-FileCopyrightText: Copyright 2025 Stacklok, Inc. | ||
| // SPDX-License-Identifier: Apache-2.0 | ||
|
|
||
| package v1beta1 | ||
|
|
||
| import ( | ||
| "testing" | ||
|
|
||
| "github.com/stretchr/testify/assert" | ||
| runtime "k8s.io/apimachinery/pkg/runtime" | ||
| ) | ||
|
|
||
| func TestMCPAuthzConfig_Validate(t *testing.T) { | ||
| t.Parallel() | ||
|
|
||
| tests := []struct { | ||
| name string | ||
| spec MCPAuthzConfigSpec | ||
| expectError bool | ||
| }{ | ||
| { | ||
| name: "valid spec passes", | ||
| spec: MCPAuthzConfigSpec{ | ||
| Type: "cedarv1", | ||
| Config: runtime.RawExtension{Raw: []byte(`{"policies":[]}`)}, | ||
| }, | ||
| expectError: false, | ||
| }, | ||
| { | ||
| name: "empty type fails", | ||
| spec: MCPAuthzConfigSpec{ | ||
| Type: "", | ||
| Config: runtime.RawExtension{Raw: []byte(`{}`)}, | ||
| }, | ||
| expectError: true, | ||
| }, | ||
| { | ||
| name: "empty config raw fails", | ||
| spec: MCPAuthzConfigSpec{ | ||
| Type: "cedarv1", | ||
| Config: runtime.RawExtension{Raw: []byte{}}, | ||
| }, | ||
| expectError: true, | ||
| }, | ||
| } | ||
|
|
||
| for _, tt := range tests { | ||
| t.Run(tt.name, func(t *testing.T) { | ||
| t.Parallel() | ||
|
|
||
| cfg := &MCPAuthzConfig{Spec: tt.spec} | ||
| err := cfg.Validate() | ||
| if tt.expectError { | ||
| assert.Error(t, err) | ||
| } else { | ||
| assert.NoError(t, err) | ||
| } | ||
| }) | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -200,6 +200,7 @@ const SessionStorageProviderRedis = "redis" | |
|
|
||
| // MCPServerSpec defines the desired state of MCPServer | ||
| // | ||
| // +kubebuilder:validation:XValidation:rule="!(has(self.authzConfig) && has(self.authzConfigRef))",message="authzConfig and authzConfigRef are mutually exclusive; use authzConfigRef to reference a shared MCPAuthzConfig" | ||
|
Collaborator
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. F7 — MEDIUM: This XValidation rule (and the parallel rules on Please add envtest coverage for all three new mutual-exclusion rules. This is the exact scenario the |
||
| // +kubebuilder:validation:XValidation:rule="!has(self.rateLimiting) || (has(self.sessionStorage) && self.sessionStorage.provider == 'redis')",message="rateLimiting requires sessionStorage with provider 'redis'" | ||
| // +kubebuilder:validation:XValidation:rule="!(has(self.rateLimiting) && has(self.rateLimiting.perUser)) || has(self.oidcConfigRef) || has(self.externalAuthConfigRef)",message="rateLimiting.perUser requires authentication (oidcConfigRef or externalAuthConfigRef)" | ||
| // +kubebuilder:validation:XValidation:rule="!has(self.rateLimiting) || !has(self.rateLimiting.tools) || self.rateLimiting.tools.all(t, !has(t.perUser)) || has(self.oidcConfigRef) || has(self.externalAuthConfigRef)",message="per-tool perUser rate limiting requires authentication (oidcConfigRef or externalAuthConfigRef)" | ||
|
|
@@ -293,10 +294,18 @@ type MCPServerSpec struct { | |
| // +optional | ||
| OIDCConfigRef *MCPOIDCConfigReference `json:"oidcConfigRef,omitempty"` | ||
|
|
||
| // AuthzConfig defines authorization policy configuration for the MCP server | ||
| // AuthzConfig defines authorization policy configuration for the MCP server. | ||
| // Deprecated: Use AuthzConfigRef to reference a shared MCPAuthzConfig resource instead. | ||
|
Collaborator
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. F16 — LOW: Marking the inline field Either defer the |
||
| // AuthzConfig and AuthzConfigRef are mutually exclusive. | ||
| // +optional | ||
| AuthzConfig *AuthzConfigRef `json:"authzConfig,omitempty"` | ||
|
|
||
| // AuthzConfigRef references a shared MCPAuthzConfig resource for authorization. | ||
| // The referenced MCPAuthzConfig must exist in the same namespace as this MCPServer. | ||
| // Mutually exclusive with authzConfig. | ||
| // +optional | ||
| AuthzConfigRef *MCPAuthzConfigReference `json:"authzConfigRef,omitempty"` | ||
|
Collaborator
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. [HIGH] Missing CEL XValidation for authzConfig/authzConfigRef mutual exclusivity (Consensus: 10/10) The // +kubebuilder:validation:XValidation:rule="!(has(self.oidcConfig) && has(self.oidcConfigRef))",message="oidcConfig and oidcConfigRef are mutually exclusive..."The same rule is needed here on // +kubebuilder:validation:XValidation:rule="!(has(self.authzConfig) && has(self.authzConfigRef))",message="authzConfig and authzConfigRef are mutually exclusive; use authzConfigRef to reference a shared MCPAuthzConfig"Without this, users can set both fields simultaneously and the controller must decide which takes precedence. Raised by: kubernetes-expert, code-reviewer, toolhive-expert
Collaborator
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. F1 — MEDIUM: Consider one of:
|
||
|
|
||
| // Audit defines audit logging configuration for the MCP server | ||
| // +optional | ||
| Audit *AuditConfig `json:"audit,omitempty"` | ||
|
|
||
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.
We don't need this? As
MCPAuthzConfigis a new CRD?