feat(auth): replace bearer token with OIDC session + admin role check, auto-fix roles from groups
This commit is contained in:
parent
3f2a7b81cf
commit
3c365e8206
3 changed files with 97 additions and 10 deletions
|
|
@ -107,10 +107,16 @@ func (sw *SyncWriter) Bootstrap() (int, error) {
|
||||||
groups += g
|
groups += g
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Determine role from groups
|
||||||
|
role := "user"
|
||||||
|
if containsGroup(groups, "admins") {
|
||||||
|
role = "admin"
|
||||||
|
}
|
||||||
|
|
||||||
_, err := sw.store.GetDB().Exec(`
|
_, err := sw.store.GetDB().Exec(`
|
||||||
INSERT INTO users (username, display_name, email, groups, password_hash, disabled, updated_at)
|
INSERT INTO users (username, display_name, email, role, groups, password_hash, disabled, updated_at)
|
||||||
VALUES (?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP)
|
VALUES (?, ?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP)
|
||||||
`, username, entry.DisplayName, entry.Email, groups, entry.Password, entry.Disabled)
|
`, username, entry.DisplayName, entry.Email, role, groups, entry.Password, entry.Disabled)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return imported, fmt.Errorf("import user %s: %w", username, err)
|
return imported, fmt.Errorf("import user %s: %w", username, err)
|
||||||
}
|
}
|
||||||
|
|
@ -119,3 +125,27 @@ func (sw *SyncWriter) Bootstrap() (int, error) {
|
||||||
|
|
||||||
return imported, nil
|
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
|
||||||
|
}
|
||||||
|
|
|
||||||
49
src/core/auth/middleware.go
Normal file
49
src/core/auth/middleware.go
Normal 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
|
||||||
|
}
|
||||||
22
src/main.go
22
src/main.go
|
|
@ -60,11 +60,19 @@ func main() {
|
||||||
logger.Info("bootstrapped authelia users", "count", imported)
|
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
|
// Create admin handler
|
||||||
adminHandler := admin.NewHandler(userStore, syncWriter, logger)
|
adminHandler := admin.NewHandler(userStore, syncWriter, logger)
|
||||||
|
|
||||||
// Initialize session store and OIDC auth
|
// Initialize session store and OIDC auth
|
||||||
sessionStore := auth.NewSessionStore(database.DB)
|
sessionStore := auth.NewSessionStore(database.DB)
|
||||||
|
roleChecker := auth.NewRoleChecker(database.DB)
|
||||||
|
|
||||||
// OIDC issuer: public-facing URL (via Zoraxy) for browser redirects
|
// OIDC issuer: public-facing URL (via Zoraxy) for browser redirects
|
||||||
// Falls back to authelia.host if not configured
|
// Falls back to authelia.host if not configured
|
||||||
|
|
@ -122,13 +130,13 @@ func main() {
|
||||||
}
|
}
|
||||||
uiHandler.RegisterRoutes(mux, combinedAuth)
|
uiHandler.RegisterRoutes(mux, combinedAuth)
|
||||||
|
|
||||||
// --- Admin API routes (JSON, protected by bearer token) ---
|
// --- Admin routes: session auth + admin role check ---
|
||||||
adminAuth := admin.TokenAuthMiddleware(cfg.Admin.SecretToken)
|
adminRoleAuth := func(next http.Handler) http.Handler {
|
||||||
adminHandler.RegisterRoutes(mux, adminAuth)
|
return sessionStore.SessionMiddleware(roleChecker.RequireAdmin(next))
|
||||||
|
}
|
||||||
// --- Admin UI routes (Templ-rendered HTML, protected by OIDC session) ---
|
adminHandler.RegisterRoutes(mux, adminRoleAuth)
|
||||||
adminHandler.RegisterUIRoutes(mux, combinedAuth)
|
adminHandler.RegisterUIRoutes(mux, adminRoleAuth)
|
||||||
adminHandler.RegisterHTMXRoutes(mux, combinedAuth)
|
adminHandler.RegisterHTMXRoutes(mux, adminRoleAuth)
|
||||||
|
|
||||||
// --- OIDC config page — shows Authelia status ---
|
// --- OIDC config page — shows Authelia status ---
|
||||||
mux.HandleFunc("GET /auth/status", func(w http.ResponseWriter, r *http.Request) {
|
mux.HandleFunc("GET /auth/status", func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue