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:
156
internal/authelia/manager.go
Normal file
156
internal/authelia/manager.go
Normal file
@@ -0,0 +1,156 @@
|
||||
// Package authelia manages the Authelia authentication service.
|
||||
// Authelia provides centralized authentication for network services via
|
||||
// HAProxy forward-auth and acts as an OIDC provider for apps with native support.
|
||||
package authelia
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Status represents the current state of the Authelia service
|
||||
type Status struct {
|
||||
Active bool `json:"active"`
|
||||
Version string `json:"version,omitempty"`
|
||||
PID int `json:"pid,omitempty"`
|
||||
LastRestart time.Time `json:"lastRestart,omitempty"`
|
||||
}
|
||||
|
||||
// Manager handles Authelia configuration and service lifecycle
|
||||
type Manager struct {
|
||||
dataDir string // e.g. /var/lib/wild-central/authelia
|
||||
}
|
||||
|
||||
// NewManager creates a new Authelia manager
|
||||
func NewManager(dataDir string) *Manager {
|
||||
return &Manager{dataDir: dataDir}
|
||||
}
|
||||
|
||||
// EnsureDataDir creates the Authelia data directory if it doesn't exist
|
||||
func (m *Manager) EnsureDataDir() error {
|
||||
if err := os.MkdirAll(m.dataDir, 0700); err != nil {
|
||||
return fmt.Errorf("creating authelia data dir: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// IsInstalled checks if the authelia binary exists on PATH
|
||||
func (m *Manager) IsInstalled() bool {
|
||||
_, err := exec.LookPath("authelia")
|
||||
return err == nil
|
||||
}
|
||||
|
||||
const luaScriptPath = "/etc/haproxy/lua/haproxy-auth-request.lua"
|
||||
|
||||
// HasLuaScript checks if the HAProxy auth-request Lua script is installed
|
||||
func (m *Manager) HasLuaScript() bool {
|
||||
info, err := os.Stat(luaScriptPath)
|
||||
return err == nil && info.Size() > 0
|
||||
}
|
||||
|
||||
// GetStatus returns the current Authelia service status
|
||||
func (m *Manager) GetStatus() (*Status, error) {
|
||||
status := &Status{}
|
||||
|
||||
cmd := exec.Command("systemctl", "is-active", "--quiet", "authelia")
|
||||
status.Active = cmd.Run() == nil
|
||||
|
||||
if status.Active {
|
||||
if out, err := exec.Command("systemctl", "show", "authelia.service", "--property=MainPID").Output(); err == nil {
|
||||
parts := strings.Split(strings.TrimSpace(string(out)), "=")
|
||||
if len(parts) == 2 {
|
||||
if pid, err := strconv.Atoi(parts[1]); err == nil {
|
||||
status.PID = pid
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if out, err := exec.Command("systemctl", "show", "authelia.service", "--property=ActiveEnterTimestamp").Output(); err == nil {
|
||||
parts := strings.Split(strings.TrimSpace(string(out)), "=")
|
||||
if len(parts) == 2 {
|
||||
if t, err := time.Parse("Mon 2006-01-02 15:04:05 MST", parts[1]); err == nil {
|
||||
status.LastRestart = t
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if out, err := exec.Command("authelia", "--version").Output(); err == nil {
|
||||
s := string(out)
|
||||
if i := strings.Index(s, "version "); i >= 0 {
|
||||
v := s[i+8:]
|
||||
if j := strings.IndexByte(v, ' '); j > 0 {
|
||||
v = v[:j]
|
||||
}
|
||||
status.Version = strings.TrimSpace(v)
|
||||
}
|
||||
}
|
||||
|
||||
return status, nil
|
||||
}
|
||||
|
||||
// RestartService restarts the Authelia systemd service
|
||||
func (m *Manager) RestartService() error {
|
||||
cmd := exec.Command("systemctl", "restart", "authelia.service")
|
||||
output, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to restart authelia: %w (output: %s)", err, string(output))
|
||||
}
|
||||
slog.Info("authelia service restarted", "component", "authelia")
|
||||
return nil
|
||||
}
|
||||
|
||||
// StopService stops the Authelia systemd service
|
||||
func (m *Manager) StopService() error {
|
||||
cmd := exec.Command("systemctl", "stop", "authelia.service")
|
||||
output, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to stop authelia: %w (output: %s)", err, string(output))
|
||||
}
|
||||
slog.Info("authelia service stopped", "component", "authelia")
|
||||
return nil
|
||||
}
|
||||
|
||||
// ReadConfig reads the current Authelia configuration file
|
||||
func (m *Manager) ReadConfig() (string, error) {
|
||||
data, err := os.ReadFile(m.configPath())
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("reading authelia config: %w", err)
|
||||
}
|
||||
return string(data), nil
|
||||
}
|
||||
|
||||
// DataDir returns the Authelia data directory path
|
||||
func (m *Manager) DataDir() string {
|
||||
return m.dataDir
|
||||
}
|
||||
|
||||
func (m *Manager) configPath() string {
|
||||
return filepath.Join(m.dataDir, "configuration.yml")
|
||||
}
|
||||
|
||||
func (m *Manager) usersDBPath() string {
|
||||
return filepath.Join(m.dataDir, "users_database.yml")
|
||||
}
|
||||
|
||||
func (m *Manager) sqlitePath() string {
|
||||
return filepath.Join(m.dataDir, "db.sqlite3")
|
||||
}
|
||||
|
||||
func (m *Manager) jwksPath() string {
|
||||
return filepath.Join(m.dataDir, "jwks.pem")
|
||||
}
|
||||
|
||||
func (m *Manager) notificationPath() string {
|
||||
return filepath.Join(m.dataDir, "notification.txt")
|
||||
}
|
||||
|
||||
func (m *Manager) oidcClientsPath() string {
|
||||
return filepath.Join(m.dataDir, "oidc_clients.yml")
|
||||
}
|
||||
Reference in New Issue
Block a user