Files
OrchestrAD/backend/internal/api/middleware.go
T
GraceSolutions 4e6ed52e51 feat: Add API handlers, LDAP client, and repositories
- API response helpers and error codes
- Authentication handlers (login, logout, me, csrf)
- Auth middleware (session validation, role checks, CSRF)
- LDAP client with TLS/StartTLS support
- LDAP filter construction from conditions
- AD operations (group membership, move, create group/OU)
- Credentials repository (CRUD, test results, usage check)
- AD connections repository (CRUD, test results)
- Schedules repository (CRUD, next run tracking)
- Rules repository with nested condition groups and actions
- go-ldap/ldap/v3 dependency added
2026-04-19 10:11:16 -04:00

159 lines
4.2 KiB
Go

// Package api - Middleware for authentication and authorization
package api
import (
"context"
"net/http"
"strings"
"github.com/Grace-Solutions/OrchestrAD/internal/auth"
"github.com/Grace-Solutions/OrchestrAD/internal/models"
)
type contextKey string
const userContextKey contextKey = "user"
// AuthMiddleware validates session tokens
func AuthMiddleware(authService *auth.Service) func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
token := extractToken(r)
if token == "" {
WriteError(w, http.StatusUnauthorized, ErrCodeUnauthorized, "Authentication required")
return
}
user, err := authService.ValidateSession(token)
if err != nil {
WriteError(w, http.StatusUnauthorized, ErrCodeUnauthorized, "Invalid or expired token")
return
}
ctx := context.WithValue(r.Context(), userContextKey, user)
next.ServeHTTP(w, r.WithContext(ctx))
})
}
}
// OptionalAuthMiddleware adds user to context if authenticated, but doesn't require it
func OptionalAuthMiddleware(authService *auth.Service) func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
token := extractToken(r)
if token != "" {
user, err := authService.ValidateSession(token)
if err == nil {
ctx := context.WithValue(r.Context(), userContextKey, user)
r = r.WithContext(ctx)
}
}
next.ServeHTTP(w, r)
})
}
}
// RequireRole creates middleware that requires a specific role
func RequireRole(roles ...string) func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
user := GetUserFromContext(r.Context())
if user == nil {
WriteError(w, http.StatusUnauthorized, ErrCodeUnauthorized, "Authentication required")
return
}
if !hasAnyRole(user, roles) {
WriteError(w, http.StatusForbidden, ErrCodeForbidden, "Insufficient permissions")
return
}
next.ServeHTTP(w, r)
})
}
}
// RequireAdmin is a convenience middleware for admin-only routes
func RequireAdmin() func(http.Handler) http.Handler {
return RequireRole("SuperAdmin", "Admin")
}
// RequireOperator is a convenience middleware for operator-level routes
func RequireOperator() func(http.Handler) http.Handler {
return RequireRole("SuperAdmin", "Admin", "Operator")
}
// GetUserFromContext retrieves the authenticated user from context
func GetUserFromContext(ctx context.Context) *models.User {
user, ok := ctx.Value(userContextKey).(*models.User)
if !ok {
return nil
}
return user
}
func extractToken(r *http.Request) string {
// Check Authorization header
auth := r.Header.Get("Authorization")
if strings.HasPrefix(auth, "Bearer ") {
return auth[7:]
}
// Check X-API-Key header for API key auth
if apiKey := r.Header.Get("X-API-Key"); apiKey != "" {
return apiKey
}
// Check cookie (for browser sessions)
cookie, err := r.Cookie("session")
if err == nil && cookie.Value != "" {
return cookie.Value
}
return ""
}
func hasAnyRole(user *models.User, roles []string) bool {
for _, userRole := range user.Roles {
for _, required := range roles {
if userRole.Name == required {
return true
}
}
}
return false
}
// CSRFMiddleware validates CSRF tokens for mutation requests
func CSRFMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Skip CSRF for GET, HEAD, OPTIONS
if r.Method == "GET" || r.Method == "HEAD" || r.Method == "OPTIONS" {
next.ServeHTTP(w, r)
return
}
// Skip CSRF for API key auth
if r.Header.Get("X-API-Key") != "" {
next.ServeHTTP(w, r)
return
}
// Skip CSRF for Bearer token auth (typically from NextAuth)
if strings.HasPrefix(r.Header.Get("Authorization"), "Bearer ") {
next.ServeHTTP(w, r)
return
}
// TODO: Validate CSRF token from header/body
csrfToken := r.Header.Get("X-CSRF-Token")
if csrfToken == "" {
WriteError(w, http.StatusForbidden, ErrCodeForbidden, "CSRF token required")
return
}
// TODO: Validate the token against stored session
next.ServeHTTP(w, r)
})
}