Add RBAC support with roles, permissions, and session management. Implement middleware for authentication and CSRF protection. Enhance audit logging with additional fields. Update HTTP handlers and routes for new features.

This commit is contained in:
2025-12-13 17:44:09 +00:00
parent d69e01bbaf
commit 8100f87686
44 changed files with 3262 additions and 76 deletions

View File

@@ -2,9 +2,14 @@ package http
import (
"context"
"crypto/rand"
"encoding/base64"
"log"
"net/http"
"strings"
"time"
"github.com/example/storage-appliance/internal/auth"
)
// ContextKey used to store values in context
@@ -12,6 +17,9 @@ type ContextKey string
const (
ContextKeyRequestID ContextKey = "request-id"
ContextKeyUser ContextKey = "user"
ContextKeyUserID ContextKey = "user.id"
ContextKeySession ContextKey = "session"
)
// RequestID middleware sets a request ID in headers and request context
@@ -30,49 +38,170 @@ func Logging(next http.Handler) http.Handler {
})
}
// Auth middleware placeholder to authenticate users
func Auth(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Basic dev auth: read X-Auth-User; in real world, validate session/jwt
username := r.Header.Get("X-Auth-User")
if username == "" {
username = "anonymous"
}
// Role hint: header X-Auth-Role (admin/operator/viewer)
role := r.Header.Get("X-Auth-Role")
if role == "" {
if username == "admin" {
role = "admin"
} else {
role = "viewer"
// AuthMiddleware creates an auth middleware that uses the provided App
func AuthMiddleware(app *App) func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Skip auth for login and public routes
if strings.HasPrefix(r.URL.Path, "/login") || strings.HasPrefix(r.URL.Path, "/static") || r.URL.Path == "/healthz" || r.URL.Path == "/metrics" {
next.ServeHTTP(w, r)
return
}
}
ctx := context.WithValue(r.Context(), ContextKey("user"), username)
ctx = context.WithValue(ctx, ContextKey("user.role"), role)
next.ServeHTTP(w, r.WithContext(ctx))
})
// Get session token from cookie
cookie, err := r.Cookie(auth.SessionCookieName)
if err != nil {
// No session, redirect to login
if r.Header.Get("HX-Request") == "true" {
w.Header().Set("HX-Redirect", "/login")
w.WriteHeader(http.StatusUnauthorized)
} else {
http.Redirect(w, r, "/login", http.StatusFound)
}
return
}
// Validate session
sessionStore := auth.NewSessionStore(app.DB)
session, err := sessionStore.GetSession(r.Context(), cookie.Value)
if err != nil {
// Invalid session, redirect to login
if r.Header.Get("HX-Request") == "true" {
w.Header().Set("HX-Redirect", "/login")
w.WriteHeader(http.StatusUnauthorized)
} else {
http.Redirect(w, r, "/login", http.StatusFound)
}
return
}
// Get user
userStore := auth.NewUserStore(app.DB)
user, err := userStore.GetUserByID(r.Context(), session.UserID)
if err != nil {
http.Error(w, "user not found", http.StatusUnauthorized)
return
}
// Store user info in context
ctx := context.WithValue(r.Context(), ContextKeyUser, user.Username)
ctx = context.WithValue(ctx, ContextKeyUserID, user.ID)
ctx = context.WithValue(ctx, ContextKeySession, session)
next.ServeHTTP(w, r.WithContext(ctx))
})
}
}
// CSRF middleware placeholder (reads X-CSRF-Token)
func CSRFMiddleware(next http.Handler) http.Handler {
// Auth is a legacy wrapper for backward compatibility
func Auth(next http.Handler) http.Handler {
// This will be replaced by AuthMiddleware in router
return next
}
// RequireAuth middleware ensures user is authenticated (alternative to Auth that doesn't redirect)
func RequireAuth(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// TODO: check and enforce CSRF tokens for mutating requests
userID := r.Context().Value(ContextKeyUserID)
if userID == nil {
http.Error(w, "unauthorized", http.StatusUnauthorized)
return
}
next.ServeHTTP(w, r)
})
}
// RBAC middleware placeholder
func RBAC(permission string) func(http.Handler) http.Handler {
// CSRFMiddleware creates a CSRF middleware that uses the provided App
func CSRFMiddleware(app *App) func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Try to read role from context and permit admin always
role := r.Context().Value(ContextKey("user.role"))
if role == "admin" {
// For safe methods, ensure CSRF token cookie exists
if r.Method == "GET" || r.Method == "HEAD" || r.Method == "OPTIONS" {
// Set CSRF token cookie if it doesn't exist
if cookie, err := r.Cookie("csrf_token"); err != nil || cookie.Value == "" {
token := generateCSRFToken()
http.SetCookie(w, &http.Cookie{
Name: "csrf_token",
Value: token,
Path: "/",
HttpOnly: false, // Needed for HTMX to read it
Secure: false,
SameSite: http.SameSiteStrictMode,
MaxAge: 86400, // 24 hours
})
}
next.ServeHTTP(w, r)
return
}
// For now, only admin is permitted; add permission checks here
// Get CSRF token from header (HTMX compatible) or form
token := r.Header.Get("X-CSRF-Token")
if token == "" {
token = r.FormValue("csrf_token")
}
// Get expected token from cookie
expectedToken := getCSRFToken(r)
if token == "" || token != expectedToken {
http.Error(w, "invalid CSRF token", http.StatusForbidden)
return
}
next.ServeHTTP(w, r)
})
}
}
// getCSRFToken retrieves or generates a CSRF token for the session
func getCSRFToken(r *http.Request) string {
// Try to get from cookie first
cookie, err := r.Cookie("csrf_token")
if err == nil && cookie.Value != "" {
return cookie.Value
}
// Generate new token (will be set in cookie by handler)
return generateCSRFToken()
}
func generateCSRFToken() string {
b := make([]byte, 32)
rand.Read(b)
return base64.URLEncoding.EncodeToString(b)
}
// RequirePermission creates a permission check middleware
func RequirePermission(app *App, permission string) func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
userID := r.Context().Value(ContextKeyUserID)
if userID == nil {
http.Error(w, "unauthorized", http.StatusUnauthorized)
return
}
rbacStore := auth.NewRBACStore(app.DB)
hasPermission, err := rbacStore.UserHasPermission(r.Context(), userID.(string), permission)
if err != nil {
log.Printf("permission check error: %v", err)
http.Error(w, "internal error", http.StatusInternalServerError)
return
}
if !hasPermission {
http.Error(w, "forbidden", http.StatusForbidden)
return
}
next.ServeHTTP(w, r)
})
}
}
// RBAC middleware (kept for backward compatibility)
func RBAC(permission string) func(http.Handler) http.Handler {
// This will be replaced by RequirePermission in router
return func(next http.Handler) http.Handler {
return next
}
}