202 lines
6.0 KiB
Go
202 lines
6.0 KiB
Go
package middleware
|
|
|
|
// Reference: P2E §1.3 — AccountScope middleware
|
|
// Extracts account_id from JWT claims and injects it plus the PolicyContext
|
|
// into the Gin context for downstream RBAC enforcement.
|
|
//
|
|
// IMPORTANT: This middleware package does NOT import internal/model or internal/service
|
|
// to avoid potential circular dependencies. Instead, it defines its own interface
|
|
// (RBACLookup) that the service layer implements. This follows Go's interface segregation
|
|
// pattern — the middleware defines what it needs, and the service provides it.
|
|
|
|
import (
|
|
"net/http"
|
|
"strconv"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
|
|
"github.com/gochat/gochat/internal/auth"
|
|
"github.com/gochat/gochat/pkg/response"
|
|
)
|
|
|
|
// AccountScope validates and injects account context from JWT claims or
|
|
// X-Account-ID header. It also constructs the PolicyContext from the
|
|
// AccountUser association so that downstream PolicyMiddleware can check permissions.
|
|
//
|
|
// Basic version (without service lookup) uses JWT claims directly.
|
|
// For production, use AccountScopeWithService() which looks up AccountUser from DB.
|
|
//
|
|
// Usage:
|
|
// router.Use(AuthRequired(jwtSvc), AccountScope())
|
|
func AccountScope() gin.HandlerFunc {
|
|
return func(c *gin.Context) {
|
|
// Step 1: Get user_id from JWT claims (set by AuthRequired)
|
|
userID, exists := c.Get("user_id")
|
|
if !exists {
|
|
response.AbortWithStatusError(c, http.StatusUnauthorized, response.ErrUnauthorized,
|
|
"User ID not found in context — ensure AuthRequired middleware is applied first")
|
|
return
|
|
}
|
|
|
|
// Step 2: Determine account_id
|
|
// Priority: X-Account-ID header > JWT claims account_id
|
|
accountID := getAccountID(c)
|
|
if accountID == 0 {
|
|
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest,
|
|
"Account ID required — provide via X-Account-ID header or JWT claims")
|
|
return
|
|
}
|
|
|
|
// Step 3: Override JWT claims account_id if header was provided
|
|
c.Set("account_id", accountID)
|
|
|
|
// Step 4: Get role from JWT claims
|
|
roleStr := "agent" // default fallback
|
|
role, roleExists := c.Get("user_role")
|
|
if roleExists {
|
|
if s, ok := role.(string); ok {
|
|
roleStr = s
|
|
}
|
|
}
|
|
|
|
// Step 5: Get custom_role_id from JWT claims
|
|
customRoleID := uint(0)
|
|
claims, claimsExists := c.Get("auth_claims")
|
|
if claimsExists {
|
|
if jwtClaims, ok := claims.(*auth.Claims); ok {
|
|
if jwtClaims.CustomRoleID != 0 {
|
|
customRoleID = jwtClaims.CustomRoleID
|
|
}
|
|
if jwtClaims.Role != "" {
|
|
roleStr = jwtClaims.Role
|
|
}
|
|
}
|
|
}
|
|
|
|
// Step 6: Build PolicyContext from JWT claims data
|
|
// For custom_role, permissions will be nil (loaded by service layer)
|
|
var permissions auth.PermissionMatrixMap
|
|
policyCtx := auth.NewPolicyContext(
|
|
userID.(uint),
|
|
accountID,
|
|
roleStr,
|
|
customRoleID,
|
|
permissions,
|
|
)
|
|
|
|
c.Set("policy_context", policyCtx)
|
|
c.Next()
|
|
}
|
|
}
|
|
|
|
// AccountScopeWithService creates an AccountScope middleware that uses
|
|
// a RBACLookup service to look up the user's AccountUser record and CustomRole
|
|
// permissions from the database. This is the production-grade version.
|
|
//
|
|
// Usage:
|
|
// rbacSvc := service.NewRBACService(db)
|
|
// router.Use(AuthRequired(jwtSvc), AccountScopeWithService(rbacSvc))
|
|
func AccountScopeWithService(lookup RBACLookup) gin.HandlerFunc {
|
|
return func(c *gin.Context) {
|
|
userID, exists := c.Get("user_id")
|
|
if !exists {
|
|
response.AbortWithStatusError(c, http.StatusUnauthorized, response.ErrUnauthorized,
|
|
"User ID not found in context — ensure AuthRequired middleware is applied first")
|
|
return
|
|
}
|
|
|
|
accountID := getAccountID(c)
|
|
if accountID == 0 {
|
|
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest,
|
|
"Account ID required — provide via X-Account-ID header or JWT claims")
|
|
return
|
|
}
|
|
|
|
c.Set("account_id", accountID)
|
|
|
|
// Look up AccountUser to get role and CustomRoleID
|
|
accountUser, err := lookup.GetAccountUser(userID.(uint), accountID)
|
|
if err != nil {
|
|
response.AbortWithStatusError(c, http.StatusForbidden, response.ErrForbidden,
|
|
"User does not belong to this account")
|
|
return
|
|
}
|
|
|
|
// Build permissions matrix based on role
|
|
permissions := auth.PermissionMatrixMap{}
|
|
if accountUser.Role == "custom_role" && accountUser.CustomRoleID > 0 {
|
|
pm, err := lookup.GetCustomRolePermissions(accountUser.CustomRoleID)
|
|
if err != nil {
|
|
// Fallback to agent defaults if custom role not found
|
|
permissions = auth.AgentDefaultPermissions
|
|
} else {
|
|
permissions = pm
|
|
}
|
|
}
|
|
|
|
policyCtx := auth.NewPolicyContext(
|
|
userID.(uint),
|
|
accountID,
|
|
accountUser.Role,
|
|
accountUser.CustomRoleID,
|
|
permissions,
|
|
)
|
|
|
|
c.Set("policy_context", policyCtx)
|
|
c.Next()
|
|
}
|
|
}
|
|
|
|
// getAccountID extracts account ID from the request.
|
|
// Priority: X-Account-ID header > JWT claims account_id
|
|
func getAccountID(c *gin.Context) uint {
|
|
// Try X-Account-ID header first (for account switching)
|
|
headerAccountID := c.GetHeader("X-Account-ID")
|
|
if headerAccountID != "" {
|
|
id, err := strconv.ParseUint(headerAccountID, 10, 32)
|
|
if err == nil && id != 0 {
|
|
return uint(id)
|
|
}
|
|
}
|
|
|
|
// Fall back to JWT claims (set by AuthRequired middleware)
|
|
if accountID, exists := c.Get("account_id"); exists {
|
|
switch v := accountID.(type) {
|
|
case uint:
|
|
if v != 0 {
|
|
return v
|
|
}
|
|
case float64:
|
|
if v != 0 {
|
|
return uint(v)
|
|
}
|
|
case int:
|
|
if v != 0 {
|
|
return uint(v)
|
|
}
|
|
}
|
|
}
|
|
|
|
return 0
|
|
}
|
|
|
|
// --- RBACLookup Interface ---
|
|
// Defines what the middleware needs from the service layer.
|
|
// The service package implements this interface, avoiding circular imports.
|
|
|
|
// RBACLookup is the interface that the RBAC service must implement
|
|
// for use with AccountScopeWithService middleware.
|
|
type RBACLookup interface {
|
|
GetAccountUser(userID, accountID uint) (*AccountUserRole, error)
|
|
GetCustomRolePermissions(customRoleID uint) (auth.PermissionMatrixMap, error)
|
|
}
|
|
|
|
// AccountUserRole holds the essential AccountUser data needed by middleware.
|
|
// This is a lightweight DTO that the service populates from the model.AccountUser.
|
|
type AccountUserRole struct {
|
|
UserID uint
|
|
AccountID uint
|
|
Role string
|
|
CustomRoleID uint
|
|
Availability string
|
|
} |