refactor: dispatcher as dumb pipe + add gRPC detection for Temporal
BREAKING CHANGE: Gateway no longer validates JWTs at dispatcher level. Each upstream service (MinIO, Authentik, Temporal) validates bearer tokens independently. Gateway passes Authorization header through unchanged. Changes: - Removed JWT validation from Dispatcher - Removed internal/auth JWT validator usage - Added gRPC URL scheme detection (grpc://) - Added temporal-frontend with gRPC config (returns 501 not-implemented) - All adapters now auth: required: false (services validate own tokens) - Gateway is now a transparent routing layer, not auth gateway gRPC forwarding requires grpcproxy middleware (future Phase 9). For now, gRPC clients should connect directly to temporal-frontend:7233.
This commit is contained in:
@@ -1,36 +1,33 @@
|
||||
package serviceadapter
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/http/httputil"
|
||||
"net/url"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"forgejo.riotpiao.com/rock/homelab-frontend/internal/auth"
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/grpc/credentials/insecure"
|
||||
|
||||
"forgejo.riotpiao.com/rock/homelab-frontend/internal/problem"
|
||||
)
|
||||
|
||||
// Dispatcher handles X-Service based routing to service adapters.
|
||||
// Dispatcher routes X-Service requests to upstreams.
|
||||
// Acts as a dumb pipe: passes Authorization header through unchanged.
|
||||
// Each upstream service validates JWTs independently.
|
||||
type Dispatcher struct {
|
||||
registry *Registry
|
||||
validators map[string]*auth.Validator // Per-service JWT validators
|
||||
registry *Registry
|
||||
}
|
||||
|
||||
// NewDispatcher creates a new service adapter dispatcher with Authentik JWT validators.
|
||||
// NewDispatcher creates a new service adapter dispatcher.
|
||||
func NewDispatcher(registry *Registry) *Dispatcher {
|
||||
dispatcher := &Dispatcher{
|
||||
registry: registry,
|
||||
validators: make(map[string]*auth.Validator),
|
||||
return &Dispatcher{
|
||||
registry: registry,
|
||||
}
|
||||
|
||||
// Create validators for all registered services
|
||||
for _, adapter := range registry.List() {
|
||||
dispatcher.validators[adapter.ServiceName] = auth.NewValidator(adapter.ServiceName)
|
||||
}
|
||||
|
||||
return dispatcher
|
||||
}
|
||||
|
||||
// Matches returns true if the request should be dispatched based on X-Service header.
|
||||
@@ -43,7 +40,6 @@ func (d *Dispatcher) Matches(r *http.Request) bool {
|
||||
func (d *Dispatcher) Dispatch(w http.ResponseWriter, r *http.Request) {
|
||||
serviceName := r.Header.Get("X-Service")
|
||||
if serviceName == "" {
|
||||
// No X-Service header — this shouldn't happen if Matches() was called
|
||||
d.writeError(w, problem.BadRequest("X-Service header required"))
|
||||
return
|
||||
}
|
||||
@@ -93,61 +89,35 @@ func (d *Dispatcher) Dispatch(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
// Check auth requirements using JWT validation
|
||||
requiredCapability := ""
|
||||
auth := resource.Auth
|
||||
if auth == nil {
|
||||
auth = &adapter.Spec.Auth
|
||||
}
|
||||
if method.Auth != nil {
|
||||
auth = method.Auth
|
||||
// Detect protocol from upstream URL scheme
|
||||
upstreamURL := adapter.Spec.Upstream.URL
|
||||
if strings.HasPrefix(upstreamURL, "grpc://") {
|
||||
// gRPC upstream (Temporal, etc.)
|
||||
d.dispatchGRPC(w, r, upstreamURL, method, adapter)
|
||||
} else {
|
||||
// HTTP upstream (MinIO, Authentik, etc.)
|
||||
d.dispatchHTTP(w, r, upstreamURL, method, adapter)
|
||||
}
|
||||
}
|
||||
|
||||
if auth != nil && auth.Required && auth.Capability != "" {
|
||||
requiredCapability = auth.Capability
|
||||
|
||||
// Validate JWT token
|
||||
validator := d.validators[serviceName]
|
||||
if validator == nil {
|
||||
p := problem.NewProblem(http.StatusInternalServerError, "about:blank#server-error",
|
||||
"Internal Server Error", fmt.Sprintf("no validator for service '%s'", serviceName))
|
||||
_ = p.Write(w)
|
||||
return
|
||||
}
|
||||
|
||||
claims, err := validator.ValidateToken(r.Header.Get("Authorization"))
|
||||
if err != nil {
|
||||
p := problem.NewProblem(http.StatusForbidden, "about:blank#forbidden",
|
||||
"Forbidden", fmt.Sprintf("authentication failed: %v", err))
|
||||
_ = p.Write(w)
|
||||
return
|
||||
}
|
||||
|
||||
// Check permission
|
||||
if !validator.HasPermission(claims, requiredCapability) {
|
||||
p := problem.NewProblem(http.StatusForbidden, "about:blank#forbidden",
|
||||
"Forbidden", fmt.Sprintf("permission '%s' required", requiredCapability))
|
||||
_ = p.Write(w)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Build upstream URL
|
||||
upstreamURL, err := url.Parse(adapter.Spec.Upstream.URL)
|
||||
// dispatchHTTP forwards HTTP requests to upstream, passing Authorization header through.
|
||||
func (d *Dispatcher) dispatchHTTP(w http.ResponseWriter, r *http.Request, upstreamURL string, method *Method, adapter *ServiceAdapter) {
|
||||
parsedURL, err := url.Parse(upstreamURL)
|
||||
if err != nil {
|
||||
d.writeError(w, problem.NewProblem(http.StatusInternalServerError, "about:blank#server-error",
|
||||
"Internal Server Error", fmt.Sprintf("invalid upstream URL: %v", err)))
|
||||
return
|
||||
}
|
||||
|
||||
// Create reverse proxy with director that rewrites the path
|
||||
proxy := httputil.NewSingleHostReverseProxy(upstreamURL)
|
||||
// Create reverse proxy
|
||||
proxy := httputil.NewSingleHostReverseProxy(parsedURL)
|
||||
proxy.Director = func(req *http.Request) {
|
||||
req.URL.Scheme = upstreamURL.Scheme
|
||||
req.URL.Host = upstreamURL.Host
|
||||
req.URL.Scheme = parsedURL.Scheme
|
||||
req.URL.Host = parsedURL.Host
|
||||
req.URL.Path = method.UpstreamPath
|
||||
req.RequestURI = ""
|
||||
req.Host = upstreamURL.Host
|
||||
req.Host = parsedURL.Host
|
||||
// Authorization header passes through unchanged
|
||||
}
|
||||
|
||||
// Set timeout
|
||||
@@ -164,7 +134,53 @@ func (d *Dispatcher) Dispatch(w http.ResponseWriter, r *http.Request) {
|
||||
proxy.ServeHTTP(w, r)
|
||||
}
|
||||
|
||||
// dispatchGRPC forwards gRPC requests to upstream.
|
||||
// gRPC URL format: grpc://host:port
|
||||
func (d *Dispatcher) dispatchGRPC(w http.ResponseWriter, r *http.Request, upstreamURL string, method *Method, adapter *ServiceAdapter) {
|
||||
// Extract host:port from grpc://host:port
|
||||
host := strings.TrimPrefix(upstreamURL, "grpc://")
|
||||
if host == upstreamURL {
|
||||
d.writeError(w, problem.NewProblem(http.StatusInternalServerError, "about:blank#server-error",
|
||||
"Internal Server Error", "invalid gRPC URL format"))
|
||||
return
|
||||
}
|
||||
|
||||
// Validate that this is a gRPC request
|
||||
if !strings.HasPrefix(r.Header.Get("Content-Type"), "application/grpc") {
|
||||
d.writeError(w, problem.NewProblem(http.StatusBadRequest, "about:blank#bad-request",
|
||||
"Bad Request", "gRPC service requires application/grpc content-type"))
|
||||
return
|
||||
}
|
||||
|
||||
// Set timeout
|
||||
timeout := adapter.Spec.Upstream.TimeoutSeconds
|
||||
if timeout <= 0 {
|
||||
timeout = 30
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(r.Context(), time.Duration(timeout)*time.Second)
|
||||
defer cancel()
|
||||
|
||||
// Dial gRPC upstream
|
||||
conn, err := grpc.DialContext(ctx, host,
|
||||
grpc.WithTransportCredentials(insecure.NewCredentials()),
|
||||
grpc.WithDefaultCallOptions(
|
||||
grpc.MaxCallRecvMsgSize(100 * 1024 * 1024), // 100MB
|
||||
),
|
||||
)
|
||||
if err != nil {
|
||||
d.writeError(w, problem.NewProblem(http.StatusBadGateway, "about:blank#bad-gateway",
|
||||
"Bad Gateway", fmt.Sprintf("failed to dial gRPC upstream: %v", err)))
|
||||
return
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
// Forward gRPC request
|
||||
// Note: Full gRPC forwarding requires grpcproxy or custom middleware.
|
||||
// For now, return unimplemented (Temporal support coming in Phase 9)
|
||||
d.writeError(w, problem.NewProblem(http.StatusNotImplemented, "about:blank#not-implemented",
|
||||
"Not Implemented", "gRPC forwarding not yet implemented - use in-cluster gRPC clients directly"))
|
||||
}
|
||||
|
||||
func (d *Dispatcher) writeError(w http.ResponseWriter, p *problem.Problem) {
|
||||
_ = p.Write(w)
|
||||
|
||||
Reference in New Issue
Block a user