Files
Authorization/services/cached_authorization.go
T
2026-01-27 10:12:25 +08:00

382 lines
12 KiB
Go

package services
import (
"authorization/models"
"authorization/redisclient"
"authorization/repository"
"context"
"encoding/json"
"fmt"
"log"
"sync"
"time"
)
const (
permissionCachePrefix = "authz:perm:"
policyCachePrefix = "authz:policy:"
userAttrCachePrefix = "authz:userattr:"
cacheTTL = 30 * time.Second
)
// getCachedUserAttributes retrieves user attributes from Redis or DB
func getCachedUserAttributes(s *models.CachedAuthorizationService, userID string) (map[string]string, error) {
// Try Redis first if available
if redisclient.RDB != nil {
ctx, cancel := context.WithTimeout(context.Background(), 100*time.Millisecond)
defer cancel()
key := userAttrCachePrefix + userID
val, err := redisclient.RDB.Get(ctx, key).Result()
if err == nil {
var attrs map[string]string
if json.Unmarshal([]byte(val), &attrs) == nil {
return attrs, nil
}
}
}
// Fallback to local cache for backward compatibility
userAttrMutex := s.UserAttrMutex.(*sync.RWMutex)
userAttrMutex.RLock()
attrs, exists := s.UserAttrCache[userID]
userAttrMutex.RUnlock()
if exists {
return attrs, nil
}
// Cache miss - fetch from DB
attrs, err := repository.GetUserAttributes(userID)
if err != nil {
return nil, err
}
// Store in both Redis and local cache
if redisclient.RDB != nil {
go func() {
ctx, cancel := context.WithTimeout(context.Background(), 100*time.Millisecond)
defer cancel()
attrsJSON, _ := json.Marshal(attrs)
key := userAttrCachePrefix + userID
redisclient.RDB.Set(ctx, key, attrsJSON, cacheTTL)
}()
}
userAttrMutex.Lock()
s.UserAttrCache[userID] = attrs
userAttrMutex.Unlock()
return attrs, nil
}
// getPermissionFromCache retrieves permission from Redis or local cache
func getPermissionFromCache(s *models.CachedAuthorizationService, cacheKey string) (*models.Permission, bool) {
// Try Redis first if available
if redisclient.RDB != nil {
ctx, cancel := context.WithTimeout(context.Background(), 100*time.Millisecond)
defer cancel()
key := permissionCachePrefix + cacheKey
val, err := redisclient.RDB.Get(ctx, key).Result()
if err == nil {
var perm models.Permission
if json.Unmarshal([]byte(val), &perm) == nil {
return &perm, true
}
}
}
// Fallback to local cache
cacheMutex := s.CacheMutex.(*sync.RWMutex)
cacheMutex.RLock()
permission, exists := s.PermissionCache[cacheKey]
cacheMutex.RUnlock()
return permission, exists
}
func storePermissionInCache(s *models.CachedAuthorizationService, cacheKey string, permission *models.Permission) {
// Store in Redis (async)
if redisclient.RDB != nil {
go func() {
ctx, cancel := context.WithTimeout(context.Background(), 100*time.Millisecond)
defer cancel()
permJSON, _ := json.Marshal(permission)
key := permissionCachePrefix + cacheKey
redisclient.RDB.Set(ctx, key, permJSON, cacheTTL)
}()
}
// Store in local cache
cacheMutex := s.CacheMutex.(*sync.RWMutex)
cacheMutex.Lock()
s.PermissionCache[cacheKey] = permission
cacheMutex.Unlock()
}
// getPoliciesFromCache retrieves policies from Redis or local cache
func getPoliciesFromCache(s *models.CachedAuthorizationService, permissionID int) []models.PolicyAttribute {
// Try Redis first if available
if redisclient.RDB != nil {
ctx, cancel := context.WithTimeout(context.Background(), 100*time.Millisecond)
defer cancel()
key := fmt.Sprintf("%s%d", policyCachePrefix, permissionID)
val, err := redisclient.RDB.Get(ctx, key).Result()
if err == nil {
var policies []models.PolicyAttribute
if json.Unmarshal([]byte(val), &policies) == nil {
return policies
}
}
}
// Fallback to local cache
cacheMutex := s.CacheMutex.(*sync.RWMutex)
cacheMutex.RLock()
policies := s.PolicyCache[permissionID]
cacheMutex.RUnlock()
return policies
}
// refreshCache reloads permissions and policies from database and stores in Redis
func refreshCache(s *models.CachedAuthorizationService) {
// Note: We no longer pre-cache all permissions since we need role-specific lookups.
// Permissions will be cached on-demand as users access them (lazy loading).
// Load all policies (these are permission-specific, not role-specific)
policies, err := repository.GetAllPolicyAttributes()
if err != nil {
log.Printf("ERROR: Failed to refresh policies cache: %v", err)
return
}
// Update policy cache atomically
cacheMutex := s.CacheMutex.(*sync.RWMutex)
cacheMutex.Lock()
s.PolicyCache = policies
s.LastCacheRefresh = time.Now()
cacheMutex.Unlock()
log.Printf("✓ Cache refreshed: %d policy groups cached", len(policies))
// Store policies in Redis for distributed access (non-blocking)
// Permissions are now cached on-demand with role awareness
if redisclient.RDB != nil {
go func() {
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
defer cancel()
// Store policies in Redis
for permID, policyList := range policies {
policiesJSON, _ := json.Marshal(policyList)
redisKey := fmt.Sprintf("%s%d", policyCachePrefix, permID)
redisclient.RDB.Set(ctx, redisKey, policiesJSON, cacheTTL)
}
log.Printf("INFO: Policy cache synced to Redis - %d policy groups", len(policies))
}()
}
}
// cleanUserAttributeCache removes old user attribute cache entries
func cleanUserAttributeCache(s *models.CachedAuthorizationService) {
userAttrMutex := s.UserAttrMutex.(*sync.RWMutex)
userAttrMutex.Lock()
defer userAttrMutex.Unlock()
// Clear local cache if too large (Redis handles its own TTL)
if len(s.UserAttrCache) > 10000 {
s.UserAttrCache = make(map[string]map[string]string)
}
}
// cacheRefreshLoop periodically refreshes the cache
func cacheRefreshLoop(s *models.CachedAuthorizationService) {
ticker := time.NewTicker(s.CacheExpiry)
defer ticker.Stop()
for range ticker.C {
refreshCache(s)
cleanUserAttributeCache(s)
}
}
func NewCachedAuthorizationService() *models.CachedAuthorizationService {
service := &models.CachedAuthorizationService{
PermissionCache: make(map[string]*models.Permission),
PolicyCache: make(map[int][]models.PolicyAttribute),
UserAttrCache: make(map[string]map[string]string),
CacheMutex: &sync.RWMutex{},
UserAttrMutex: &sync.RWMutex{},
CacheExpiry: 30 * time.Second, // Changed from 5 minutes for faster updates
LastCacheRefresh: time.Now(),
}
// Initial cache load
refreshCache(service)
// Background cache refresh
go cacheRefreshLoop(service)
return service
}
// AuthorizeWithCache performs cached RBAC + ABAC authorization with distributed caching
func AuthorizeWithCache(s *models.CachedAuthorizationService, ctx *models.AuthorizationContext) (*models.AuthorizationResult, error) {
startTime := time.Now()
log.Printf("[AuthZ Cached] Starting authorization check for user=%s, resource=%s, action=%s", ctx.UserID, ctx.Resource, ctx.Action)
// Step 0: Get user to retrieve role_id (needed for role-based permission lookup)
log.Printf("[AuthZ Step 0] Fetching user details for userID=%s", ctx.UserID)
user, err := repository.GetUserByID(ctx.UserID)
if err != nil {
log.Printf("✗ User not found for userID=%s: %v", ctx.UserID, err)
return &models.AuthorizationResult{
Allowed: false,
Message: fmt.Sprintf("User not found: %v", err),
}, nil
}
log.Printf("[AuthZ Step 0] User found: role_id=%d", user.RoleID)
// Step 1: Check if the user's role has the permission (not just if permission exists)
// Use role-aware cache key: roleID:resource:action
cacheKey := fmt.Sprintf("%d:%s:%s", user.RoleID, ctx.Resource, ctx.Action)
log.Printf("[AuthZ Step 1] Looking up permission in cache with role: %s", cacheKey)
permission, exists := getPermissionFromCache(s, cacheKey)
if !exists {
// Cache miss - try database lookup with role check
log.Printf("[AuthZ Step 1] Cache miss - querying database for role_id=%d, resource=%s, action=%s", user.RoleID, ctx.Resource, ctx.Action)
permission, err = repository.GetPermissionByResourceActionAndRole(ctx.Resource, ctx.Action, user.RoleID)
if err != nil {
log.Printf("✗ Permission not found or not granted to role_id=%d for resource=%s, action=%s: %v", user.RoleID, ctx.Resource, ctx.Action, err)
return &models.AuthorizationResult{
Allowed: false,
Message: "Permission not granted to your role",
}, nil
}
log.Printf("[AuthZ Step 1] Permission found in DB: ID=%d, Name=%s", permission.ID, permission.PermissionName)
// Cache the result for future use
storePermissionInCache(s, cacheKey, permission)
} else {
log.Printf("[AuthZ Step 1] Permission found in cache: ID=%d, Name=%s", permission.ID, permission.PermissionName)
}
// Step 2: Get user attributes (with distributed cache)
log.Printf("[AuthZ Step 2] Fetching user attributes for userID=%s", ctx.UserID)
userAttrs, err := getCachedUserAttributes(s, ctx.UserID)
if err != nil {
log.Printf("✗ Failed to get user attributes for userID=%s: %v", ctx.UserID, err)
return &models.AuthorizationResult{
Allowed: false,
Message: "Failed to get user attributes",
}, err
}
ctx.UserAttributes = userAttrs
log.Printf("[AuthZ Step 2] User attributes retrieved: %d attributes", len(userAttrs))
// Step 3: Get policies from distributed cache
log.Printf("[AuthZ Step 3] Fetching policies for permissionID=%d", permission.ID)
policies := getPoliciesFromCache(s, permission.ID)
log.Printf("[AuthZ Step 3] Policies retrieved: %d policies to evaluate", len(policies))
log.Printf("[AuthZ Step 4] Using RoleID: %s (from context or user record)", ctx.RoleID)
allowed, reason := EvaluatePolicies(policies, ctx)
result := &models.AuthorizationResult{
Allowed: allowed,
}
if allowed {
result.Message = "Access granted"
log.Printf("✓ Authorization GRANTED for user=%s, resource=%s, action=%s (evaluated in %v)",
ctx.UserID, ctx.Resource, ctx.Action, time.Since(startTime))
} else {
result.Message = reason
log.Printf("✗ Authorization DENIED for user=%s, resource=%s, action=%s - Reason: %s (evaluated in %v)",
ctx.UserID, ctx.Resource, ctx.Action, reason, time.Since(startTime))
}
// Performance monitoring
evalTime := time.Since(startTime)
if evalTime < 50*time.Millisecond {
log.Print("Cached authorization evaluation time: ", evalTime,
" for user=", ctx.UserID, ", resource=", ctx.Resource, ", action=", ctx.Action)
}
if evalTime > 50*time.Millisecond {
log.Print("WARN: Slow cached authorization evaluation: ", evalTime,
" for user=", ctx.UserID, ", resource=", ctx.Resource, ", action=", ctx.Action)
}
return result, nil
}
// InvalidateUserCache clears cache for a specific user from both Redis and local cache
func InvalidateUserCache(s *models.CachedAuthorizationService, userID string) {
// Clear from Redis
if redisclient.RDB != nil {
go func() {
ctx, cancel := context.WithTimeout(context.Background(), 100*time.Millisecond)
defer cancel()
key := userAttrCachePrefix + userID
redisclient.RDB.Del(ctx, key)
}()
}
// Clear from local cache
userAttrMutex := s.UserAttrMutex.(*sync.RWMutex)
userAttrMutex.Lock()
delete(s.UserAttrCache, userID)
userAttrMutex.Unlock()
}
// RefreshCacheNow forces an immediate cache refresh (useful for admin endpoints)
func RefreshCacheNow(s *models.CachedAuthorizationService) {
refreshCache(s)
}
// GetCacheStats returns cache statistics including Redis availability
func GetCacheStats(s *models.CachedAuthorizationService) map[string]interface{} {
cacheMutex := s.CacheMutex.(*sync.RWMutex)
userAttrMutex := s.UserAttrMutex.(*sync.RWMutex)
cacheMutex.RLock()
userAttrMutex.RLock()
defer cacheMutex.RUnlock()
defer userAttrMutex.RUnlock()
stats := map[string]interface{}{
"permissions_cached": len(s.PermissionCache),
"policies_cached": len(s.PolicyCache),
"user_attributes_cached": len(s.UserAttrCache),
"last_refresh": s.LastCacheRefresh,
"cache_age_seconds": time.Since(s.LastCacheRefresh).Seconds(),
"distributed_cache": redisclient.RDB != nil,
}
// Add Redis cache stats if available
if redisclient.RDB != nil {
ctx, cancel := context.WithTimeout(context.Background(), 100*time.Millisecond)
defer cancel()
// Try to get Redis info
if info, err := redisclient.RDB.Info(ctx, "stats").Result(); err == nil {
stats["redis_available"] = true
stats["redis_info"] = info
} else {
stats["redis_available"] = false
}
}
return stats
}