355 lines
11 KiB
Go
355 lines
11 KiB
Go
package dashboard
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"net/http"
|
|
"strings"
|
|
)
|
|
|
|
// ContextKey type for context keys
|
|
type ContextKey string
|
|
|
|
const (
|
|
UserContextKey ContextKey = "user"
|
|
)
|
|
|
|
// SecurityManager combines authentication and CSRF protection
|
|
type SecurityManager struct {
|
|
authManager *AuthManager
|
|
csrfManager *CSRFManager
|
|
validator *InputValidator
|
|
}
|
|
|
|
// NewSecurityManager creates a new security manager
|
|
func NewSecurityManager(authManager *AuthManager) *SecurityManager {
|
|
return &SecurityManager{
|
|
authManager: authManager,
|
|
csrfManager: NewCSRFManager(),
|
|
validator: NewInputValidator(),
|
|
}
|
|
}
|
|
|
|
// AuthMiddleware provides authentication for web pages
|
|
func (sm *SecurityManager) AuthMiddleware(next http.HandlerFunc) http.HandlerFunc {
|
|
return func(w http.ResponseWriter, r *http.Request) {
|
|
// Skip authentication for login page and static assets
|
|
if r.URL.Path == "/login" || r.URL.Path == "/api/auth/login" ||
|
|
strings.HasPrefix(r.URL.Path, "/static/") {
|
|
next(w, r)
|
|
return
|
|
}
|
|
|
|
// Check for session token in cookie
|
|
cookie, err := r.Cookie("session_token")
|
|
if err != nil {
|
|
sm.redirectToLogin(w, r)
|
|
return
|
|
}
|
|
|
|
user, err := sm.authManager.ValidateSession(cookie.Value)
|
|
if err != nil {
|
|
sm.redirectToLogin(w, r)
|
|
return
|
|
}
|
|
|
|
// Add user to request context
|
|
ctx := context.WithValue(r.Context(), UserContextKey, user)
|
|
next(w, r.WithContext(ctx))
|
|
}
|
|
}
|
|
|
|
// APIAuthMiddleware provides authentication for API endpoints
|
|
func (sm *SecurityManager) APIAuthMiddleware(next http.HandlerFunc) http.HandlerFunc {
|
|
return func(w http.ResponseWriter, r *http.Request) {
|
|
var user *User
|
|
var err error
|
|
|
|
// Check for API key in header
|
|
apiKey := r.Header.Get("X-API-Key")
|
|
if apiKey != "" {
|
|
user, err = sm.authManager.ValidateAPIKey(apiKey)
|
|
if err != nil {
|
|
sm.sendJSONError(w, "Invalid API key", http.StatusUnauthorized)
|
|
return
|
|
}
|
|
} else {
|
|
// Check for session token in cookie (for web-based API calls)
|
|
cookie, err := r.Cookie("session_token")
|
|
if err != nil {
|
|
sm.sendJSONError(w, "Authentication required", http.StatusUnauthorized)
|
|
return
|
|
}
|
|
|
|
user, err = sm.authManager.ValidateSession(cookie.Value)
|
|
if err != nil {
|
|
sm.sendJSONError(w, "Invalid session", http.StatusUnauthorized)
|
|
return
|
|
}
|
|
}
|
|
|
|
// Add user to request context
|
|
ctx := context.WithValue(r.Context(), UserContextKey, user)
|
|
next(w, r.WithContext(ctx))
|
|
}
|
|
}
|
|
|
|
// RoleMiddleware checks if user has required role
|
|
func (sm *SecurityManager) RoleMiddleware(requiredRole string) func(http.HandlerFunc) http.HandlerFunc {
|
|
return func(next http.HandlerFunc) http.HandlerFunc {
|
|
return func(w http.ResponseWriter, r *http.Request) {
|
|
user := sm.GetUserFromContext(r.Context())
|
|
if user == nil {
|
|
http.Error(w, "Unauthorized", http.StatusUnauthorized)
|
|
return
|
|
}
|
|
|
|
if !sm.hasPermission(user.Role, requiredRole) {
|
|
http.Error(w, "Insufficient permissions", http.StatusForbidden)
|
|
return
|
|
}
|
|
|
|
next(w, r)
|
|
}
|
|
}
|
|
}
|
|
|
|
// CSRFMiddleware provides CSRF protection
|
|
func (sm *SecurityManager) CSRFMiddleware(next http.HandlerFunc) http.HandlerFunc {
|
|
return sm.csrfManager.CSRFMiddleware(next)
|
|
}
|
|
|
|
// GetUserFromContext extracts user from request context
|
|
func (sm *SecurityManager) GetUserFromContext(ctx context.Context) *User {
|
|
user, ok := ctx.Value(UserContextKey).(*User)
|
|
if !ok {
|
|
return nil
|
|
}
|
|
return user
|
|
}
|
|
|
|
// hasPermission checks if a role has permission for an action
|
|
func (sm *SecurityManager) hasPermission(userRole, requiredRole string) bool {
|
|
roleHierarchy := map[string]int{
|
|
"readonly": 1,
|
|
"user": 2,
|
|
"admin": 3,
|
|
}
|
|
|
|
userLevel, exists := roleHierarchy[userRole]
|
|
if !exists {
|
|
return false
|
|
}
|
|
|
|
requiredLevel, exists := roleHierarchy[requiredRole]
|
|
if !exists {
|
|
return false
|
|
}
|
|
|
|
return userLevel >= requiredLevel
|
|
}
|
|
|
|
// redirectToLogin redirects to login page
|
|
func (sm *SecurityManager) redirectToLogin(w http.ResponseWriter, r *http.Request) {
|
|
// If it's an AJAX request, return JSON error
|
|
if r.Header.Get("X-Requested-With") == "XMLHttpRequest" ||
|
|
strings.Contains(r.Header.Get("Accept"), "application/json") {
|
|
sm.sendJSONError(w, "Authentication required", http.StatusUnauthorized)
|
|
return
|
|
}
|
|
|
|
// Otherwise redirect to login page
|
|
http.Redirect(w, r, "/login?redirect="+r.URL.Path, http.StatusSeeOther)
|
|
}
|
|
|
|
// sendJSONError sends a JSON error response
|
|
func (sm *SecurityManager) sendJSONError(w http.ResponseWriter, message string, statusCode int) {
|
|
w.Header().Set("Content-Type", "application/json")
|
|
w.WriteHeader(statusCode)
|
|
json.NewEncoder(w).Encode(map[string]string{"error": message})
|
|
}
|
|
|
|
// AddCSRFToken adds CSRF token to response data
|
|
func (sm *SecurityManager) AddCSRFToken(w http.ResponseWriter, data map[string]interface{}) error {
|
|
return sm.csrfManager.AddCSRFTokenToResponse(w, data)
|
|
}
|
|
|
|
// ValidateInput validates input using the input validator
|
|
func (sm *SecurityManager) ValidateInput() *InputValidator {
|
|
return sm.validator
|
|
}
|
|
|
|
// GenerateCSRFToken generates a new CSRF token
|
|
func (sm *SecurityManager) GenerateCSRFToken() (string, error) {
|
|
return sm.csrfManager.GenerateToken()
|
|
}
|
|
|
|
// LoginHandler handles user login
|
|
func (sm *SecurityManager) LoginHandler(w http.ResponseWriter, r *http.Request) {
|
|
if r.Method == "GET" {
|
|
// Show login form
|
|
sm.showLoginForm(w, r)
|
|
return
|
|
}
|
|
|
|
if r.Method == "POST" {
|
|
sm.handleLogin(w, r)
|
|
return
|
|
}
|
|
|
|
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
|
}
|
|
|
|
// LogoutHandler handles user logout
|
|
func (sm *SecurityManager) LogoutHandler(w http.ResponseWriter, r *http.Request) {
|
|
// Get session token from cookie
|
|
cookie, err := r.Cookie("session_token")
|
|
if err == nil {
|
|
// Delete session from database
|
|
sm.authManager.DeleteSession(cookie.Value)
|
|
}
|
|
|
|
// Clear session cookie
|
|
http.SetCookie(w, &http.Cookie{
|
|
Name: "session_token",
|
|
Value: "",
|
|
Path: "/",
|
|
MaxAge: -1,
|
|
HttpOnly: true,
|
|
Secure: r.TLS != nil,
|
|
SameSite: http.SameSiteStrictMode,
|
|
})
|
|
|
|
// Redirect to login page
|
|
http.Redirect(w, r, "/login", http.StatusSeeOther)
|
|
}
|
|
|
|
// showLoginForm displays the login form
|
|
func (sm *SecurityManager) showLoginForm(w http.ResponseWriter, r *http.Request) {
|
|
data := map[string]interface{}{
|
|
"Title": "Login",
|
|
"Redirect": r.URL.Query().Get("redirect"),
|
|
"Error": r.URL.Query().Get("error"),
|
|
}
|
|
|
|
// Add CSRF token
|
|
sm.AddCSRFToken(w, data)
|
|
|
|
// For now, return a simple HTML form
|
|
// This will be replaced with proper template rendering
|
|
w.Header().Set("Content-Type", "text/html")
|
|
w.Write([]byte(`
|
|
<!DOCTYPE html>
|
|
<html class="dark h-full">
|
|
<head>
|
|
<meta charset="utf-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
<title>Login - Honeypot Dashboard</title>
|
|
<script src="https://cdn.tailwindcss.com"></script>
|
|
<script>
|
|
tailwind.config = {
|
|
darkMode: 'class',
|
|
theme: {
|
|
extend: {
|
|
colors: {
|
|
primary: {
|
|
50: '#eff6ff', 100: '#dbeafe', 200: '#bfdbfe', 300: '#93c5fd', 400: '#60a5fa',
|
|
500: '#3b82f6', 600: '#2563eb', 700: '#1d4ed8', 800: '#1e40af', 900: '#1e3a8a'
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
</script>
|
|
</head>
|
|
<body class="h-full bg-gray-900">
|
|
<div class="min-h-full flex items-center justify-center py-12 px-4 sm:px-6 lg:px-8">
|
|
<div class="max-w-md w-full space-y-8">
|
|
<div>
|
|
<h2 class="mt-6 text-center text-3xl font-extrabold text-white">
|
|
Sign in to Honeypot Dashboard
|
|
</h2>
|
|
</div>
|
|
<form class="mt-8 space-y-6" method="POST">
|
|
<input type="hidden" name="csrf_token" value="` + data["CSRFToken"].(string) + `">
|
|
<input type="hidden" name="redirect" value="` + data["Redirect"].(string) + `">
|
|
<div class="rounded-md shadow-sm -space-y-px">
|
|
<div>
|
|
<label for="username" class="sr-only">Username</label>
|
|
<input id="username" name="username" type="text" required
|
|
class="appearance-none rounded-none relative block w-full px-3 py-2 border border-gray-700 bg-gray-800 text-gray-100 placeholder-gray-400 rounded-t-md focus:outline-none focus:ring-primary-500 focus:border-primary-500 focus:z-10 sm:text-sm"
|
|
placeholder="Username">
|
|
</div>
|
|
<div>
|
|
<label for="password" class="sr-only">Password</label>
|
|
<input id="password" name="password" type="password" required
|
|
class="appearance-none rounded-none relative block w-full px-3 py-2 border border-gray-700 bg-gray-800 text-gray-100 placeholder-gray-400 rounded-b-md focus:outline-none focus:ring-primary-500 focus:border-primary-500 focus:z-10 sm:text-sm"
|
|
placeholder="Password">
|
|
</div>
|
|
</div>
|
|
<div>
|
|
<button type="submit"
|
|
class="group relative w-full flex justify-center py-2 px-4 border border-transparent text-sm font-medium rounded-md text-white bg-primary-600 hover:bg-primary-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary-500">
|
|
Sign in
|
|
</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
</body>
|
|
</html>
|
|
`))
|
|
}
|
|
|
|
// handleLogin processes login form submission
|
|
func (sm *SecurityManager) handleLogin(w http.ResponseWriter, r *http.Request) {
|
|
// Parse form data
|
|
err := r.ParseForm()
|
|
if err != nil {
|
|
http.Redirect(w, r, "/login?error=Invalid+form+data", http.StatusSeeOther)
|
|
return
|
|
}
|
|
|
|
username := r.FormValue("username")
|
|
password := r.FormValue("password")
|
|
redirect := r.FormValue("redirect")
|
|
|
|
// Validate input
|
|
if err := sm.validator.ValidateUsername(username); err != nil {
|
|
http.Redirect(w, r, "/login?error="+err.Error(), http.StatusSeeOther)
|
|
return
|
|
}
|
|
|
|
// Authenticate user
|
|
user, err := sm.authManager.AuthenticateUser(username, password)
|
|
if err != nil {
|
|
http.Redirect(w, r, "/login?error=Invalid+credentials", http.StatusSeeOther)
|
|
return
|
|
}
|
|
|
|
// Create session
|
|
session, err := sm.authManager.CreateSession(user.ID, r.RemoteAddr, r.UserAgent())
|
|
if err != nil {
|
|
http.Redirect(w, r, "/login?error=Failed+to+create+session", http.StatusSeeOther)
|
|
return
|
|
}
|
|
|
|
// Set session cookie
|
|
http.SetCookie(w, &http.Cookie{
|
|
Name: "session_token",
|
|
Value: session.Token,
|
|
Path: "/",
|
|
Expires: session.ExpiresAt,
|
|
HttpOnly: true,
|
|
Secure: r.TLS != nil,
|
|
SameSite: http.SameSiteStrictMode,
|
|
})
|
|
|
|
// Redirect to intended page or dashboard
|
|
if redirect != "" && redirect != "/login" {
|
|
http.Redirect(w, r, redirect, http.StatusSeeOther)
|
|
} else {
|
|
http.Redirect(w, r, "/", http.StatusSeeOther)
|
|
}
|
|
}
|