Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
332 changes: 325 additions & 7 deletions cmd/thv-operator/api/v1beta1/mcpexternalauthconfig_types.go

Large diffs are not rendered by default.

286 changes: 286 additions & 0 deletions cmd/thv-operator/api/v1beta1/mcpexternalauthconfig_types_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -929,6 +929,182 @@ func TestMCPExternalAuthConfig_validateEmbeddedAuthServer(t *testing.T) {
expectErr: true,
errMsg: "actor_matcher",
},
{
// validateEmbeddedAuthServer deliberately runs no Go-level
// permission-shaped SPIFFE pre-check (resources/scopes) at this
// layer — same precedent as DelegateClients: a meaningful
// resources/scopes check needs AllowedAudiences/ScopesSupported,
// which only exist once derived at reconcile time. (It does run
// the admission-time-safe bundle-URL and principal-overlap
// checks below — see validateSPIFFEBundleEndpoints and
// validateSPIFFEPrincipalPatternOverlap tests.) A resources
// entry and a non-default scope must therefore pass here even
// though they'd need revalidating once those derived values are
// known (see
// TestBuildAuthServerRunConfigInvalidSPIFFEIsTypedAndNotYetEnforced
// in controllerutil for the reconcile-time revalidation this
// relies on). CEL (spiffe_cel_test.go) covers structural
// correctness (trust-domain refs, method subsets, etc.) at
// admission.
name: "spiffe client with resources and custom scope - valid at this layer",
config: &MCPExternalAuthConfig{
Spec: MCPExternalAuthConfigSpec{
Type: ExternalAuthTypeEmbeddedAuthServer,
EmbeddedAuthServer: &EmbeddedAuthServerConfig{
Issuer: "https://auth.example.com",
UpstreamProviders: []UpstreamProviderConfig{{
Name: "github",
Type: UpstreamProviderTypeOIDC,
OIDCConfig: &OIDCUpstreamConfig{IssuerURL: "https://github.com", ClientID: "client-id"},
}},
SPIFFETrustDomains: []SPIFFETrustDomainConfig{{
Name: "example", TrustDomain: "example.org",
Methods: []SPIFFEAuthenticationMethod{SPIFFEAuthenticationMethodX509},
BundleSource: SPIFFEBundleSourceConfig{
Type: SPIFFEBundleSourceTypeWorkloadAPI,
WorkloadAPI: &SPIFFEWorkloadAPIBundleSourceConfig{},
},
}},
InboundGrants: &InboundGrantsConfig{
SPIFFEClientAuth: []SPIFFEClientConfig{{
TrustDomainRef: "example",
PrincipalPattern: "spiffe://example.org/ns/default/agent",
ClientID: "spiffe-client",
Methods: []SPIFFEAuthenticationMethod{SPIFFEAuthenticationMethodX509},
// Not in allowed_audiences and not a default
// scope — would be rejected if the removed
// admission-time pre-check fabricated a nil
// allowlist/default-scopes set instead of
// deferring to reconcile time.
Resources: []string{"https://backend.example.com"},
Audiences: []string{"https://mcp.example.com"},
Scopes: []string{"custom:scope"},
}},
},
},
},
},
expectErr: false,
},
{
name: "spiffe bundle-endpoint URL rejects non-https scheme",
config: mustEmbeddedAuthServerConfigWithBundleEndpoint("http://bundle.example.com"),
expectErr: true,
errMsg: "must be an absolute HTTPS URL",
},
{
name: "spiffe bundle-endpoint URL rejects userinfo",
config: mustEmbeddedAuthServerConfigWithBundleEndpoint("https://user:pass@bundle.example.com"),
expectErr: true,
errMsg: "must not contain credentials",
},
{
name: "spiffe bundle-endpoint URL rejects a query string",
config: mustEmbeddedAuthServerConfigWithBundleEndpoint("https://bundle.example.com?x=1"),
expectErr: true,
errMsg: "must not contain credentials",
},
{
name: "spiffe bundle-endpoint URL rejects a fragment",
config: mustEmbeddedAuthServerConfigWithBundleEndpoint("https://bundle.example.com/bundle#frag"),
expectErr: true,
errMsg: "must not contain credentials",
},
{
name: "spiffe bundle-endpoint URL rejects an IP-literal host",
config: mustEmbeddedAuthServerConfigWithBundleEndpoint("https://192.0.2.1"),
expectErr: true,
errMsg: "must not contain credentials",
},
{
name: "spiffe bundle-endpoint URL rejects the localhost host",
config: mustEmbeddedAuthServerConfigWithBundleEndpoint("https://localhost"),
expectErr: true,
errMsg: "must not contain credentials",
},
{
name: "spiffe bundle-endpoint URL rejects the 127.0.0.1 loopback host",
config: mustEmbeddedAuthServerConfigWithBundleEndpoint("https://127.0.0.1"),
expectErr: true,
errMsg: "must not contain credentials",
},
{
name: "spiffe bundle-endpoint URL rejects the [::1] loopback host",
config: mustEmbeddedAuthServerConfigWithBundleEndpoint("https://[::1]"),
expectErr: true,
errMsg: "must not contain credentials",
},
{
name: "spiffe bundle-endpoint URL accepts a valid https URL",
config: mustEmbeddedAuthServerConfigWithBundleEndpoint("https://bundle.example.com"),
expectErr: false,
},
{
name: "spiffe principal patterns reject an identical duplicate pair",
config: mustEmbeddedAuthServerConfigWithPrincipalPatterns(
"spiffe://example.org/ns/default/agent", "spiffe://example.org/ns/default/agent",
),
expectErr: true,
errMsg: "overlaps",
},
{
name: "spiffe principal patterns reject a wildcard overlapping a concrete principal",
config: mustEmbeddedAuthServerConfigWithPrincipalPatterns(
"spiffe://example.org/agent/*", "spiffe://example.org/agent/one",
),
expectErr: true,
errMsg: "overlaps",
},
{
name: "spiffe principal patterns reject two overlapping wildcards",
config: mustEmbeddedAuthServerConfigWithPrincipalPatterns(
"spiffe://example.org/agent/*", "spiffe://example.org/agent/one/*",
),
expectErr: true,
errMsg: "overlaps",
},
{
name: "spiffe principal patterns accept a genuinely non-overlapping pair",
config: mustEmbeddedAuthServerConfigWithPrincipalPatterns(
"spiffe://example.org/agent/*", "spiffe://example.org/other/*",
),
expectErr: false,
},
{
// Regression case: when only the SECOND entry in a pair is
// malformed, the error must name index 1, not index 0 — a
// pairwise loop that always blames the first entry in the pair
// it happens to be comparing would get this wrong.
name: "spiffe principal patterns blame the correct index when the second entry is malformed",
config: mustEmbeddedAuthServerConfigWithPrincipalPatterns(
"spiffe://example.org/agent/one", "spiffe://example.org/agent~2",
),
expectErr: true,
errMsg: "spiffeClientAuth[1].principalPattern",
},
{
name: "spiffe resource indicator rejects an empty-host authority",
config: mustEmbeddedAuthServerConfigWithResource("https://:443/resource"),
expectErr: true,
errMsg: "must be an absolute HTTP(S) URI",
},
{
name: "spiffe resource indicator rejects userinfo",
config: mustEmbeddedAuthServerConfigWithResource("https://user@mcp.example.com/resource"),
expectErr: true,
errMsg: "must be an absolute HTTP(S) URI",
},
{
name: "spiffe resource indicator rejects a fragment",
config: mustEmbeddedAuthServerConfigWithResource("https://mcp.example.com/resource#fragment"),
expectErr: true,
errMsg: "must be an absolute HTTP(S) URI",
},
{
name: "spiffe resource indicator accepts a well-formed URI",
config: mustEmbeddedAuthServerConfigWithResource("https://mcp.example.com/resource"),
expectErr: false,
},
}

for _, tt := range tests {
Expand Down Expand Up @@ -1070,6 +1246,116 @@ func TestEmbeddedAuthServerConfig_ValidateInboundGrants_DuplicateCredentialIssue
}
}

// mustEmbeddedAuthServerConfigWithBundleEndpoint builds a minimal valid
// MCPExternalAuthConfig with a single spiffeTrustDomains entry whose
// bundleSource is a bundle_endpoint with the given URL (always using the
// https_web profile), for exercising validateSPIFFEBundleEndpoints in
// isolation.
func mustEmbeddedAuthServerConfigWithBundleEndpoint(url string) *MCPExternalAuthConfig {
return &MCPExternalAuthConfig{
Spec: MCPExternalAuthConfigSpec{
Type: ExternalAuthTypeEmbeddedAuthServer,
EmbeddedAuthServer: &EmbeddedAuthServerConfig{
Issuer: "https://auth.example.com",
UpstreamProviders: []UpstreamProviderConfig{{
Name: "github",
Type: UpstreamProviderTypeOIDC,
OIDCConfig: &OIDCUpstreamConfig{IssuerURL: "https://github.com", ClientID: "client-id"},
}},
SPIFFETrustDomains: []SPIFFETrustDomainConfig{{
Name: "example", TrustDomain: "example.org",
Methods: []SPIFFEAuthenticationMethod{SPIFFEAuthenticationMethodX509},
BundleSource: SPIFFEBundleSourceConfig{
Type: SPIFFEBundleSourceTypeEndpoint,
Endpoint: &SPIFFEBundleEndpointSourceConfig{
URL: url, Profile: SPIFFEBundleEndpointProfileHTTPSWeb,
},
},
}},
},
},
}
}

