Add Authelia as centralized authentication and OIDC provider
Authelia runs as a managed native service on Wild Central, providing two integration patterns for network services: - Forward-auth via HAProxy for apps without native SSO (Lua auth-request script intercepts requests, redirects unauthenticated users to login portal) - OIDC provider for apps with native support (Gitea, Grafana, etc.) Backend: new internal/authelia/ package with service manager, config generation, file-based user management (argon2id), and OIDC client management. API endpoints for status, config, users CRUD, and OIDC clients CRUD. HAProxy: config generator extended with lua-load, auth-request directives, and Authelia backend. Auth directives scoped to session cookie domain — only subdomains of the auth portal's parent are eligible for forward-auth. Frontend: Authentication page with enable/disable, user management, OIDC client management (add/edit/delete), and protected domains toggles. Advanced subsystem page for raw config view. Dashboard service card and sidebar entries.
This commit is contained in:
@@ -833,3 +833,213 @@ line3
|
||||
t.Errorf("expected 0 broken services when no markers, got %d", len(broken))
|
||||
}
|
||||
}
|
||||
|
||||
// --- Authelia forward-auth tests ---
|
||||
|
||||
func TestGenerateWithOpts_AuthLuaLoad(t *testing.T) {
|
||||
m := NewManager("")
|
||||
cfg := m.GenerateWithOpts(nil, nil, GenerateOpts{
|
||||
AuthEnabled: true,
|
||||
AuthBackend: "127.0.0.1:9091",
|
||||
AuthDomain: "auth.payne.io",
|
||||
})
|
||||
|
||||
if !strings.Contains(cfg, "lua-load /etc/haproxy/lua/haproxy-auth-request.lua") {
|
||||
t.Error("config should contain lua-load directive when auth is enabled")
|
||||
}
|
||||
}
|
||||
|
||||
func TestGenerateWithOpts_NoAuthLuaLoadWhenDisabled(t *testing.T) {
|
||||
m := NewManager("")
|
||||
cfg := m.GenerateWithOpts(nil, nil, GenerateOpts{
|
||||
AuthEnabled: false,
|
||||
})
|
||||
|
||||
if strings.Contains(cfg, "lua-load") {
|
||||
t.Error("config should NOT contain lua-load when auth is disabled")
|
||||
}
|
||||
}
|
||||
|
||||
func TestGenerateWithOpts_AuthBackend(t *testing.T) {
|
||||
m := NewManager("")
|
||||
httpRoutes := []HTTPRoute{
|
||||
{
|
||||
Name: "app.payne.io",
|
||||
Domain: "app.payne.io",
|
||||
Routes: []HTTPRouteBackend{
|
||||
{Backend: "127.0.0.1:8080", AuthEnabled: true},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
cfg := m.GenerateWithOpts(nil, nil, GenerateOpts{
|
||||
HTTPRoutes: httpRoutes,
|
||||
CertsDir: "/tmp/certs/",
|
||||
AuthEnabled: true,
|
||||
AuthBackend: "127.0.0.1:9091",
|
||||
AuthDomain: "auth.payne.io",
|
||||
})
|
||||
|
||||
if !strings.Contains(cfg, "backend be_authelia") {
|
||||
t.Error("config should contain Authelia backend")
|
||||
}
|
||||
if !strings.Contains(cfg, "server s0 127.0.0.1:9091") {
|
||||
t.Error("config should contain Authelia server address")
|
||||
}
|
||||
}
|
||||
|
||||
func TestGenerateWithOpts_AuthInterceptDirective(t *testing.T) {
|
||||
m := NewManager("")
|
||||
httpRoutes := []HTTPRoute{
|
||||
{
|
||||
Name: "app.payne.io",
|
||||
Domain: "app.payne.io",
|
||||
Routes: []HTTPRouteBackend{
|
||||
{Backend: "127.0.0.1:8080", AuthEnabled: true},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
cfg := m.GenerateWithOpts(nil, nil, GenerateOpts{
|
||||
HTTPRoutes: httpRoutes,
|
||||
CertsDir: "/tmp/certs/",
|
||||
AuthEnabled: true,
|
||||
AuthBackend: "127.0.0.1:9091",
|
||||
AuthDomain: "auth.payne.io",
|
||||
})
|
||||
|
||||
if !strings.Contains(cfg, "lua.auth-request be_authelia") {
|
||||
t.Error("config should contain auth-intercept directive for protected route")
|
||||
}
|
||||
if !strings.Contains(cfg, "auth.payne.io/?rd=") {
|
||||
t.Error("config should contain redirect to auth portal")
|
||||
}
|
||||
}
|
||||
|
||||
func TestGenerateWithOpts_AuthDomainExempt(t *testing.T) {
|
||||
m := NewManager("")
|
||||
httpRoutes := []HTTPRoute{
|
||||
{
|
||||
Name: "auth.payne.io",
|
||||
Domain: "auth.payne.io",
|
||||
Routes: []HTTPRouteBackend{
|
||||
{Backend: "127.0.0.1:9091", AuthEnabled: true},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "app.payne.io",
|
||||
Domain: "app.payne.io",
|
||||
Routes: []HTTPRouteBackend{
|
||||
{Backend: "127.0.0.1:8080", AuthEnabled: true},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
cfg := m.GenerateWithOpts(nil, nil, GenerateOpts{
|
||||
HTTPRoutes: httpRoutes,
|
||||
CertsDir: "/tmp/certs/",
|
||||
AuthEnabled: true,
|
||||
AuthBackend: "127.0.0.1:9091",
|
||||
AuthDomain: "auth.payne.io",
|
||||
})
|
||||
|
||||
// Count auth-intercept directives — should be 1 (for app), not 2 (auth domain exempt)
|
||||
count := strings.Count(cfg, "lua.auth-request")
|
||||
if count != 1 {
|
||||
t.Errorf("expected 1 auth-intercept directive (auth domain exempt), got %d", count)
|
||||
}
|
||||
|
||||
// The auth comment should only appear for app.payne.io
|
||||
if !strings.Contains(cfg, "# auth: app.payne.io") {
|
||||
t.Error("should have auth comment for app.payne.io")
|
||||
}
|
||||
if strings.Contains(cfg, "# auth: auth.payne.io") {
|
||||
t.Error("should NOT have auth comment for auth.payne.io (exempt)")
|
||||
}
|
||||
}
|
||||
|
||||
func TestGenerateWithOpts_NoAuthForUnprotectedRoutes(t *testing.T) {
|
||||
m := NewManager("")
|
||||
httpRoutes := []HTTPRoute{
|
||||
{
|
||||
Name: "public.payne.io",
|
||||
Domain: "public.payne.io",
|
||||
Routes: []HTTPRouteBackend{
|
||||
{Backend: "127.0.0.1:8080", AuthEnabled: false},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
cfg := m.GenerateWithOpts(nil, nil, GenerateOpts{
|
||||
HTTPRoutes: httpRoutes,
|
||||
CertsDir: "/tmp/certs/",
|
||||
AuthEnabled: true,
|
||||
AuthBackend: "127.0.0.1:9091",
|
||||
AuthDomain: "auth.payne.io",
|
||||
})
|
||||
|
||||
if strings.Contains(cfg, "lua.auth-request") {
|
||||
t.Error("should NOT contain auth-intercept for unprotected routes")
|
||||
}
|
||||
}
|
||||
|
||||
func TestGenerateWithOpts_AuthDisabledNoDirectives(t *testing.T) {
|
||||
m := NewManager("")
|
||||
httpRoutes := []HTTPRoute{
|
||||
{
|
||||
Name: "app.payne.io",
|
||||
Domain: "app.payne.io",
|
||||
Routes: []HTTPRouteBackend{
|
||||
{Backend: "127.0.0.1:8080", AuthEnabled: true},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
cfg := m.GenerateWithOpts(nil, nil, GenerateOpts{
|
||||
HTTPRoutes: httpRoutes,
|
||||
CertsDir: "/tmp/certs/",
|
||||
AuthEnabled: false,
|
||||
})
|
||||
|
||||
if strings.Contains(cfg, "lua.auth-request") {
|
||||
t.Error("should NOT contain auth directives when auth is globally disabled")
|
||||
}
|
||||
if strings.Contains(cfg, "be_authelia") {
|
||||
t.Error("should NOT contain authelia backend when auth is globally disabled")
|
||||
}
|
||||
}
|
||||
|
||||
func TestGenerateWithOpts_AuthWithHeaders(t *testing.T) {
|
||||
m := NewManager("")
|
||||
httpRoutes := []HTTPRoute{
|
||||
{
|
||||
Name: "app.payne.io",
|
||||
Domain: "app.payne.io",
|
||||
Routes: []HTTPRouteBackend{
|
||||
{
|
||||
Backend: "127.0.0.1:8080",
|
||||
AuthEnabled: true,
|
||||
Headers: &domains.HeaderConfig{
|
||||
Request: map[string]string{"X-Custom": "value"},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
cfg := m.GenerateWithOpts(nil, nil, GenerateOpts{
|
||||
HTTPRoutes: httpRoutes,
|
||||
CertsDir: "/tmp/certs/",
|
||||
AuthEnabled: true,
|
||||
AuthBackend: "127.0.0.1:9091",
|
||||
AuthDomain: "auth.payne.io",
|
||||
})
|
||||
|
||||
// Should have both auth and headers
|
||||
if !strings.Contains(cfg, "lua.auth-request") {
|
||||
t.Error("should contain auth-intercept")
|
||||
}
|
||||
if !strings.Contains(cfg, "X-Custom") {
|
||||
t.Error("should still contain custom headers alongside auth")
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user