diff --git a/cmd/thv-operator/api/v1beta1/mcpserver_types.go b/cmd/thv-operator/api/v1beta1/mcpserver_types.go index 200874bc95..7021efab20 100644 --- a/cmd/thv-operator/api/v1beta1/mcpserver_types.go +++ b/cmd/thv-operator/api/v1beta1/mcpserver_types.go @@ -773,6 +773,16 @@ type InlineAuthzConfig struct { // +kubebuilder:default="[]" // +optional EntitiesJSON string `json:"entitiesJson,omitempty"` + + // PrimaryUpstreamProvider names the upstream IDP whose access token's claims + // Cedar should evaluate. Only meaningful for VirtualMCPServer with an embedded + // auth server. When empty and an embedded auth server has upstreams configured, + // the controller defaults to the first upstream provider. Ignored by MCPServer + // and MCPRemoteProxy. The name must match one of the upstreams declared on + // spec.authServerConfig.upstreamProviders; otherwise the VirtualMCPServer is + // rejected with AuthServerConfigValidated=False. + // +optional + PrimaryUpstreamProvider string `json:"primaryUpstreamProvider,omitempty"` } // AuditConfig defines audit logging configuration for the MCP server diff --git a/cmd/thv-operator/api/v1beta1/virtualmcpserver_types.go b/cmd/thv-operator/api/v1beta1/virtualmcpserver_types.go index c63139b133..58b4e4b9a8 100644 --- a/cmd/thv-operator/api/v1beta1/virtualmcpserver_types.go +++ b/cmd/thv-operator/api/v1beta1/virtualmcpserver_types.go @@ -400,6 +400,12 @@ const ( // the Cedar claim source. The advisory message names the selected upstream. ConditionReasonAuthzUpstreamAutoSelected = "AuthzUpstreamAutoSelected" + // ConditionReasonAuthzUpstreamUnknown indicates that + // spec.incomingAuth.authzConfig.inline.primaryUpstreamProvider names an upstream + // IDP that is not declared on spec.authServerConfig.upstreamProviders. Cedar + // would otherwise deny every request at runtime; reject at admission instead. + ConditionReasonAuthzUpstreamUnknown = "AuthzUpstreamUnknown" + // ConditionReasonVirtualMCPServerTelemetryConfigRefValid indicates the referenced MCPTelemetryConfig is valid ConditionReasonVirtualMCPServerTelemetryConfigRefValid = "TelemetryConfigRefValid" diff --git a/cmd/thv-operator/controllers/virtualmcpserver_controller.go b/cmd/thv-operator/controllers/virtualmcpserver_controller.go index b32bdec40d..da24728987 100644 --- a/cmd/thv-operator/controllers/virtualmcpserver_controller.go +++ b/cmd/thv-operator/controllers/virtualmcpserver_controller.go @@ -552,6 +552,20 @@ func (*VirtualMCPServerReconciler) applyAuthServerIdentitySynthesizedCondition( ) } +// extractExplicitPrimaryUpstreamProvider returns the user-specified primary +// upstream provider name from the authz config, or "" if none is set. +// +// Currently reads from inline config only. ConfigMap-sourced authz needs to +// load and parse the referenced ConfigMap; until that path lands (see the +// matching TODO in pkg/vmcpconfig/converter.go), configMap users always fall +// through to auto-selection of the first upstream. +func extractExplicitPrimaryUpstreamProvider(authzConfig *mcpv1beta1.AuthzConfigRef) string { + if authzConfig == nil || authzConfig.Inline == nil { + return "" + } + return authzConfig.Inline.PrimaryUpstreamProvider +} + // validateAuthzUpstreamAvailable ensures that when authorization policies are // configured via IncomingAuth.AuthzConfig AND an embedded AuthServer is in use, // at least one upstream IDP is declared so Cedar evaluates claim references @@ -583,7 +597,49 @@ func (*VirtualMCPServerReconciler) validateAuthzUpstreamAvailable( // Direct-IdP flow: no embedded AS. Cedar evaluates against identity.Claims // populated by incoming OIDC middleware from the IdP token. No upstream // needed; nothing to warn about. Remove any stale condition. + // + // However, an explicit primaryUpstreamProvider is meaningless in this mode + // — there is no upstream-token table for Cedar to look it up in — so the + // converter would forward a name that cannot resolve at runtime. Reject at + // admission for the same "fail loudly instead of denying every request" + // reason as the configured-AS mismatch path below. if vmcp.Spec.AuthServerConfig == nil { + explicitProvider := extractExplicitPrimaryUpstreamProvider(vmcp.Spec.IncomingAuth.AuthzConfig) + if explicitProvider != "" { + statusManager.RemoveConditionsWithPrefix(mcpv1beta1.ConditionTypeAuthzUpstreamSelectionWarning, []string{}) + + message := fmt.Sprintf( + "spec.incomingAuth.authzConfig.inline.primaryUpstreamProvider=%q is set but "+ + "spec.authServerConfig is not configured. The field names an upstream IDP "+ + "on the embedded auth server, which is required for it to take effect. "+ + "Remove primaryUpstreamProvider, or configure spec.authServerConfig with "+ + "an upstream of that name.", + explicitProvider, + ) + + ctxLogger := log.FromContext(ctx) + ctxLogger.Info("authz primaryUpstreamProvider set without an embedded auth server; rejecting VirtualMCPServer", + "name", vmcp.Name, + "namespace", vmcp.Namespace, + "primaryUpstreamProvider", explicitProvider, + "reason", mcpv1beta1.ConditionReasonAuthzUpstreamUnknown, + ) + + statusManager.SetPhase(mcpv1beta1.VirtualMCPServerPhaseFailed) + statusManager.SetMessage(message) + statusManager.SetAuthServerConfigValidatedCondition( + mcpv1beta1.ConditionReasonAuthzUpstreamUnknown, + message, + metav1.ConditionFalse, + ) + statusManager.SetObservedGeneration(vmcp.Generation) + return &SpecValidationError{ + Message: fmt.Sprintf( + "authz primaryUpstreamProvider %q set without an embedded auth server", + explicitProvider, + ), + } + } statusManager.RemoveConditionsWithPrefix(mcpv1beta1.ConditionTypeAuthzUpstreamSelectionWarning, []string{}) return nil } @@ -621,10 +677,62 @@ func (*VirtualMCPServerReconciler) validateAuthzUpstreamAvailable( return stderrors.New("authz configured without an upstream IDP") } - // Valid configuration. When multiple upstreams are declared, surface an - // advisory naming the auto-selected upstream; otherwise ensure any stale - // warning is cleared. - if len(vmcp.Spec.AuthServerConfig.UpstreamProviders) > 1 { + // If the user has set spec.incomingAuth.authzConfig.inline.primaryUpstreamProvider + // explicitly, the name must resolve to one of the declared upstreams after + // normalization on both sides. A mismatch would cause Cedar to deny every + // request at runtime — fail loudly at admission instead. + explicitProvider := extractExplicitPrimaryUpstreamProvider(vmcp.Spec.IncomingAuth.AuthzConfig) + if explicitProvider != "" { + resolved := authserver.ResolveUpstreamName(explicitProvider) + matched := false + for _, up := range vmcp.Spec.AuthServerConfig.UpstreamProviders { + if authserver.ResolveUpstreamName(up.Name) == resolved { + matched = true + break + } + } + if !matched { + statusManager.RemoveConditionsWithPrefix(mcpv1beta1.ConditionTypeAuthzUpstreamSelectionWarning, []string{}) + + message := fmt.Sprintf( + "spec.incomingAuth.authzConfig.inline.primaryUpstreamProvider=%q does not "+ + "match any upstream declared on spec.authServerConfig.upstreamProviders. "+ + "Set primaryUpstreamProvider to one of the configured upstream names, or "+ + "leave it empty to default to the first upstream.", + explicitProvider, + ) + + ctxLogger := log.FromContext(ctx) + ctxLogger.Info("authz primaryUpstreamProvider does not match any upstream; rejecting VirtualMCPServer", + "name", vmcp.Name, + "namespace", vmcp.Namespace, + "primaryUpstreamProvider", explicitProvider, + "reason", mcpv1beta1.ConditionReasonAuthzUpstreamUnknown, + ) + + statusManager.SetPhase(mcpv1beta1.VirtualMCPServerPhaseFailed) + statusManager.SetMessage(message) + statusManager.SetAuthServerConfigValidatedCondition( + mcpv1beta1.ConditionReasonAuthzUpstreamUnknown, + message, + metav1.ConditionFalse, + ) + statusManager.SetObservedGeneration(vmcp.Generation) + return &SpecValidationError{ + Message: fmt.Sprintf( + "authz primaryUpstreamProvider %q does not match any configured upstream", + explicitProvider, + ), + } + } + } + + // Valid configuration. When multiple upstreams are declared AND the user has + // not pinned a choice via primaryUpstreamProvider, surface an advisory naming + // the auto-selected upstream so the operator can reorder or set the explicit + // field. Otherwise — single upstream, or an explicit choice that disambiguates + // the multi-upstream case — ensure any stale warning is cleared. + if len(vmcp.Spec.AuthServerConfig.UpstreamProviders) > 1 && explicitProvider == "" { selected := vmcp.Spec.AuthServerConfig.UpstreamProviders[0].Name statusManager.SetCondition( mcpv1beta1.ConditionTypeAuthzUpstreamSelectionWarning, @@ -632,7 +740,8 @@ func (*VirtualMCPServerReconciler) validateAuthzUpstreamAvailable( fmt.Sprintf( "multiple upstreamProviders configured; Cedar policies will evaluate "+ "claims from the first upstream (%q). If another upstream should be "+ - "authoritative, remove or reorder the list.", + "authoritative, set spec.incomingAuth.authzConfig.inline."+ + "primaryUpstreamProvider explicitly, or remove or reorder the list.", selected, ), metav1.ConditionTrue, diff --git a/cmd/thv-operator/controllers/virtualmcpserver_controller_test.go b/cmd/thv-operator/controllers/virtualmcpserver_controller_test.go index 3a918bcbfa..8b1580440b 100644 --- a/cmd/thv-operator/controllers/virtualmcpserver_controller_test.go +++ b/cmd/thv-operator/controllers/virtualmcpserver_controller_test.go @@ -3544,6 +3544,18 @@ func TestVirtualMCPServerValidateAuthzUpstreamAvailable(t *testing.T) { }, } + // authzRefWithPrimary builds an inline authz ref with an explicit + // PrimaryUpstreamProvider — used to exercise the override branch. + authzRefWithPrimary := func(primary string) *mcpv1beta1.AuthzConfigRef { + return &mcpv1beta1.AuthzConfigRef{ + Type: "inline", + Inline: &mcpv1beta1.InlineAuthzConfig{ + Policies: []string{`permit(principal, action, resource);`}, + PrimaryUpstreamProvider: primary, + }, + } + } + // warningExpectation captures the expected state of the advisory // AuthzUpstreamSelectionWarning condition after validation. When // expectPresent is false the condition must not appear in status at @@ -3633,6 +3645,77 @@ func TestVirtualMCPServerValidateAuthzUpstreamAvailable(t *testing.T) { messageSubstr: `"okta"`, }, }, + { + // Explicit PrimaryUpstreamProvider matching one of the upstreams is + // valid and emits no advisory — the user has disambiguated the choice. + name: "explicit primary provider matching an upstream is valid", + incomingAuth: &mcpv1beta1.IncomingAuthConfig{ + Type: "oidc", + AuthzConfig: authzRefWithPrimary("entra"), + }, + authServerConfig: &mcpv1beta1.EmbeddedAuthServerConfig{ + Issuer: "https://authserver.example.com", + UpstreamProviders: []mcpv1beta1.UpstreamProviderConfig{ + {Name: "okta", Type: mcpv1beta1.UpstreamProviderTypeOIDC}, + {Name: "entra", Type: mcpv1beta1.UpstreamProviderTypeOIDC}, + }, + }, + expectedWarning: warningExpectation{expectPresent: false}, + }, + { + // Explicit PrimaryUpstreamProvider with multiple upstreams suppresses + // the advisory warning — auto-selection is no longer happening. + name: "explicit primary provider suppresses multi-upstream advisory", + incomingAuth: &mcpv1beta1.IncomingAuthConfig{ + Type: "oidc", + AuthzConfig: authzRefWithPrimary("okta"), + }, + authServerConfig: &mcpv1beta1.EmbeddedAuthServerConfig{ + Issuer: "https://authserver.example.com", + UpstreamProviders: []mcpv1beta1.UpstreamProviderConfig{ + {Name: "okta", Type: mcpv1beta1.UpstreamProviderTypeOIDC}, + {Name: "entra", Type: mcpv1beta1.UpstreamProviderTypeOIDC}, + {Name: "google", Type: mcpv1beta1.UpstreamProviderTypeOIDC}, + }, + }, + expectedWarning: warningExpectation{expectPresent: false}, + }, + { + // Explicit PrimaryUpstreamProvider that does not match any declared + // upstream is rejected at admission. Cedar would otherwise deny every + // request at runtime; failing loudly is the right behavior. + name: "explicit primary provider not matching any upstream is invalid", + incomingAuth: &mcpv1beta1.IncomingAuthConfig{ + Type: "oidc", + AuthzConfig: authzRefWithPrimary("ping"), + }, + authServerConfig: &mcpv1beta1.EmbeddedAuthServerConfig{ + Issuer: "https://authserver.example.com", + UpstreamProviders: []mcpv1beta1.UpstreamProviderConfig{ + {Name: "okta", Type: mcpv1beta1.UpstreamProviderTypeOIDC}, + {Name: "entra", Type: mcpv1beta1.UpstreamProviderTypeOIDC}, + }, + }, + expectError: true, + expectedReason: mcpv1beta1.ConditionReasonAuthzUpstreamUnknown, + expectedWarning: warningExpectation{expectPresent: false}, + }, + { + // Explicit PrimaryUpstreamProvider with no embedded auth server at + // all is rejected at admission. The field names an upstream IDP on + // the embedded AS — without an AS there is nothing for it to refer + // to, and the converter would otherwise forward an unresolvable + // name. Same condition reason as the upstream-mismatch case. + name: "explicit primary provider without embedded auth server is invalid", + incomingAuth: &mcpv1beta1.IncomingAuthConfig{ + Type: "oidc", + AuthzConfig: authzRefWithPrimary("okta"), + }, + authServerConfig: nil, + expectError: true, + expectedReason: mcpv1beta1.ConditionReasonAuthzUpstreamUnknown, + expectedWarning: warningExpectation{expectPresent: false}, + }, } for _, tt := range tests { diff --git a/cmd/thv-operator/pkg/vmcpconfig/converter.go b/cmd/thv-operator/pkg/vmcpconfig/converter.go index cd31af6d69..c2a1db4f9f 100644 --- a/cmd/thv-operator/pkg/vmcpconfig/converter.go +++ b/cmd/thv-operator/pkg/vmcpconfig/converter.go @@ -198,7 +198,23 @@ func (c *Converter) convertIncomingAuth( // injectUpstreamProviderIfNeeded in pkg/runner/middleware.go (thv run path). // Leaving PrimaryUpstreamProvider empty (no embedded AS or no upstreams) lets // Cedar fall back to claims from the ToolHive-issued token. - if vmcp.Spec.AuthServerConfig != nil && len(vmcp.Spec.AuthServerConfig.UpstreamProviders) > 0 { + // + // When the user has set spec.incomingAuth.authzConfig.inline.primaryUpstreamProvider + // explicitly, honor it (after normalization). Otherwise fall back to the first + // configured upstream — matching the SubjectProviderName precedent on the + // token-exchange and AWS-STS strategies. The validator at + // validateAuthzUpstreamAvailable rejects an explicit name that does not match + // any declared upstream, and also rejects an explicit name when no embedded + // auth server is configured at all, so by the time we reach this branch the + // value resolves to a real upstream on the embedded AS. + // TODO: load primaryUpstreamProvider from configMap + switch { + case vmcp.Spec.IncomingAuth.AuthzConfig.Inline != nil && + vmcp.Spec.IncomingAuth.AuthzConfig.Inline.PrimaryUpstreamProvider != "": + incoming.Authz.PrimaryUpstreamProvider = authserver.ResolveUpstreamName( + vmcp.Spec.IncomingAuth.AuthzConfig.Inline.PrimaryUpstreamProvider, + ) + case vmcp.Spec.AuthServerConfig != nil && len(vmcp.Spec.AuthServerConfig.UpstreamProviders) > 0: incoming.Authz.PrimaryUpstreamProvider = authserver.ResolveUpstreamName( vmcp.Spec.AuthServerConfig.UpstreamProviders[0].Name, ) diff --git a/cmd/thv-operator/pkg/vmcpconfig/converter_test.go b/cmd/thv-operator/pkg/vmcpconfig/converter_test.go index cee72256af..ba5a99f74c 100644 --- a/cmd/thv-operator/pkg/vmcpconfig/converter_test.go +++ b/cmd/thv-operator/pkg/vmcpconfig/converter_test.go @@ -1898,16 +1898,22 @@ func TestConverter_TelemetryConfigRef(t *testing.T) { // propagates the first configured upstream provider name into AuthzConfig so Cedar // evaluates claims from the upstream IDP token rather than the ToolHive-issued // AS token. Without this, policies referencing upstream claims (e.g. "department") -// fail at runtime because Cedar reads the wrong token. +// fail at runtime because Cedar reads the wrong token. Also verifies that the +// user-supplied spec.incomingAuth.authzConfig.inline.primaryUpstreamProvider +// overrides the auto-selected first upstream when set. func TestConvertIncomingAuth_PrimaryUpstreamProvider(t *testing.T) { t.Parallel() - inlineAuthzRef := &mcpv1beta1.AuthzConfigRef{ - Type: "inline", - Inline: &mcpv1beta1.InlineAuthzConfig{ - Policies: []string{`permit(principal, action, resource);`}, - }, + authzWith := func(primary string) *mcpv1beta1.AuthzConfigRef { + return &mcpv1beta1.AuthzConfigRef{ + Type: "inline", + Inline: &mcpv1beta1.InlineAuthzConfig{ + Policies: []string{`permit(principal, action, resource);`}, + PrimaryUpstreamProvider: primary, + }, + } } + inlineAuthzRef := authzWith("") tests := []struct { name string @@ -1986,6 +1992,63 @@ func TestConvertIncomingAuth_PrimaryUpstreamProvider(t *testing.T) { authzConfig: nil, expectAuthzNil: true, }, + { + // Explicit primaryUpstreamProvider with a single upstream is honored + // (and matches it). Validates the explicit branch is taken at all. + name: "explicit primary provider with single upstream is honored", + authServerConfig: &mcpv1beta1.EmbeddedAuthServerConfig{ + Issuer: "https://authserver.example.com", + UpstreamProviders: []mcpv1beta1.UpstreamProviderConfig{ + {Name: "okta", Type: mcpv1beta1.UpstreamProviderTypeOIDC}, + }, + }, + authzConfig: authzWith("okta"), + expectedProvider: "okta", + }, + { + // Explicit primaryUpstreamProvider overrides the auto-selected first + // upstream when multiple are configured. This is the core feature. + name: "explicit primary provider overrides first of multiple upstreams", + authServerConfig: &mcpv1beta1.EmbeddedAuthServerConfig{ + Issuer: "https://authserver.example.com", + UpstreamProviders: []mcpv1beta1.UpstreamProviderConfig{ + {Name: "okta", Type: mcpv1beta1.UpstreamProviderTypeOIDC}, + {Name: "github", Type: mcpv1beta1.UpstreamProviderTypeOAuth2}, + {Name: "google", Type: mcpv1beta1.UpstreamProviderTypeOIDC}, + }, + }, + authzConfig: authzWith("github"), + expectedProvider: "github", + }, + { + // Exercises the actual normalization step inside ResolveUpstreamName: + // the upstream is declared with Name:"" (which resolves to "default") + // and the user pins primaryUpstreamProvider to "default". The explicit + // branch must forward "default" — exercising both the explicit path + // and the resolver's empty-input handling. The previous "okta -> okta" + // case did not exercise normalization because ResolveUpstreamName is + // the identity function for non-empty input. + name: "explicit primary provider 'default' resolves to default upstream", + authServerConfig: &mcpv1beta1.EmbeddedAuthServerConfig{ + Issuer: "https://authserver.example.com", + UpstreamProviders: []mcpv1beta1.UpstreamProviderConfig{ + {Name: "", Type: mcpv1beta1.UpstreamProviderTypeOIDC}, + }, + }, + authzConfig: authzWith("default"), + expectedProvider: "default", + }, + { + // Explicit primary provider is honored even without an embedded AS + // configured on the spec. The validator rejects this combination + // (covered by TestVirtualMCPServerValidateAuthzUpstreamAvailable), + // but the converter still forwards the explicit value as a defined + // contract — this case locks that contract in. + name: "explicit primary provider without auth server is forwarded", + authServerConfig: nil, + authzConfig: authzWith("okta"), + expectedProvider: "okta", + }, } for _, tt := range tests { diff --git a/deploy/charts/operator-crds/files/crds/toolhive.stacklok.dev_mcpremoteproxies.yaml b/deploy/charts/operator-crds/files/crds/toolhive.stacklok.dev_mcpremoteproxies.yaml index 915417ddd9..869e3a666a 100644 --- a/deploy/charts/operator-crds/files/crds/toolhive.stacklok.dev_mcpremoteproxies.yaml +++ b/deploy/charts/operator-crds/files/crds/toolhive.stacklok.dev_mcpremoteproxies.yaml @@ -131,6 +131,16 @@ spec: minItems: 1 type: array x-kubernetes-list-type: atomic + primaryUpstreamProvider: + description: |- + PrimaryUpstreamProvider names the upstream IDP whose access token's claims + Cedar should evaluate. Only meaningful for VirtualMCPServer with an embedded + auth server. When empty and an embedded auth server has upstreams configured, + the controller defaults to the first upstream provider. Ignored by MCPServer + and MCPRemoteProxy. The name must match one of the upstreams declared on + spec.authServerConfig.upstreamProviders; otherwise the VirtualMCPServer is + rejected with AuthServerConfigValidated=False. + type: string required: - policies type: object @@ -697,6 +707,16 @@ spec: minItems: 1 type: array x-kubernetes-list-type: atomic + primaryUpstreamProvider: + description: |- + PrimaryUpstreamProvider names the upstream IDP whose access token's claims + Cedar should evaluate. Only meaningful for VirtualMCPServer with an embedded + auth server. When empty and an embedded auth server has upstreams configured, + the controller defaults to the first upstream provider. Ignored by MCPServer + and MCPRemoteProxy. The name must match one of the upstreams declared on + spec.authServerConfig.upstreamProviders; otherwise the VirtualMCPServer is + rejected with AuthServerConfigValidated=False. + type: string required: - policies type: object diff --git a/deploy/charts/operator-crds/files/crds/toolhive.stacklok.dev_mcpservers.yaml b/deploy/charts/operator-crds/files/crds/toolhive.stacklok.dev_mcpservers.yaml index 3be75f8a63..3dacc23315 100644 --- a/deploy/charts/operator-crds/files/crds/toolhive.stacklok.dev_mcpservers.yaml +++ b/deploy/charts/operator-crds/files/crds/toolhive.stacklok.dev_mcpservers.yaml @@ -138,6 +138,16 @@ spec: minItems: 1 type: array x-kubernetes-list-type: atomic + primaryUpstreamProvider: + description: |- + PrimaryUpstreamProvider names the upstream IDP whose access token's claims + Cedar should evaluate. Only meaningful for VirtualMCPServer with an embedded + auth server. When empty and an embedded auth server has upstreams configured, + the controller defaults to the first upstream provider. Ignored by MCPServer + and MCPRemoteProxy. The name must match one of the upstreams declared on + spec.authServerConfig.upstreamProviders; otherwise the VirtualMCPServer is + rejected with AuthServerConfigValidated=False. + type: string required: - policies type: object @@ -1002,6 +1012,16 @@ spec: minItems: 1 type: array x-kubernetes-list-type: atomic + primaryUpstreamProvider: + description: |- + PrimaryUpstreamProvider names the upstream IDP whose access token's claims + Cedar should evaluate. Only meaningful for VirtualMCPServer with an embedded + auth server. When empty and an embedded auth server has upstreams configured, + the controller defaults to the first upstream provider. Ignored by MCPServer + and MCPRemoteProxy. The name must match one of the upstreams declared on + spec.authServerConfig.upstreamProviders; otherwise the VirtualMCPServer is + rejected with AuthServerConfigValidated=False. + type: string required: - policies type: object diff --git a/deploy/charts/operator-crds/files/crds/toolhive.stacklok.dev_virtualmcpservers.yaml b/deploy/charts/operator-crds/files/crds/toolhive.stacklok.dev_virtualmcpservers.yaml index a51fe4b5bd..42e0e0be3f 100644 --- a/deploy/charts/operator-crds/files/crds/toolhive.stacklok.dev_virtualmcpservers.yaml +++ b/deploy/charts/operator-crds/files/crds/toolhive.stacklok.dev_virtualmcpservers.yaml @@ -2089,6 +2089,16 @@ spec: minItems: 1 type: array x-kubernetes-list-type: atomic + primaryUpstreamProvider: + description: |- + PrimaryUpstreamProvider names the upstream IDP whose access token's claims + Cedar should evaluate. Only meaningful for VirtualMCPServer with an embedded + auth server. When empty and an embedded auth server has upstreams configured, + the controller defaults to the first upstream provider. Ignored by MCPServer + and MCPRemoteProxy. The name must match one of the upstreams declared on + spec.authServerConfig.upstreamProviders; otherwise the VirtualMCPServer is + rejected with AuthServerConfigValidated=False. + type: string required: - policies type: object @@ -4585,6 +4595,16 @@ spec: minItems: 1 type: array x-kubernetes-list-type: atomic + primaryUpstreamProvider: + description: |- + PrimaryUpstreamProvider names the upstream IDP whose access token's claims + Cedar should evaluate. Only meaningful for VirtualMCPServer with an embedded + auth server. When empty and an embedded auth server has upstreams configured, + the controller defaults to the first upstream provider. Ignored by MCPServer + and MCPRemoteProxy. The name must match one of the upstreams declared on + spec.authServerConfig.upstreamProviders; otherwise the VirtualMCPServer is + rejected with AuthServerConfigValidated=False. + type: string required: - policies type: object diff --git a/deploy/charts/operator-crds/templates/toolhive.stacklok.dev_mcpremoteproxies.yaml b/deploy/charts/operator-crds/templates/toolhive.stacklok.dev_mcpremoteproxies.yaml index 79154701fb..d58349e312 100644 --- a/deploy/charts/operator-crds/templates/toolhive.stacklok.dev_mcpremoteproxies.yaml +++ b/deploy/charts/operator-crds/templates/toolhive.stacklok.dev_mcpremoteproxies.yaml @@ -134,6 +134,16 @@ spec: minItems: 1 type: array x-kubernetes-list-type: atomic + primaryUpstreamProvider: + description: |- + PrimaryUpstreamProvider names the upstream IDP whose access token's claims + Cedar should evaluate. Only meaningful for VirtualMCPServer with an embedded + auth server. When empty and an embedded auth server has upstreams configured, + the controller defaults to the first upstream provider. Ignored by MCPServer + and MCPRemoteProxy. The name must match one of the upstreams declared on + spec.authServerConfig.upstreamProviders; otherwise the VirtualMCPServer is + rejected with AuthServerConfigValidated=False. + type: string required: - policies type: object @@ -700,6 +710,16 @@ spec: minItems: 1 type: array x-kubernetes-list-type: atomic + primaryUpstreamProvider: + description: |- + PrimaryUpstreamProvider names the upstream IDP whose access token's claims + Cedar should evaluate. Only meaningful for VirtualMCPServer with an embedded + auth server. When empty and an embedded auth server has upstreams configured, + the controller defaults to the first upstream provider. Ignored by MCPServer + and MCPRemoteProxy. The name must match one of the upstreams declared on + spec.authServerConfig.upstreamProviders; otherwise the VirtualMCPServer is + rejected with AuthServerConfigValidated=False. + type: string required: - policies type: object diff --git a/deploy/charts/operator-crds/templates/toolhive.stacklok.dev_mcpservers.yaml b/deploy/charts/operator-crds/templates/toolhive.stacklok.dev_mcpservers.yaml index 89b0a20107..109ddd08e6 100644 --- a/deploy/charts/operator-crds/templates/toolhive.stacklok.dev_mcpservers.yaml +++ b/deploy/charts/operator-crds/templates/toolhive.stacklok.dev_mcpservers.yaml @@ -141,6 +141,16 @@ spec: minItems: 1 type: array x-kubernetes-list-type: atomic + primaryUpstreamProvider: + description: |- + PrimaryUpstreamProvider names the upstream IDP whose access token's claims + Cedar should evaluate. Only meaningful for VirtualMCPServer with an embedded + auth server. When empty and an embedded auth server has upstreams configured, + the controller defaults to the first upstream provider. Ignored by MCPServer + and MCPRemoteProxy. The name must match one of the upstreams declared on + spec.authServerConfig.upstreamProviders; otherwise the VirtualMCPServer is + rejected with AuthServerConfigValidated=False. + type: string required: - policies type: object @@ -1005,6 +1015,16 @@ spec: minItems: 1 type: array x-kubernetes-list-type: atomic + primaryUpstreamProvider: + description: |- + PrimaryUpstreamProvider names the upstream IDP whose access token's claims + Cedar should evaluate. Only meaningful for VirtualMCPServer with an embedded + auth server. When empty and an embedded auth server has upstreams configured, + the controller defaults to the first upstream provider. Ignored by MCPServer + and MCPRemoteProxy. The name must match one of the upstreams declared on + spec.authServerConfig.upstreamProviders; otherwise the VirtualMCPServer is + rejected with AuthServerConfigValidated=False. + type: string required: - policies type: object diff --git a/deploy/charts/operator-crds/templates/toolhive.stacklok.dev_virtualmcpservers.yaml b/deploy/charts/operator-crds/templates/toolhive.stacklok.dev_virtualmcpservers.yaml index 6078670479..28ef8b5bdd 100644 --- a/deploy/charts/operator-crds/templates/toolhive.stacklok.dev_virtualmcpservers.yaml +++ b/deploy/charts/operator-crds/templates/toolhive.stacklok.dev_virtualmcpservers.yaml @@ -2092,6 +2092,16 @@ spec: minItems: 1 type: array x-kubernetes-list-type: atomic + primaryUpstreamProvider: + description: |- + PrimaryUpstreamProvider names the upstream IDP whose access token's claims + Cedar should evaluate. Only meaningful for VirtualMCPServer with an embedded + auth server. When empty and an embedded auth server has upstreams configured, + the controller defaults to the first upstream provider. Ignored by MCPServer + and MCPRemoteProxy. The name must match one of the upstreams declared on + spec.authServerConfig.upstreamProviders; otherwise the VirtualMCPServer is + rejected with AuthServerConfigValidated=False. + type: string required: - policies type: object @@ -4588,6 +4598,16 @@ spec: minItems: 1 type: array x-kubernetes-list-type: atomic + primaryUpstreamProvider: + description: |- + PrimaryUpstreamProvider names the upstream IDP whose access token's claims + Cedar should evaluate. Only meaningful for VirtualMCPServer with an embedded + auth server. When empty and an embedded auth server has upstreams configured, + the controller defaults to the first upstream provider. Ignored by MCPServer + and MCPRemoteProxy. The name must match one of the upstreams declared on + spec.authServerConfig.upstreamProviders; otherwise the VirtualMCPServer is + rejected with AuthServerConfigValidated=False. + type: string required: - policies type: object diff --git a/docs/operator/crd-api.md b/docs/operator/crd-api.md index e60f05015e..cf43767529 100644 --- a/docs/operator/crd-api.md +++ b/docs/operator/crd-api.md @@ -1415,6 +1415,7 @@ _Appears in:_ | --- | --- | --- | --- | | `policies` _string array_ | Policies is a list of Cedar policy strings | | MinItems: 1
Required: \{\}
| | `entitiesJson` _string_ | EntitiesJSON is a JSON string representing Cedar entities | [] | Optional: \{\}
| +| `primaryUpstreamProvider` _string_ | PrimaryUpstreamProvider names the upstream IDP whose access token's claims
Cedar should evaluate. Only meaningful for VirtualMCPServer with an embedded
auth server. When empty and an embedded auth server has upstreams configured,
the controller defaults to the first upstream provider. Ignored by MCPServer
and MCPRemoteProxy. The name must match one of the upstreams declared on
spec.authServerConfig.upstreamProviders; otherwise the VirtualMCPServer is
rejected with AuthServerConfigValidated=False. | | Optional: \{\}
| #### api.v1beta1.InlineOIDCSharedConfig