ZITADEL does not put roles into a machine-user access token on this instance. The grant exists, the scope asks for urn:zitadel:iam:org:project:roles, and the claim still never arrives; a colon-free role key makes no difference either. A service account therefore cannot satisfy requiredCapability by any configuration. auth.allowListedMachineIdentities admits such a caller when its client_id appears in zitadelClientIds. The caller is still authenticated: it signed an assertion with a private key whose public half ZITADEL holds. The allow-list entry is a reviewed line of configuration, not a wildcard. The new path is off by default, and a token that does state its roles is judged by the role check as before, so the weaker rule never applies twice.
205 lines
7.0 KiB
Go
205 lines
7.0 KiB
Go
package config
|
|
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
"time"
|
|
|
|
"forgejo.riotpiao.com/rock/homelab-frontend/internal/serviceadapter"
|
|
)
|
|
|
|
// Config holds the gateway configuration.
|
|
type Config struct {
|
|
// ListenAddr is the address to listen on for HTTP traffic.
|
|
ListenAddr string
|
|
// ShutdownTimeout is the maximum time to wait for in-flight requests
|
|
// to complete before forcing shutdown.
|
|
ShutdownTimeout time.Duration
|
|
// Routes maps route names to their upstream configuration.
|
|
Routes map[string]*Route
|
|
// Models maps model names to their upstream configuration.
|
|
// Multiple models can point to the same upstream address.
|
|
Models map[string]*ModelUpstream
|
|
// Adapters holds service adapter definitions for X-Service routing.
|
|
Adapters []*serviceadapter.ServiceAdapter
|
|
// Auth holds JWT authentication configuration for /v1/* endpoints.
|
|
Auth AuthConfig
|
|
// Temporal holds Temporal server configuration.
|
|
Temporal TemporalConfig
|
|
}
|
|
|
|
// ModelUpstream holds upstream configuration for a specific model.
|
|
type ModelUpstream struct {
|
|
// Name is the model name clients send (e.g., "reasoning", "ornith:35b").
|
|
Name string
|
|
// Address is the upstream server address (host:port).
|
|
// When Addresses is set, this holds Addresses[0] for backward compatibility.
|
|
Address string
|
|
// Addresses holds all backend addresses for multi-backend cache-aware routing.
|
|
// Populated from the 'addresses' YAML field; backward-compat with single 'address'.
|
|
Addresses []string
|
|
// Path is the upstream path for this model (e.g., "/v1/chat/completions").
|
|
Path string
|
|
// UpstreamModel is the model name to send to the upstream server.
|
|
// If empty, the client-provided model name (Name) is used as-is.
|
|
// Use this when the upstream expects a different model name than clients send.
|
|
UpstreamModel string
|
|
// AuthRequired indicates whether this model requires JWT authentication.
|
|
AuthRequired bool
|
|
}
|
|
|
|
// TemporalConfig holds Temporal server configuration.
|
|
type TemporalConfig struct {
|
|
// HostPort is the address of the Temporal server (host:port).
|
|
HostPort string
|
|
}
|
|
|
|
// AuthConfig holds JWT authentication configuration.
|
|
type AuthConfig struct {
|
|
// Enabled globally enables/disables auth for /v1/* endpoints.
|
|
Enabled bool
|
|
// Issuer is the expected JWT issuer (iss claim).
|
|
Issuer string
|
|
// Audience is the expected JWT audience (aud claim).
|
|
Audience string
|
|
// JWKSURL is the URL to fetch JSON Web Key Set for signature validation.
|
|
JWKSURL string
|
|
// RequiredCapability is the permission required for LLM inference (e.g., "llm:inference").
|
|
RequiredCapability string
|
|
// AllowListedMachineIdentities admits a caller whose token carries no roles
|
|
// claim, when its client id is in ZitadelClientIDs. ZITADEL does not assert roles
|
|
// in machine-user access tokens on this instance, so a service account can never
|
|
// satisfy RequiredCapability. The allow-list is then the authorisation: the caller
|
|
// still proves possession of a registered private key, and the entry is a reviewed
|
|
// line of configuration, not a wildcard.
|
|
AllowListedMachineIdentities bool
|
|
// TokenURL is the OIDC token endpoint used for token exchange
|
|
// (client_credentials / password grants).
|
|
TokenURL string
|
|
// ClientID is the OAuth2 client ID for token exchange.
|
|
ClientID string
|
|
// ClientSecret is the OAuth2 client secret (loaded from env, never from config file).
|
|
ClientSecret string
|
|
// ZitadelDomain is the ZITADEL instance host (for example
|
|
// "zitadel.riotpiao.com"). When set, token validation uses the official
|
|
// ZITADEL Go SDK instead of the raw JWKS path.
|
|
ZitadelDomain string
|
|
// ZitadelInsecurePort, when non-empty, makes the SDK speak plain HTTP to
|
|
// that port. Only for a local port-forward.
|
|
ZitadelInsecurePort string
|
|
// ZitadelClientIDs is the audience allow-list for the SDK verifier. Empty
|
|
// accepts any audience issued by the same instance.
|
|
ZitadelClientIDs []string
|
|
}
|
|
|
|
// Route represents a single route and its upstream configuration.
|
|
type Route struct {
|
|
// Name is the route identifier.
|
|
Name string
|
|
// Upstream holds the upstream server configuration.
|
|
Upstream Upstream
|
|
}
|
|
|
|
// Upstream holds upstream server configuration for a route.
|
|
type Upstream struct {
|
|
// Address is the upstream server address (host:port).
|
|
Address string
|
|
// PathRewrite is an optional path prefix rewrite. Empty string means no rewrite.
|
|
PathRewrite string
|
|
// ConnectTimeout is the maximum time to establish a connection to the upstream.
|
|
ConnectTimeout time.Duration
|
|
// ReadTimeout is the maximum time to read a response from the upstream.
|
|
ReadTimeout time.Duration
|
|
// WriteTimeout is the maximum time to write a request to the upstream.
|
|
WriteTimeout time.Duration
|
|
// MaxBodySize is the maximum request body size in bytes.
|
|
MaxBodySize int64
|
|
// AuthRequired indicates whether this route requires authentication.
|
|
AuthRequired bool
|
|
}
|
|
|
|
// LookupModel finds a model by name (case-sensitive, exact match).
|
|
func (c *Config) LookupModel(name string) *ModelUpstream {
|
|
if c == nil || c.Models == nil {
|
|
return nil
|
|
}
|
|
return c.Models[name]
|
|
}
|
|
|
|
// ModelNames returns a sorted list of all known model names.
|
|
func (c *Config) ModelNames() []string {
|
|
if c == nil || c.Models == nil {
|
|
return nil
|
|
}
|
|
names := make([]string, 0, len(c.Models))
|
|
for name := range c.Models {
|
|
names = append(names, name)
|
|
}
|
|
return names
|
|
}
|
|
|
|
// AllAddresses returns all configured backend addresses for the model.
|
|
// Returns Addresses if non-empty; otherwise wraps Address in a slice.
|
|
func (m *ModelUpstream) AllAddresses() []string {
|
|
if len(m.Addresses) > 0 {
|
|
return m.Addresses
|
|
}
|
|
if m.Address != "" {
|
|
return []string{m.Address}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// Load reads configuration from environment variables with defaults.
|
|
func Load() (*Config, error) {
|
|
listenAddr := "127.0.0.1:8080"
|
|
// Allow override via environment variable
|
|
if addr, ok := os.LookupEnv("LISTEN_ADDR"); ok {
|
|
listenAddr = addr
|
|
}
|
|
|
|
shutdownTimeout := 30 * time.Second
|
|
// Allow override via environment variable
|
|
if timeout, ok := os.LookupEnv("SHUTDOWN_TIMEOUT"); ok {
|
|
d, err := time.ParseDuration(timeout)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("invalid SHUTDOWN_TIMEOUT: %w", err)
|
|
}
|
|
shutdownTimeout = d
|
|
}
|
|
|
|
// Load routes, models, adapters, and auth from config file
|
|
routes := make(map[string]*Route)
|
|
models := make(map[string]*ModelUpstream)
|
|
var adapters []*serviceadapter.ServiceAdapter
|
|
var authConfig AuthConfig
|
|
if configPath, ok := os.LookupEnv("CONFIG_PATH"); ok {
|
|
loadedRoutes, loadedModels, loadedAdapters, loadedAuth, err := LoadRoutesAndModelsFromFile(configPath)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
routes = loadedRoutes
|
|
models = loadedModels
|
|
adapters = loadedAdapters
|
|
authConfig = loadedAuth
|
|
}
|
|
|
|
temporalHostPort := "localhost:7233"
|
|
// Allow override via environment variable
|
|
if hostPort, ok := os.LookupEnv("TEMPORAL_HOST_PORT"); ok {
|
|
temporalHostPort = hostPort
|
|
}
|
|
|
|
return &Config{
|
|
ListenAddr: listenAddr,
|
|
ShutdownTimeout: shutdownTimeout,
|
|
Routes: routes,
|
|
Models: models,
|
|
Adapters: adapters,
|
|
Auth: authConfig,
|
|
Temporal: TemporalConfig{
|
|
HostPort: temporalHostPort,
|
|
},
|
|
}, nil
|
|
}
|