feat(auth): replace bearer token with OIDC session + admin role check, auto-fix roles from groups

This commit is contained in:
Claus Lohmar 2026-06-14 14:56:39 +00:00
parent 3f2a7b81cf
commit 3c365e8206
3 changed files with 97 additions and 10 deletions

View file

@ -107,10 +107,16 @@ func (sw *SyncWriter) Bootstrap() (int, error) {
groups += g
}
// Determine role from groups
role := "user"
if containsGroup(groups, "admins") {
role = "admin"
}
_, err := sw.store.GetDB().Exec(`
INSERT INTO users (username, display_name, email, groups, password_hash, disabled, updated_at)
VALUES (?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP)
`, username, entry.DisplayName, entry.Email, groups, entry.Password, entry.Disabled)
INSERT INTO users (username, display_name, email, role, groups, password_hash, disabled, updated_at)
VALUES (?, ?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP)
`, username, entry.DisplayName, entry.Email, role, groups, entry.Password, entry.Disabled)
if err != nil {
return imported, fmt.Errorf("import user %s: %w", username, err)
}
@ -119,3 +125,27 @@ func (sw *SyncWriter) Bootstrap() (int, error) {
return imported, nil
}
// FixRoles updates existing users' roles based on their groups.
func (sw *SyncWriter) FixRoles() (int, error) {
users, err := sw.store.List()
if err != nil {
return 0, err
}
fixed := 0
for _, u := range users {
expectedRole := "user"
if containsGroup(u.Groups, "admins") {
expectedRole = "admin"
}
if u.Role != expectedRole {
_, err := sw.store.GetDB().Exec("UPDATE users SET role = ? WHERE username = ?", expectedRole, u.Username)
if err != nil {
return fixed, err
}
fixed++
}
}
return fixed, nil
}

View file

@ -0,0 +1,49 @@
package auth
import (
"database/sql"
"net/http"
)
// RoleChecker validates that the session user has the required role.
type RoleChecker struct {
db *sql.DB
}
// NewRoleChecker creates a role checker backed by the database.
func NewRoleChecker(db *sql.DB) *RoleChecker {
return &RoleChecker{db: db}
}
// RequireAdmin is middleware that allows only users with the "admin" role.
// Must run after SessionMiddleware has populated the context.
func (rc *RoleChecker) RequireAdmin(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
userID, ok := GetUserID(r)
if !ok {
http.Error(w, `{"error":"unauthorized"}`, http.StatusUnauthorized)
return
}
isAdmin, err := rc.isAdmin(userID)
if err != nil || !isAdmin {
http.Error(w, `{"error":"forbidden"}`, http.StatusForbidden)
return
}
next.ServeHTTP(w, r)
})
}
// isAdmin checks if a user has the admin role.
func (rc *RoleChecker) isAdmin(username string) (bool, error) {
var role string
err := rc.db.QueryRow("SELECT role FROM users WHERE username = ?", username).Scan(&role)
if err == sql.ErrNoRows {
return false, nil
}
if err != nil {
return false, err
}
return role == "admin", nil
}

View file

@ -60,11 +60,19 @@ func main() {
logger.Info("bootstrapped authelia users", "count", imported)
}
// Fix existing user roles based on groups
if fixed, err := syncWriter.FixRoles(); err != nil {
logger.Warn("fix roles", "error", err)
} else if fixed > 0 {
logger.Info("fixed user roles", "count", fixed)
}
// Create admin handler
adminHandler := admin.NewHandler(userStore, syncWriter, logger)
// Initialize session store and OIDC auth
sessionStore := auth.NewSessionStore(database.DB)
roleChecker := auth.NewRoleChecker(database.DB)
// OIDC issuer: public-facing URL (via Zoraxy) for browser redirects
// Falls back to authelia.host if not configured
@ -122,13 +130,13 @@ func main() {
}
uiHandler.RegisterRoutes(mux, combinedAuth)
// --- Admin API routes (JSON, protected by bearer token) ---
adminAuth := admin.TokenAuthMiddleware(cfg.Admin.SecretToken)
adminHandler.RegisterRoutes(mux, adminAuth)
// --- Admin UI routes (Templ-rendered HTML, protected by OIDC session) ---
adminHandler.RegisterUIRoutes(mux, combinedAuth)
adminHandler.RegisterHTMXRoutes(mux, combinedAuth)
// --- Admin routes: session auth + admin role check ---
adminRoleAuth := func(next http.Handler) http.Handler {
return sessionStore.SessionMiddleware(roleChecker.RequireAdmin(next))
}
adminHandler.RegisterRoutes(mux, adminRoleAuth)
adminHandler.RegisterUIRoutes(mux, adminRoleAuth)
adminHandler.RegisterHTMXRoutes(mux, adminRoleAuth)
// --- OIDC config page — shows Authelia status ---
mux.HandleFunc("GET /auth/status", func(w http.ResponseWriter, r *http.Request) {