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:
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user