Skip to content

Commit b5d2821

Browse files
committed
Address code review feedback
Fixed issues from code review: - MEDIUM: Reject explicit primaryUpstreamProvider when no embedded auth server is configured. The early-return direct-IdP branch in validateAuthzUpstreamAvailable now checks for a non-empty explicit name first and returns SpecValidationError with ConditionReasonAuthzUpstreamUnknown when set — closing the silent misconfiguration where the converter would forward an unresolvable name into Cedar config at runtime. - MEDIUM: Update the converter block comment so it accurately describes both rejection paths (mismatch with declared upstreams AND explicit name without an embedded AS), keeping the comment synchronized with the validator's behavior per the go-style.md rule. - MEDIUM: Replace the misleading "is normalized via ResolveUpstreamName" converter test with a case that actually exercises normalization (upstream Name:"" resolving to "default", user pinning to "default"). Removes redundancy with the single-upstream-honored case and matches the test's claimed assertion. - MEDIUM: Add validator test case for explicit primaryUpstreamProvider with no embedded auth server, locking the new rejection in.
1 parent 84b062d commit b5d2821

4 files changed

Lines changed: 81 additions & 12 deletions

File tree

cmd/thv-operator/controllers/virtualmcpserver_controller.go

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -583,7 +583,52 @@ func (*VirtualMCPServerReconciler) validateAuthzUpstreamAvailable(
583583
// Direct-IdP flow: no embedded AS. Cedar evaluates against identity.Claims
584584
// populated by incoming OIDC middleware from the IdP token. No upstream
585585
// needed; nothing to warn about. Remove any stale condition.
586+
//
587+
// However, an explicit primaryUpstreamProvider is meaningless in this mode
588+
// — there is no upstream-token table for Cedar to look it up in — so the
589+
// converter would forward a name that cannot resolve at runtime. Reject at
590+
// admission for the same "fail loudly instead of denying every request"
591+
// reason as the configured-AS mismatch path below.
586592
if vmcp.Spec.AuthServerConfig == nil {
593+
explicitProvider := ""
594+
if vmcp.Spec.IncomingAuth.AuthzConfig.Inline != nil {
595+
explicitProvider = vmcp.Spec.IncomingAuth.AuthzConfig.Inline.PrimaryUpstreamProvider
596+
}
597+
if explicitProvider != "" {
598+
statusManager.RemoveConditionsWithPrefix(mcpv1beta1.ConditionTypeAuthzUpstreamSelectionWarning, []string{})
599+
600+
message := fmt.Sprintf(
601+
"spec.incomingAuth.authzConfig.inline.primaryUpstreamProvider=%q is set but "+
602+
"spec.authServerConfig is not configured. The field names an upstream IDP "+
603+
"on the embedded auth server, which is required for it to take effect. "+
604+
"Remove primaryUpstreamProvider, or configure spec.authServerConfig with "+
605+
"an upstream of that name.",
606+
explicitProvider,
607+
)
608+
609+
ctxLogger := log.FromContext(ctx)
610+
ctxLogger.Info("authz primaryUpstreamProvider set without an embedded auth server; rejecting VirtualMCPServer",
611+
"name", vmcp.Name,
612+
"namespace", vmcp.Namespace,
613+
"primaryUpstreamProvider", explicitProvider,
614+
"reason", mcpv1beta1.ConditionReasonAuthzUpstreamUnknown,
615+
)
616+
617+
statusManager.SetPhase(mcpv1beta1.VirtualMCPServerPhaseFailed)
618+
statusManager.SetMessage(message)
619+
statusManager.SetAuthServerConfigValidatedCondition(
620+
mcpv1beta1.ConditionReasonAuthzUpstreamUnknown,
621+
message,
622+
metav1.ConditionFalse,
623+
)
624+
statusManager.SetObservedGeneration(vmcp.Generation)
625+
return &SpecValidationError{
626+
Message: fmt.Sprintf(
627+
"authz primaryUpstreamProvider %q set without an embedded auth server",
628+
explicitProvider,
629+
),
630+
}
631+
}
587632
statusManager.RemoveConditionsWithPrefix(mcpv1beta1.ConditionTypeAuthzUpstreamSelectionWarning, []string{})
588633
return nil
589634
}

cmd/thv-operator/controllers/virtualmcpserver_controller_test.go

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3700,6 +3700,22 @@ func TestVirtualMCPServerValidateAuthzUpstreamAvailable(t *testing.T) {
37003700
expectedReason: mcpv1beta1.ConditionReasonAuthzUpstreamUnknown,
37013701
expectedWarning: warningExpectation{expectPresent: false},
37023702
},
3703+
{
3704+
// Explicit PrimaryUpstreamProvider with no embedded auth server at
3705+
// all is rejected at admission. The field names an upstream IDP on
3706+
// the embedded AS — without an AS there is nothing for it to refer
3707+
// to, and the converter would otherwise forward an unresolvable
3708+
// name. Same condition reason as the upstream-mismatch case.
3709+
name: "explicit primary provider without embedded auth server is invalid",
3710+
incomingAuth: &mcpv1beta1.IncomingAuthConfig{
3711+
Type: "oidc",
3712+
AuthzConfig: authzRefWithPrimary("okta"),
3713+
},
3714+
authServerConfig: nil,
3715+
expectError: true,
3716+
expectedReason: mcpv1beta1.ConditionReasonAuthzUpstreamUnknown,
3717+
expectedWarning: warningExpectation{expectPresent: false},
3718+
},
37033719
}
37043720

