e5623d2f84
- Add Infrastructure Health section with CPU/Memory/Disk panels - Add Service Status section with PM2/Docker panels - Create GrafanaPanel component for iframe embedding - Build passes successfully
245 lines
7.0 KiB
Go
245 lines
7.0 KiB
Go
package middleware
|
|
|
|
import (
|
|
"net/http"
|
|
"regexp"
|
|
"strings"
|
|
"time"
|
|
|
|
"golang.org/x/time/rate"
|
|
)
|
|
|
|
// ── Input Validation ───────────────────────────────────────────────────────
|
|
|
|
var (
|
|
emailRegex = regexp.MustCompile(`^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$`)
|
|
uuidRegex = regexp.MustCompile(`^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$`)
|
|
phoneRegex = regexp.MustCompile(`^[+0-9\s()-]{8,20}$`)
|
|
orgNumRegex = regexp.MustCompile(`^\d{6}-\d{4}$`)
|
|
)
|
|
|
|
// ValidateEmail kontrollerar email-format
|
|
func ValidateEmail(email string) bool {
|
|
return emailRegex.MatchString(email)
|
|
}
|
|
|
|
// ValidateUUID kontrollerar UUID-format
|
|
func ValidateUUID(id string) bool {
|
|
return uuidRegex.MatchString(id)
|
|
}
|
|
|
|
// ValidatePhone kontrollerar telefonnummer
|
|
func ValidatePhone(phone string) bool {
|
|
return phoneRegex.MatchString(phone)
|
|
}
|
|
|
|
// ValidateOrgNumber kontrollerar svenskt orgnummer
|
|
func ValidateOrgNumber(org string) bool {
|
|
return orgNumRegex.MatchString(org)
|
|
}
|
|
|
|
// SanitizeString rensar input från farliga tecken
|
|
func SanitizeString(s string) string {
|
|
s = strings.TrimSpace(s)
|
|
// Ta bort potentiellt farliga tecken
|
|
s = strings.ReplaceAll(s, "<", "<")
|
|
s = strings.ReplaceAll(s, ">", ">")
|
|
s = strings.ReplaceAll(s, "\"", """)
|
|
return s
|
|
}
|
|
|
|
// ── RBAC (Role Based Access Control) ──────────────────────────────────────
|
|
|
|
type Permission string
|
|
|
|
const (
|
|
PermRead Permission = "read"
|
|
PermWrite Permission = "write"
|
|
PermDelete Permission = "delete"
|
|
PermAdmin Permission = "admin"
|
|
)
|
|
|
|
type Resource string
|
|
|
|
const (
|
|
ResCustomers Resource = "customers"
|
|
ResEmployees Resource = "employees"
|
|
ResFinance Resource = "finance"
|
|
ResLegal Resource = "legal"
|
|
ResHR Resource = "hr"
|
|
ResSettings Resource = "settings"
|
|
ResAudit Resource = "audit"
|
|
)
|
|
|
|
// RolePermissions definierar vilka permissions varje roll har
|
|
var RolePermissions = map[string]map[Resource][]Permission{
|
|
"admin": {
|
|
ResCustomers: {PermRead, PermWrite, PermDelete},
|
|
ResEmployees: {PermRead, PermWrite, PermDelete},
|
|
ResFinance: {PermRead, PermWrite, PermDelete},
|
|
ResLegal: {PermRead, PermWrite, PermDelete},
|
|
ResHR: {PermRead, PermWrite, PermDelete},
|
|
ResSettings: {PermRead, PermWrite, PermDelete},
|
|
ResAudit: {PermRead, PermWrite, PermDelete},
|
|
},
|
|
"manager": {
|
|
ResCustomers: {PermRead, PermWrite},
|
|
ResEmployees: {PermRead, PermWrite},
|
|
ResFinance: {PermRead},
|
|
ResLegal: {PermRead},
|
|
ResHR: {PermRead, PermWrite},
|
|
},
|
|
"user": {
|
|
ResCustomers: {PermRead},
|
|
ResEmployees: {PermRead},
|
|
ResFinance: {PermRead},
|
|
},
|
|
"viewer": {
|
|
ResCustomers: {PermRead},
|
|
ResEmployees: {PermRead},
|
|
},
|
|
}
|
|
|
|
// HasPermission kontrollerar om en roll har en specifik permission
|
|
func HasPermission(role string, resource Resource, permission Permission) bool {
|
|
perms, ok := RolePermissions[role]
|
|
if !ok {
|
|
return false
|
|
}
|
|
|
|
resourcePerms, ok := perms[resource]
|
|
if !ok {
|
|
return false
|
|
}
|
|
|
|
for _, p := range resourcePerms {
|
|
if p == permission || p == PermAdmin {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// RBACMiddleware kontrollerar behörigheter
|
|
func RBACMiddleware(resource Resource, permission Permission) func(http.Handler) http.Handler {
|
|
return func(next http.Handler) http.Handler {
|
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
// Hämta roll från context (satt av auth middleware)
|
|
role, ok := r.Context().Value("role").(string)
|
|
if !ok {
|
|
http.Error(w, `{"error":"unauthorized"}`, http.StatusUnauthorized)
|
|
return
|
|
}
|
|
|
|
if !HasPermission(role, resource, permission) {
|
|
http.Error(w, `{"error":"forbidden"}`, http.StatusForbidden)
|
|
return
|
|
}
|
|
|
|
next.ServeHTTP(w, r)
|
|
})
|
|
}
|
|
}
|
|
|
|
// ── Rate Limiting ─────────────────────────────────────────────────────────
|
|
|
|
type RateLimiter struct {
|
|
limiters map[string]*rate.Limiter
|
|
}
|
|
|
|
func NewRateLimiter() *RateLimiter {
|
|
return &RateLimiter{
|
|
limiters: make(map[string]*rate.Limiter),
|
|
}
|
|
}
|
|
|
|
func (rl *RateLimiter) GetLimiter(key string) *rate.Limiter {
|
|
limiter, ok := rl.limiters[key]
|
|
if !ok {
|
|
limiter = rate.NewLimiter(rate.Every(time.Second), 10) // 10 req/s
|
|
rl.limiters[key] = limiter
|
|
}
|
|
return limiter
|
|
}
|
|
|
|
// RateLimit middleware
|
|
func RateLimit(rl *RateLimiter) func(http.Handler) http.Handler {
|
|
return func(next http.Handler) http.Handler {
|
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
key := r.RemoteAddr
|
|
if !rl.GetLimiter(key).Allow() {
|
|
http.Error(w, `{"error":"rate limit exceeded"}`, http.StatusTooManyRequests)
|
|
return
|
|
}
|
|
next.ServeHTTP(w, r)
|
|
})
|
|
}
|
|
}
|
|
|
|
// ── Audit Log ─────────────────────────────────────────────────────────────
|
|
|
|
type AuditEvent struct {
|
|
Timestamp time.Time `json:"timestamp"`
|
|
UserID string `json:"user_id"`
|
|
Action string `json:"action"`
|
|
Resource string `json:"resource"`
|
|
ResourceID string `json:"resource_id,omitempty"`
|
|
IP string `json:"ip"`
|
|
UserAgent string `json:"user_agent"`
|
|
Success bool `json:"success"`
|
|
Details string `json:"details,omitempty"`
|
|
}
|
|
|
|
// AuditLog middleware loggar alla requests
|
|
func AuditLog(next http.Handler) http.Handler {
|
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
start := time.Now()
|
|
|
|
// Wrap response writer för att fånga status code
|
|
wrapped := &responseRecorder{ResponseWriter: w, statusCode: http.StatusOK}
|
|
|
|
next.ServeHTTP(wrapped, r)
|
|
|
|
// Logga audit event
|
|
event := AuditEvent{
|
|
Timestamp: start,
|
|
Action: r.Method,
|
|
Resource: r.URL.Path,
|
|
IP: r.RemoteAddr,
|
|
UserAgent: r.UserAgent(),
|
|
Success: wrapped.statusCode < 400,
|
|
}
|
|
|
|
// Hämta user ID från context om finns
|
|
if userID, ok := r.Context().Value("user_id").(string); ok {
|
|
event.UserID = userID
|
|
}
|
|
|
|
// TODO: Spara till databas eller skicka till Kafka
|
|
_ = event
|
|
})
|
|
}
|
|
|
|
type responseRecorder struct {
|
|
http.ResponseWriter
|
|
statusCode int
|
|
}
|
|
|
|
func (rr *responseRecorder) WriteHeader(code int) {
|
|
rr.statusCode = code
|
|
rr.ResponseWriter.WriteHeader(code)
|
|
}
|
|
|
|
// ── Security Headers ──────────────────────────────────────────────────────
|
|
|
|
func SecurityHeaders(next http.Handler) http.Handler {
|
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
w.Header().Set("X-Content-Type-Options", "nosniff")
|
|
w.Header().Set("X-Frame-Options", "DENY")
|
|
w.Header().Set("X-XSS-Protection", "1; mode=block")
|
|
w.Header().Set("Referrer-Policy", "strict-origin-when-cross-origin")
|
|
w.Header().Set("Content-Security-Policy", "default-src 'self'")
|
|
next.ServeHTTP(w, r)
|
|
})
|
|
}
|