feat(config): add tokenUrl, clientId, clientSecret to auth config (#17)
CI / CI (push) Successful in 3m20s
CI / CI (push) Successful in 3m20s
Closes homelab#12 (P3.7) ## Changes - `AuthConfig`: added `TokenURL`, `ClientID`, `ClientSecret` fields - `loader.go`: reads `tokenUrl`/`clientId` from YAML, `ClientSecret` from `AUTH_CLIENT_SECRET` env - `deployment.yaml`: `AUTH_CLIENT_SECRET` from `api-gw-client-secret` Secret (optional) - `gateway-config-secret.enc.yaml` + `configmap.yaml`: added `tokenUrl` and `clientId` ## Secret never in YAML `clientSecret` deliberately omitted from YAML struct. Loaded from env only. ## Tests 3 tests: full config load, env-only secret, backward compat (missing fields = zero). Co-authored-by: poimen <[email protected]>
This commit was merged in pull request #17.
This commit is contained in:
@@ -0,0 +1,104 @@
|
||||
package config_test
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"forgejo.riotpiao.com/rock/homelab-frontend/internal/config"
|
||||
)
|
||||
|
||||
func TestLoadAuthConfig_TokenURLAndClientID(t *testing.T) {
|
||||
yaml := `
|
||||
routes: []
|
||||
models: []
|
||||
auth:
|
||||
enabled: true
|
||||
issuer: "https://authentik.example.com/application/o/api-gw/"
|
||||
audience: "api-gw"
|
||||
jwksUrl: "https://authentik.example.com/application/o/api-gw/jwks/"
|
||||
requiredCapability: "llm:inference"
|
||||
tokenUrl: "https://authentik.example.com/application/o/token/"
|
||||
clientId: "api-gw"
|
||||
`
|
||||
dir := t.TempDir()
|
||||
path := filepath.Join(dir, "config.yaml")
|
||||
if err := os.WriteFile(path, []byte(yaml), 0644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Set env for client secret
|
||||
t.Setenv("AUTH_CLIENT_SECRET", "test-secret-value")
|
||||
|
||||
_, _, _, auth, err := config.LoadRoutesAndModelsFromFile(path)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
|
||||
if !auth.Enabled {
|
||||
t.Error("auth should be enabled")
|
||||
}
|
||||
if auth.TokenURL != "https://authentik.example.com/application/o/token/" {
|
||||
t.Errorf("tokenUrl = %q, want authentik token endpoint", auth.TokenURL)
|
||||
}
|
||||
if auth.ClientID != "api-gw" {
|
||||
t.Errorf("clientId = %q, want api-gw", auth.ClientID)
|
||||
}
|
||||
if auth.ClientSecret != "test-secret-value" {
|
||||
t.Errorf("clientSecret = %q, want test-secret-value", auth.ClientSecret)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadAuthConfig_ClientSecretFromEnvOnly(t *testing.T) {
|
||||
yaml := `
|
||||
routes: []
|
||||
models: []
|
||||
auth:
|
||||
enabled: true
|
||||
tokenUrl: "https://example.com/token/"
|
||||
clientId: "test"
|
||||
`
|
||||
dir := t.TempDir()
|
||||
path := filepath.Join(dir, "config.yaml")
|
||||
os.WriteFile(path, []byte(yaml), 0644)
|
||||
|
||||
// No AUTH_CLIENT_SECRET env set
|
||||
t.Setenv("AUTH_CLIENT_SECRET", "")
|
||||
|
||||
_, _, _, auth, err := config.LoadRoutesAndModelsFromFile(path)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
|
||||
if auth.ClientSecret != "" {
|
||||
t.Errorf("clientSecret should be empty when env not set, got %q", auth.ClientSecret)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadAuthConfig_BackwardCompatible(t *testing.T) {
|
||||
// Config without tokenUrl/clientId should still load (zero values)
|
||||
yaml := `
|
||||
routes: []
|
||||
models: []
|
||||
auth:
|
||||
enabled: true
|
||||
issuer: "https://example.com/"
|
||||
jwksUrl: "https://example.com/jwks/"
|
||||
requiredCapability: "llm:inference"
|
||||
`
|
||||
dir := t.TempDir()
|
||||
path := filepath.Join(dir, "config.yaml")
|
||||
os.WriteFile(path, []byte(yaml), 0644)
|
||||
|
||||
_, _, _, auth, err := config.LoadRoutesAndModelsFromFile(path)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
|
||||
if auth.TokenURL != "" {
|
||||
t.Errorf("tokenUrl should be empty, got %q", auth.TokenURL)
|
||||
}
|
||||
if auth.ClientID != "" {
|
||||
t.Errorf("clientId should be empty, got %q", auth.ClientID)
|
||||
}
|
||||
}
|
||||
@@ -50,6 +50,12 @@ type AuthConfig struct {
|
||||
JWKSURL string
|
||||
// RequiredCapability is the permission required for LLM inference (e.g., "llm:inference").
|
||||
RequiredCapability string
|
||||
// TokenURL is the Authentik token endpoint for password/refresh 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
|
||||
}
|
||||
|
||||
// Route represents a single route and its upstream configuration.
|
||||
|
||||
@@ -25,6 +25,8 @@ type rawAuth struct {
|
||||
Audience string `yaml:"audience"`
|
||||
JWKSURL string `yaml:"jwksUrl"`
|
||||
RequiredCapability string `yaml:"requiredCapability"`
|
||||
TokenURL string `yaml:"tokenUrl"`
|
||||
ClientID string `yaml:"clientId"`
|
||||
}
|
||||
|
||||
// rawRoute represents a single route in the YAML configuration.
|
||||
@@ -176,6 +178,9 @@ func LoadRoutesAndModelsFromFile(path string) (map[string]*Route, map[string]*Mo
|
||||
Audience: raw.Auth.Audience,
|
||||
JWKSURL: raw.Auth.JWKSURL,
|
||||
RequiredCapability: raw.Auth.RequiredCapability,
|
||||
TokenURL: raw.Auth.TokenURL,
|
||||
ClientID: raw.Auth.ClientID,
|
||||
ClientSecret: os.Getenv("AUTH_CLIENT_SECRET"),
|
||||
}
|
||||
|
||||
return routes, models, adapters, authConfig, nil
|
||||
|
||||
Reference in New Issue
Block a user