37053721
for _, tt := range tests {

cmd/thv-operator/pkg/vmcpconfig/converter.go

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -203,9 +203,11 @@ func (c *Converter) convertIncomingAuth(
203203
// explicitly, honor it (after normalization). Otherwise fall back to the first
204204
// configured upstream — matching the SubjectProviderName precedent on the
205205
// token-exchange and AWS-STS strategies. The validator at
206-
// validateAuthzUpstreamAvailable rejects names that don't match any declared
207-
// upstream, so by the time we reach this branch the explicit value is known
208-
// to resolve to a real upstream.
206+
// validateAuthzUpstreamAvailable rejects an explicit name that does not match
207+
// any declared upstream, and also rejects an explicit name when no embedded
208+
// auth server is configured at all, so by the time we reach this branch the
209+
// value resolves to a real upstream on the embedded AS.
210+
// TODO: load primaryUpstreamProvider from configMap
209211
switch {
210212
case vmcp.Spec.IncomingAuth.AuthzConfig.Inline != nil &&
211213
vmcp.Spec.IncomingAuth.AuthzConfig.Inline.PrimaryUpstreamProvider != "":

cmd/thv-operator/pkg/vmcpconfig/converter_test.go

Lines changed: 15 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2021,23 +2021,29 @@ func TestConvertIncomingAuth_PrimaryUpstreamProvider(t *testing.T) {
20212021
expectedProvider: "github",
20222022
},
20232023
{
2024-
// Explicit value flows through ResolveUpstreamName for normalization
2025-
// parity with the auto-select branch — though for non-empty input the
2026-
// resolver is the identity function. Keeps both paths normalized.
2027-
name: "explicit primary provider is normalized via ResolveUpstreamName",
2024+
// Exercises the actual normalization step inside ResolveUpstreamName:
2025+
// the upstream is declared with Name:"" (which resolves to "default")
2026+
// and the user pins primaryUpstreamProvider to "default". The explicit
2027+
// branch must forward "default" — exercising both the explicit path
2028+
// and the resolver's empty-input handling. The previous "okta -> okta"
2029+
// case did not exercise normalization because ResolveUpstreamName is
2030+
// the identity function for non-empty input.
2031+
name: "explicit primary provider 'default' resolves to default upstream",
20282032
authServerConfig: &mcpv1beta1.EmbeddedAuthServerConfig{
20292033
Issuer: "https://authserver.example.com",
20302034
UpstreamProviders: []mcpv1beta1.UpstreamProviderConfig{
2031-
{Name: "okta", Type: mcpv1beta1.UpstreamProviderTypeOIDC},
2035+
{Name: "", Type: mcpv1beta1.UpstreamProviderTypeOIDC},
20322036
},
20332037
},
2034-
authzConfig: authzWith("okta"),
2035-
expectedProvider: "okta",
2038+
authzConfig: authzWith("default"),
2039+
expectedProvider: "default",
20362040
},
20372041
{
20382042
// Explicit primary provider is honored even without an embedded AS
2039-
// configured on the spec. The validator catches the mismatch in this
2040-
// case; the converter's job is only to forward the explicit choice.
2043+
// configured on the spec. The validator rejects this combination
2044+
// (covered by TestVirtualMCPServerValidateAuthzUpstreamAvailable),
2045+
// but the converter still forwards the explicit value as a defined
2046+
// contract — this case locks that contract in.
20412047
name: "explicit primary provider without auth server is forwarded",
20422048
authServerConfig: nil,
20432049
authzConfig: authzWith("okta"),

0 commit comments

Comments
 (0)