Skip to content

Commit 571bdfc

Browse files
committed
Add factory package to resolve auth import cycle
Introduces pkg/vmcp/auth/factory to break the circular dependency between pkg/vmcp/auth and pkg/vmcp/auth/strategies. The import cycle occurred because: - auth package needed to import strategies to instantiate them - strategies package imported auth for Identity and context helpers The factory package sits at the composition layer and can import both auth (for interfaces) and strategies (for implementations) without creating cycles.
1 parent 8bb674d commit 571bdfc

File tree

1 file changed

+166
-0
lines changed

1 file changed

+166
-0
lines changed

pkg/vmcp/auth/factory/outgoing.go

Lines changed: 166 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,166 @@
1+
// Copyright 2025 Stacklok, Inc.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
// Package factory provides factory functions for creating vMCP authentication components.
16+
package factory
17+
18+
import (
19+
"context"
20+
"fmt"
21+
"strings"
22+
23+
"github.com/stacklok/toolhive/pkg/vmcp/auth"
24+
"github.com/stacklok/toolhive/pkg/vmcp/auth/strategies"
25+
"github.com/stacklok/toolhive/pkg/vmcp/config"
26+
)
27+
28+
// NewOutgoingAuthRegistry creates an OutgoingAuthRegistry from configuration.
29+
// It registers all strategies found in the configuration (both default and backend-specific).
30+
//
31+
// The factory ALWAYS registers the "unauthenticated" strategy as a default fallback,
32+
// ensuring that backends without explicit authentication configuration can function.
33+
// This makes empty/nil configuration safe: the registry will have at least one
34+
// usable strategy.
35+
//
36+
// Strategy Registration:
37+
// - "unauthenticated" is always registered (default fallback)
38+
// - Additional strategies are registered based on configuration
39+
// - Each strategy is instantiated once and shared across backends
40+
// - Strategies are stateless (except token_exchange which has internal caching)
41+
//
42+
// Parameters:
43+
// - ctx: Context for any initialization that requires it
44+
// - cfg: The outgoing authentication configuration (may be nil)
45+
//
46+
// Returns:
47+
// - auth.OutgoingAuthRegistry: Configured registry with registered strategies
48+
// - error: Any error during strategy initialization or registration
49+
func NewOutgoingAuthRegistry(_ context.Context, cfg *config.OutgoingAuthConfig) (auth.OutgoingAuthRegistry, error) {
50+
registry := auth.NewDefaultOutgoingAuthRegistry()
51+
52+
// ALWAYS register the unauthenticated strategy as the default fallback.
53+
if err := registerUnauthenticatedStrategy(registry); err != nil {
54+
return nil, err
55+
}
56+
57+
// Handle nil config gracefully - return registry with unauthenticated strategy
58+
if cfg == nil {
59+
return registry, nil
60+
}
61+
62+
// Validate configuration structure
63+
if err := validateConfig(cfg); err != nil {
64+
return nil, err
65+
}
66+
67+
// Collect and register all unique strategy types from configuration
68+
strategyTypes := collectStrategyTypes(cfg)
69+
if err := registerStrategies(registry, strategyTypes); err != nil {
70+
return nil, err
71+
}
72+
73+
return registry, nil
74+
}
75+
76+
// registerUnauthenticatedStrategy registers the default unauthenticated strategy.
77+
func registerUnauthenticatedStrategy(registry auth.OutgoingAuthRegistry) error {
78+
unauthStrategy := strategies.NewUnauthenticatedStrategy()
79+
if err := registry.RegisterStrategy("unauthenticated", unauthStrategy); err != nil {
80+
return fmt.Errorf("failed to register default unauthenticated strategy: %w", err)
81+
}
82+
return nil
83+
}
84+
85+
// validateConfig validates the configuration structure.
86+
func validateConfig(cfg *config.OutgoingAuthConfig) error {
87+
if cfg.Default != nil && strings.TrimSpace(cfg.Default.Type) == "" {
88+
return fmt.Errorf("default auth strategy type cannot be empty")
89+
}
90+
91+
for backendID, backendCfg := range cfg.Backends {
92+
if backendCfg != nil && strings.TrimSpace(backendCfg.Type) == "" {
93+
return fmt.Errorf("backend %q has empty auth strategy type", backendID)
94+
}
95+
}
96+
97+
return nil
98+
}
99+
100+
// collectStrategyTypes collects all unique strategy types from configuration.
101+
func collectStrategyTypes(cfg *config.OutgoingAuthConfig) map[string]struct{} {
102+
strategyTypes := make(map[string]struct{})
103+
104+
// Add default strategy type if present
105+
if cfg.Default != nil && cfg.Default.Type != "" {
106+
strategyTypes[cfg.Default.Type] = struct{}{}
107+
}
108+
109+
// Add all backend strategy types
110+
for _, backendCfg := range cfg.Backends {
111+
if backendCfg != nil && backendCfg.Type != "" {
112+
strategyTypes[backendCfg.Type] = struct{}{}
113+
}
114+
}
115+
116+
return strategyTypes
117+
}
118+
119+
// registerStrategies instantiates and registers each unique strategy type.
120+
func registerStrategies(registry auth.OutgoingAuthRegistry, strategyTypes map[string]struct{}) error {
121+
for strategyType := range strategyTypes {
122+
// Skip "unauthenticated" - already registered
123+
if strategyType == "unauthenticated" {
124+
continue
125+
}
126+
127+
strategy, err := createStrategy(strategyType)
128+
if err != nil {
129+
return fmt.Errorf("failed to create strategy %q: %w", strategyType, err)
130+
}
131+
132+
if err := registry.RegisterStrategy(strategyType, strategy); err != nil {
133+
return fmt.Errorf("failed to register strategy %q: %w", strategyType, err)
134+
}
135+
}
136+
137+
return nil
138+
}
139+
140+
// createStrategy instantiates a strategy based on its type.
141+
//
142+
// Each strategy instance is stateless (except token_exchange which has internal caching).
143+
// This function validates that the strategy type is not empty and returns an appropriate
144+
// error for unknown strategy types.
145+
//
146+
// Parameters:
147+
// - strategyType: The type identifier of the strategy to create
148+
//
149+
// Returns:
150+
// - auth.Strategy: The instantiated strategy
151+
// - error: Any error during strategy creation or validation
152+
func createStrategy(strategyType string) (auth.Strategy, error) {
153+
// Validate strategy type is not empty
154+
if strings.TrimSpace(strategyType) == "" {
155+
return nil, fmt.Errorf("strategy type cannot be empty")
156+
}
157+
158+
switch strategyType {
159+
case "header_injection":
160+
return strategies.NewHeaderInjectionStrategy(), nil
161+
case "unauthenticated":
162+
return strategies.NewUnauthenticatedStrategy(), nil
163+
default:
164+
return nil, fmt.Errorf("unknown strategy type: %s", strategyType)
165+
}
166+
}

0 commit comments

Comments
 (0)