// 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") }