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
|
JWKSURL string
|
||||||
// RequiredCapability is the permission required for LLM inference (e.g., "llm:inference").
|
// RequiredCapability is the permission required for LLM inference (e.g., "llm:inference").
|
||||||
RequiredCapability string
|
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.
|
// Route represents a single route and its upstream configuration.
|
||||||
|
|||||||
@@ -25,6 +25,8 @@ type rawAuth struct {
|
|||||||
Audience string `yaml:"audience"`
|
Audience string `yaml:"audience"`
|
||||||
JWKSURL string `yaml:"jwksUrl"`
|
JWKSURL string `yaml:"jwksUrl"`
|
||||||
RequiredCapability string `yaml:"requiredCapability"`
|
RequiredCapability string `yaml:"requiredCapability"`
|
||||||
|
TokenURL string `yaml:"tokenUrl"`
|
||||||
|
ClientID string `yaml:"clientId"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// rawRoute represents a single route in the YAML configuration.
|
// 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,
|
Audience: raw.Auth.Audience,
|
||||||
JWKSURL: raw.Auth.JWKSURL,
|
JWKSURL: raw.Auth.JWKSURL,
|
||||||
RequiredCapability: raw.Auth.RequiredCapability,
|
RequiredCapability: raw.Auth.RequiredCapability,
|
||||||
|
TokenURL: raw.Auth.TokenURL,
|
||||||
|
ClientID: raw.Auth.ClientID,
|
||||||
|
ClientSecret: os.Getenv("AUTH_CLIENT_SECRET"),
|
||||||
}
|
}
|
||||||
|
|
||||||
return routes, models, adapters, authConfig, nil
|
return routes, models, adapters, authConfig, nil
|
||||||
|
|||||||
@@ -17,6 +17,8 @@ data:
|
|||||||
audience: "api-gw"
|
audience: "api-gw"
|
||||||
jwksUrl: "http://authentik-server.iam.svc.cluster.local/application/o/api-gw/jwks/"
|
jwksUrl: "http://authentik-server.iam.svc.cluster.local/application/o/api-gw/jwks/"
|
||||||
requiredCapability: "llm:inference"
|
requiredCapability: "llm:inference"
|
||||||
|
tokenUrl: "http://authentik-server.iam.svc.cluster.local/application/o/token/"
|
||||||
|
clientId: "api-gw"
|
||||||
|
|
||||||
# Routes: standard HTTP proxy routes (not LLM-specific)
|
# Routes: standard HTTP proxy routes (not LLM-specific)
|
||||||
# These are for non-LLM services (agent-pod/console, etc.)
|
# These are for non-LLM services (agent-pod/console, etc.)
|
||||||
|
|||||||
@@ -57,6 +57,12 @@ spec:
|
|||||||
value: "0.0.0.0:8080"
|
value: "0.0.0.0:8080"
|
||||||
- name: CONFIG_PATH
|
- name: CONFIG_PATH
|
||||||
value: "/etc/gateway/config.yaml"
|
value: "/etc/gateway/config.yaml"
|
||||||
|
- name: AUTH_CLIENT_SECRET
|
||||||
|
valueFrom:
|
||||||
|
secretKeyRef:
|
||||||
|
name: api-gw-client-secret
|
||||||
|
key: client-secret
|
||||||
|
optional: true
|
||||||
- name: SHUTDOWN_TIMEOUT
|
- name: SHUTDOWN_TIMEOUT
|
||||||
value: "5m"
|
value: "5m"
|
||||||
- name: LOG_LEVEL
|
- name: LOG_LEVEL
|
||||||
|
|||||||
@@ -14,6 +14,8 @@ stringData:
|
|||||||
audience: "api-gw"
|
audience: "api-gw"
|
||||||
jwksUrl: "http://authentik-server.iam.svc.cluster.local/application/o/api-gw/jwks/"
|
jwksUrl: "http://authentik-server.iam.svc.cluster.local/application/o/api-gw/jwks/"
|
||||||
requiredCapability: "llm:inference"
|
requiredCapability: "llm:inference"
|
||||||
|
tokenUrl: "http://authentik-server.iam.svc.cluster.local/application/o/token/"
|
||||||
|
clientId: "api-gw"
|
||||||
routes: []
|
routes: []
|
||||||
models:
|
models:
|
||||||
- name: "reasoning"
|
- name: "reasoning"
|
||||||
|
|||||||
Reference in New Issue
Block a user