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
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
|
|
|||
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)
|
||||
}
|
||||
|
||||
// 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) {
|
||||
|
|
|
|||
Loading…
Reference in a new issue