-
Notifications
You must be signed in to change notification settings - Fork 191
Description
Description
Add upstream_inject startup validation rules V-01, V-02, and V-06 to pkg/vmcp/config/validator.go. These rules prevent misconfigured deployments from reaching runtime by rejecting upstream_inject backends that reference a missing auth server, reference an unknown upstream provider name, or omit the required provider name. This phase depends on RFC-0053 Phase 3 (#4142) to scaffold validateAuthServerIntegration, and on Phase 1 (#4144) for the StrategyTypeUpstreamInject constant and UpstreamInjectConfig type.
Context
This is Phase 3 of the RFC-0054 epic (#3925), which implements the upstream_inject outgoing auth strategy for vMCP. The strategy reads upstream IDP access tokens from identity.UpstreamTokens (populated by RFC-0052's auth middleware) and injects them as Authorization: Bearer headers on outgoing backend requests.
RFC-0053 declared validation rules V-01 through V-07 and scaffolded the validateAuthServerIntegration function in pkg/vmcp/config/validator.go. RFC-0053 Phase 3 (#4142) implements the scaffold and the non-upstream_inject-specific rules (V-03 through V-07). This phase (RFC-0054 Phase 3) adds the upstream_inject case to that switch statement, implementing V-01, V-02, and V-06. Additionally, StrategyTypeUpstreamInject must be added to the validTypes slice in validateBackendAuthStrategy.
The validator is called at process startup (YAML/CLI path) and by the operator reconciler (Kubernetes path), so these rules catch misconfiguration at two enforcement points.
Dependencies: #4144 (Phase 1 — core types and sentinel), #4142 (RFC-0053 Phase 3 — validateAuthServerIntegration scaffold)
Blocks: None (Phase 4 does not depend on Phase 3)
Acceptance Criteria
-
authtypes.StrategyTypeUpstreamInjectis added to thevalidTypesslice invalidateBackendAuthStrategyinpkg/vmcp/config/validator.go - V-01:
validateAuthServerIntegrationreturns an error when any backend strategy hasType == "upstream_inject"andcfg.AuthServer == nil; error message references "no authServer" - V-06:
validateAuthServerIntegrationreturns an error when any backend strategy hasType == "upstream_inject"andstrategy.UpstreamInject == nil || strategy.UpstreamInject.ProviderName == ""; error message references "non-empty providerName" - V-02:
validateAuthServerIntegrationreturns an error whenUpstreamInject.ProviderNameis non-empty but is not found in the AS upstream provider list (viahasUpstreamProviderhelper); error message includes the unknown provider name - V-01 check occurs before V-06 (nil AS is checked before accessing
cfg.AuthServer.RunConfig.Upstreams) - V-06 check occurs before V-02 (empty
ProviderNameis caught before the lookup that would use it) - Positive case:
upstream_injectbackend with a valid AS and a matching provider name passes validation (returnsnil) - Four new table-driven test entries added to
pkg/vmcp/config/validator_test.gofollowing the existingtestingpackage style: V-01 negative, V-02 negative, V-06 negative, positive - All existing tests continue to pass (
task test) - SPDX license header is present on all modified Go files (
task license-check) -
task lintpasses (ortask lint-fixresolves all issues)
Technical Approach
Recommended Implementation
This phase consists of two changes to existing files: add the upstream_inject case to validateAuthServerIntegration in pkg/vmcp/config/validator.go, and add four new test entries to pkg/vmcp/config/validator_test.go.
Important prerequisite ordering: This phase must be implemented after both #4144 (Phase 1 — types) and #4142 (RFC-0053 Phase 3 — validateAuthServerIntegration scaffold) are merged. Phase 1 provides authtypes.StrategyTypeUpstreamInject and authtypes.UpstreamInjectConfig; RFC-0053 Phase 3 provides the validateAuthServerIntegration function and the hasUpstreamProvider helper that this phase extends.
Step 1: Add upstream_inject to validTypes
In validateBackendAuthStrategy (around line 187–193 of validator.go), append authtypes.StrategyTypeUpstreamInject to the validTypes slice:
validTypes := []string{
authtypes.StrategyTypeUnauthenticated,
authtypes.StrategyTypeHeaderInjection,
authtypes.StrategyTypeTokenExchange,
authtypes.StrategyTypeUpstreamInject,
}Step 2: Add upstream_inject case to validateAuthServerIntegration
RFC-0053 Phase 3 (#4142) scaffolds validateAuthServerIntegration with a switch on strategy.Type. Add the case authtypes.StrategyTypeUpstreamInject: block implementing V-01 (checked first), V-06 (checked second), and V-02 (checked last, since it reads strategy.UpstreamInject.ProviderName and calls into cfg.AuthServer.RunConfig):
case authtypes.StrategyTypeUpstreamInject:
// V-01: upstream_inject requires the embedded auth server to be configured
if !asConfigured {
return fmt.Errorf(
"outgoingAuth backend %q uses upstream_inject but no authServer is configured; "+
"upstream_inject requires identity.UpstreamTokens which is only populated "+
"when the embedded auth server is active",
backendName)
}
// V-06: providerName must be non-empty
if strategy.UpstreamInject == nil || strategy.UpstreamInject.ProviderName == "" {
return fmt.Errorf(
"outgoingAuth backend %q upstream_inject requires a non-empty providerName",
backendName)
}
// V-02: providerName must exist in the AS upstream list
if !hasUpstreamProvider(cfg.AuthServer.RunConfig, strategy.UpstreamInject.ProviderName) {
return fmt.Errorf(
"outgoingAuth backend %q references upstream provider %q which is not "+
"configured in authServer.runConfig.upstreams",
backendName, strategy.UpstreamInject.ProviderName)
}Note: the asConfigured local variable, backendName iteration, and hasUpstreamProvider helper are all provided by the RFC-0053 Phase 3 scaffold.
Step 3: Add unit tests
Extend the existing table-driven test suite in validator_test.go with a TestValidateAuthServerIntegration function (or add entries to an existing one if RFC-0053 Phase 3 created it). Follow the existing testing package style — []struct{name string; cfg *Config; wantErr bool; errMsg string} with t.Run and t.Parallel(). See the Component Interfaces section for test fixture construction.
Patterns and Frameworks
- Table-driven tests (
testingpackage): Match the pattern inpkg/vmcp/config/validator_test.go— no Ginkgo in this package; do not add a Ginkgo bootstrap. require.NoErrorandrequire.ErrorContains: Usegithub.com/stretchr/testify/requirefor assertions in tests rather thant.Fatalor manualstrings.Contains.- Check order matters: V-01 must precede V-06, which must precede V-02. V-02 dereferences
cfg.AuthServer.RunConfigwhich is nil-safe only after V-01 passes; V-02 usesstrategy.UpstreamInject.ProviderNamewhich is safe only after V-06 passes. - No token values in error messages: Validation error messages include backend name and provider name only — never token values (hard security requirement).
- SPDX headers:
task license-fixcan add missing headers automatically.
Code Pointers
pkg/vmcp/config/validator.go— Primary file to modify.validateBackendAuthStrategy(lines 182–223) — addStrategyTypeUpstreamInjectto thevalidTypesslice.validateAuthServerIntegration(added by RFC-0053 Phase 3 Phase 3: Startup validation (V-01..V-07) — upstream_inject types and validateAuthServerIntegration #4142) — add theupstream_injectcase. ThecontainsandcontainsStrategyhelper functions at the bottom of the file (lines 441–457) show the existing helper pattern.pkg/vmcp/config/validator_test.go— Add 4 new table-driven entries to theTestValidateAuthServerIntegrationtest function (created by RFC-0053 Phase 3 Phase 3: Startup validation (V-01..V-07) — upstream_inject types and validateAuthServerIntegration #4142).pkg/vmcp/auth/types/types.go— Source ofauthtypes.StrategyTypeUpstreamInjectandauthtypes.UpstreamInjectConfig; provided by Phase 1 (Phase 1: Add core types and sentinel for upstream_inject strategy (RFC-0054) #4144).pkg/authserver/config.go—authserver.RunConfigtype withUpstreams []UpstreamRunConfig(each withName string). Used to construct test fixtures for V-02.pkg/vmcp/config/validator.gohasUpstreamProvider— Helper function provided by RFC-0053 Phase 3 (Phase 3: Startup validation (V-01..V-07) — upstream_inject types and validateAuthServerIntegration #4142); iteratescfg.AuthServer.RunConfig.Upstreamsto find a match byName.
Component Interfaces
The following interfaces and types are used in this phase. All types are provided by Phase 1 (#4144) and RFC-0053 Phase 3 (#4142).
// From pkg/vmcp/auth/types/types.go (Phase 1, #4144)
const StrategyTypeUpstreamInject = "upstream_inject"
type UpstreamInjectConfig struct {
ProviderName string `json:"providerName" yaml:"providerName"`
}
// BackendAuthStrategy (field added by Phase 1)
type BackendAuthStrategy struct {
Type string `json:"type" yaml:"type"`
// ... existing fields ...
UpstreamInject *UpstreamInjectConfig `json:"upstreamInject,omitempty" yaml:"upstreamInject,omitempty"`
}The validateAuthServerIntegration function is scaffolded by RFC-0053 Phase 3 (#4142) with the following structure (for reference when adding the upstream_inject case):
// validateAuthServerIntegration validates AS integration (RFC-0053 Phase 3 scaffold).
// RFC-0054 Phase 3 adds the upstream_inject case to this switch.
func (v *DefaultValidator) validateAuthServerIntegration(cfg *Config) error {
// asConfigured is true when cfg.AuthServer != nil
asConfigured := cfg.AuthServer != nil
// Iterates all backends, calling the switch below for each strategy
for backendName, strategy := range collectAllBackendStrategies(cfg.OutgoingAuth) {
switch strategy.Type {
// ... existing cases for token_exchange, etc. (V-03) ...
case authtypes.StrategyTypeUpstreamInject:
// V-01, V-06, V-02 — added by this phase (RFC-0054 Phase 3)
}
}
// ... other AS-level checks V-04, V-05, V-07 ...
}Test fixture construction for the four new table entries:
// Helper: build a minimal valid Config with an upstream_inject backend
func makeUpstreamInjectConfig(providerName string, authServer *AuthServerConfig) *Config {
return &Config{
Name: "test-vmcp",
Group: "test-group",
IncomingAuth: &IncomingAuthConfig{Type: "anonymous"},
OutgoingAuth: &OutgoingAuthConfig{
Source: "inline",
Backends: map[string]*authtypes.BackendAuthStrategy{
"test-backend": {
Type: authtypes.StrategyTypeUpstreamInject,
UpstreamInject: &authtypes.UpstreamInjectConfig{
ProviderName: providerName,
},
},
},
},
Aggregation: &AggregationConfig{
ConflictResolution: vmcp.ConflictStrategyPrefix,
ConflictResolutionConfig: &ConflictResolutionConfig{
PrefixFormat: "{workload}_",
},
},
AuthServer: authServer,
}
}
// authserver.RunConfig with a single upstream provider named "github"
// (pkg/authserver/config.go — used by hasUpstreamProvider)
validAS := &AuthServerConfig{
RunConfig: &authserver.RunConfig{
Issuer: "https://vmcp-example.com",
Upstreams: []authserver.UpstreamRunConfig{
{Name: "github"},
},
},
}Table entries:
{
name: "V-01: upstream_inject with no authServer",
cfg: makeUpstreamInjectConfig("github", nil), // AuthServer == nil
wantErr: true,
errMsg: "no authServer",
},
{
name: "V-02: upstream_inject providerName not in AS upstreams",
cfg: makeUpstreamInjectConfig("unknown", validAS), // "unknown" not in ["github"]
wantErr: true,
errMsg: "unknown",
},
{
name: "V-06: upstream_inject with empty providerName",
cfg: makeUpstreamInjectConfig("", validAS), // empty providerName
wantErr: true,
errMsg: "non-empty providerName",
},
{
name: "positive: upstream_inject with valid AS and matching provider",
cfg: makeUpstreamInjectConfig("github", validAS), // "github" in ["github"]
wantErr: false,
},Testing Strategy
Unit Tests — 4 new table-driven entries in pkg/vmcp/config/validator_test.go:
- V-01 negative:
upstream_injectbackend,AuthServer == nil— error containing "no authServer" - V-02 negative:
upstream_injectbackend,providerName: "unknown", AS has["github"]— error containing "unknown" - V-06 negative:
upstream_injectbackend, emptyproviderName— error containing "non-empty providerName" - Positive:
upstream_injectbackend, valid AS with["github"],providerName: "github"—nilerror
Regression check
- Existing tests in
validator_test.gocontinue to pass unmodified — no existing behavior is altered
Codegen and lint
-
task testpasses -
task lintpasses (ortask lint-fixresolves all issues) -
task license-checkpasses (SPDX headers on all modified files)
Out of Scope
- V-03, V-04, V-05, V-07 validation rules — these are implemented by RFC-0053 Phase 3 (Phase 3: Startup validation (V-01..V-07) — upstream_inject types and validateAuthServerIntegration #4142)
validateAuthServerIntegrationfunction scaffold — provided by RFC-0053 Phase 3 (Phase 3: Startup validation (V-01..V-07) — upstream_inject types and validateAuthServerIntegration #4142)- The
collectAllBackendStrategiesandhasUpstreamProviderhelpers — provided by RFC-0053 Phase 3 (Phase 3: Startup validation (V-01..V-07) — upstream_inject types and validateAuthServerIntegration #4142) - Implementation of
UpstreamInjectStrategy(Phase 2 of RFC-0054) - CRD type additions (
ExternalAuthTypeUpstreamInject,UpstreamInjectSpec) and converter (Phase 4 of RFC-0054) - Architecture documentation updates (
docs/arch/02-core-concepts.md,docs/vmcp-auth.md) - Step-up auth signaling mechanics (UC-06) —
ErrUpstreamTokenNotFoundsentinel is defined in Phase 1 but the intercept/redirect flow is a separate RFC - New integration or E2E tests beyond the four unit test entries
References
- RFC-0054 (primary):
docs/proposals/THV-0054-vmcp-upstream-inject-strategy.md - Parent epic: vMCP: implement upstream_inject outgoing auth strategy #3925
- Phase 1 (upstream dependency — core types): Phase 1: Add core types and sentinel for upstream_inject strategy (RFC-0054) #4144
- RFC-0053 Phase 3 (upstream dependency —
validateAuthServerIntegrationscaffold): Phase 3: Startup validation (V-01..V-07) — upstream_inject types and validateAuthServerIntegration #4142 - RFC-0052 (multi-upstream IDP, defines
identity.UpstreamTokens): Auth Server: multi-upstream provider support #3924 - RFC-0053 (embedded AS in vMCP, parent of Phase 3: Startup validation (V-01..V-07) — upstream_inject types and validateAuthServerIntegration #4142): vMCP: add embedded authorization server #4120