// mustEmbeddedAuthServerConfigWithPrincipalPatterns builds a minimal valid
// MCPExternalAuthConfig with two spiffeClientAuth entries using the given
// principal patterns, for exercising validateSPIFFEPrincipalPatternOverlap
// in isolation.
func mustEmbeddedAuthServerConfigWithPrincipalPatterns(first, second string) *MCPExternalAuthConfig {
return &MCPExternalAuthConfig{
Spec: MCPExternalAuthConfigSpec{
Type: ExternalAuthTypeEmbeddedAuthServer,
EmbeddedAuthServer: &EmbeddedAuthServerConfig{
Issuer: "https://auth.example.com",
UpstreamProviders: []UpstreamProviderConfig{{
Name: "github",
Type: UpstreamProviderTypeOIDC,
OIDCConfig: &OIDCUpstreamConfig{IssuerURL: "https://github.com", ClientID: "client-id"},
}},
SPIFFETrustDomains: []SPIFFETrustDomainConfig{{
Name: "example", TrustDomain: "example.org",
Methods: []SPIFFEAuthenticationMethod{SPIFFEAuthenticationMethodX509},
BundleSource: SPIFFEBundleSourceConfig{
Type: SPIFFEBundleSourceTypeWorkloadAPI,
WorkloadAPI: &SPIFFEWorkloadAPIBundleSourceConfig{},
},
}},
InboundGrants: &InboundGrantsConfig{
SPIFFEClientAuth: []SPIFFEClientConfig{
{
TrustDomainRef: "example", PrincipalPattern: first, ClientID: "spiffe-client-1",
Methods: []SPIFFEAuthenticationMethod{SPIFFEAuthenticationMethodX509},
Audiences: []string{"https://mcp.example.com"}, Scopes: []string{"openid"},
},
{
TrustDomainRef: "example", PrincipalPattern: second, ClientID: "spiffe-client-2",
Methods: []SPIFFEAuthenticationMethod{SPIFFEAuthenticationMethodX509},
Audiences: []string{"https://mcp.example.com"}, Scopes: []string{"openid"},
},
},
},
},
},
}
}

// mustEmbeddedAuthServerConfigWithResource builds a minimal valid
// MCPExternalAuthConfig with a single spiffeClientAuth entry carrying the
// given resource indicator, for exercising validateSPIFFEResourceShapes in
// isolation.
func mustEmbeddedAuthServerConfigWithResource(resource string) *MCPExternalAuthConfig {
return &MCPExternalAuthConfig{
Spec: MCPExternalAuthConfigSpec{
Type: ExternalAuthTypeEmbeddedAuthServer,
EmbeddedAuthServer: &EmbeddedAuthServerConfig{
Issuer: "https://auth.example.com",
UpstreamProviders: []UpstreamProviderConfig{{
Name: "github",
Type: UpstreamProviderTypeOIDC,
OIDCConfig: &OIDCUpstreamConfig{IssuerURL: "https://github.com", ClientID: "client-id"},
}},
SPIFFETrustDomains: []SPIFFETrustDomainConfig{{
Name: "example", TrustDomain: "example.org",
Methods: []SPIFFEAuthenticationMethod{SPIFFEAuthenticationMethodX509},
BundleSource: SPIFFEBundleSourceConfig{
Type: SPIFFEBundleSourceTypeWorkloadAPI,
WorkloadAPI: &SPIFFEWorkloadAPIBundleSourceConfig{},
},
}},
InboundGrants: &InboundGrantsConfig{
SPIFFEClientAuth: []SPIFFEClientConfig{{
TrustDomainRef: "example", PrincipalPattern: "spiffe://example.org/ns/default/agent",
ClientID: "spiffe-client",
Methods: []SPIFFEAuthenticationMethod{SPIFFEAuthenticationMethodX509},
Audiences: []string{"https://mcp.example.com"}, Scopes: []string{"openid"},
Resources: []string{resource},
}},
},
},
},
}
}

func TestMCPExternalAuthConfig_ZeroUpstreamAlternatives(t *testing.T) {
t.Parallel()

Expand Down
Loading
Loading