Skip to content

Phase 3: Add upstream_inject startup validation rules V-01, V-02, V-06 (RFC-0054) #4147

@tgrunnagle

Description

@tgrunnagle

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.StrategyTypeUpstreamInject is added to the validTypes slice in validateBackendAuthStrategy in pkg/vmcp/config/validator.go
  • V-01: validateAuthServerIntegration returns an error when any backend strategy has Type == "upstream_inject" and cfg.AuthServer == nil; error message references "no authServer"
  • V-06: validateAuthServerIntegration returns an error when any backend strategy has Type == "upstream_inject" and strategy.UpstreamInject == nil || strategy.UpstreamInject.ProviderName == ""; error message references "non-empty providerName"
  • V-02: validateAuthServerIntegration returns an error when UpstreamInject.ProviderName is non-empty but is not found in the AS upstream provider list (via hasUpstreamProvider helper); 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 ProviderName is caught before the lookup that would use it)
  • Positive case: upstream_inject backend with a valid AS and a matching provider name passes validation (returns nil)
  • Four new table-driven test entries added to pkg/vmcp/config/validator_test.go following the existing testing package 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 lint passes (or task lint-fix resolves 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 (testing package): Match the pattern in pkg/vmcp/config/validator_test.go — no Ginkgo in this package; do not add a Ginkgo bootstrap.
  • require.NoError and require.ErrorContains: Use github.com/stretchr/testify/require for assertions in tests rather than t.Fatal or manual strings.Contains.
  • Check order matters: V-01 must precede V-06, which must precede V-02. V-02 dereferences cfg.AuthServer.RunConfig which is nil-safe only after V-01 passes; V-02 uses strategy.UpstreamInject.ProviderName which 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-fix can add missing headers automatically.

Code Pointers

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_inject backend, AuthServer == nil — error containing "no authServer"
  • V-02 negative: upstream_inject backend, providerName: "unknown", AS has ["github"] — error containing "unknown"
  • V-06 negative: upstream_inject backend, empty providerName — error containing "non-empty providerName"
  • Positive: upstream_inject backend, valid AS with ["github"], providerName: "github"nil error

Regression check

  • Existing tests in validator_test.go continue to pass unmodified — no existing behavior is altered

Codegen and lint

  • task test passes
  • task lint passes (or task lint-fix resolves all issues)
  • task license-check passes (SPDX headers on all modified files)

Out of Scope

References

Metadata

Metadata

Assignees

No one assigned

    Labels

    authenticationauthorizationenhancementNew feature or requestgoPull requests that update go codevmcpVirtual MCP Server related issues

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions