fixed authorization

This commit is contained in:
2025-12-09 15:42:35 +08:00
parent ca49e8e24b
commit 5743dbf22d
15 changed files with 1936 additions and 62 deletions
+74 -27
View File
@@ -2,39 +2,86 @@ package services
import (
"authorization/models"
"strings"
"authorization/repository"
"database/sql"
"fmt"
"time"
)
// Authorize checks if the user has permission to perform the action on the resource
func Authorize(claims *models.Claims, request *models.AuthorizationRequest) (bool, string) {
// Verify the user ID matches the JWT claims
if claims.UserID != request.UserID {
return false, "User ID mismatch"
// Authorize performs RBAC + ABAC authorization check
func Authorize(repo *repository.PermissionRepository, ctx *models.AuthorizationContext) (*models.AuthorizationResult, error) {
startTime := time.Now()
// Step 1: Find the permission for the requested resource and action
permission, err := repo.GetPermissionByResourceAndAction(ctx.Resource, ctx.Action)
if err != nil {
return &models.AuthorizationResult{
Allowed: false,
Message: fmt.Sprintf("Permission not found: %v", err),
}, nil
}
// Admin role has access to everything
if strings.ToLower(claims.Role) == "admin" {
return true, "Admin access granted"
// Step 2: Get user attributes
userAttrs, err := repo.GetUserAttributes(ctx.UserID)
if err != nil {
return &models.AuthorizationResult{
Allowed: false,
Message: fmt.Sprintf("Failed to get user attributes: %v", err),
}, err
}
ctx.UserAttributes = userAttrs
// Step 3: Get policy attributes for the permission
policies, err := repo.GetPolicyAttributesByPermission(permission.ID)
if err != nil {
return &models.AuthorizationResult{
Allowed: false,
Message: fmt.Sprintf("Failed to get policies: %v", err),
}, err
}
// Add your custom authorization logic here
// Example: Role-based access control
switch strings.ToLower(claims.Role) {
case "user":
// Users can only read their own resources
if request.Action == "read" && strings.Contains(request.Resource, claims.UserID) {
return true, "User read access granted"
}
return false, "Insufficient permissions"
// Step 4: Evaluate ABAC policies
allowed, reason := EvaluatePolicies(policies, ctx)
case "moderator":
// Moderators can read and update
if request.Action == "read" || request.Action == "update" {
return true, "Moderator access granted"
}
return false, "Moderators cannot perform this action"
default:
return false, "Unknown role"
result := &models.AuthorizationResult{
Allowed: allowed,
}
if allowed {
result.Message = "Access granted"
} else {
result.Message = reason
}
// Log evaluation time for performance monitoring
evalTime := time.Since(startTime)
if evalTime > 100*time.Millisecond {
fmt.Printf("WARN: Slow authorization evaluation: %v for user=%s, resource=%s, action=%s\n",
evalTime, ctx.UserID, ctx.Resource, ctx.Action)
}
return result, nil
}
// CheckPermission is a simplified authorization check
func CheckPermission(db *sql.DB, userID, resource, action string, resourceData map[string]string) (bool, string, error) {
repo := repository.NewPermissionRepository(db)
ctx := &models.AuthorizationContext{
UserID: userID,
Resource: resource,
Action: action,
ResourceData: resourceData,
Environment: make(map[string]string),
}
// Add current time to environment
ctx.Environment["time"] = time.Now().Format(time.RFC3339)
result, err := Authorize(repo, ctx)
if err != nil {
return false, fmt.Sprintf("Authorization error: %v", err), err
}
return result.Allowed, result.Message, nil
}
+193
View File
@@ -0,0 +1,193 @@
package services
import (
"authorization/models"
"authorization/repository"
"database/sql"
"sync"
"time"
)
// getCachedUserAttributes retrieves user attributes with caching
func getCachedUserAttributes(s *models.CachedAuthorizationService, userID string) (map[string]string, error) {
// Check cache first
userAttrMutex := s.UserAttrMutex.(*sync.RWMutex)
userAttrMutex.RLock()
attrs, exists := s.UserAttrCache[userID]
userAttrMutex.RUnlock()
if exists {
return attrs, nil
}
// Cache miss - fetch from DB
repo := s.Repo.(*repository.PermissionRepository)
attrs, err := repo.GetUserAttributes(userID)
if err != nil {
return nil, err
}
// Store in cache
userAttrMutex.Lock()
s.UserAttrCache[userID] = attrs
userAttrMutex.Unlock()
return attrs, nil
}
// refreshCache reloads permissions and policies from database
func refreshCache(s *models.CachedAuthorizationService) {
repo := s.Repo.(*repository.PermissionRepository)
// Load all permissions
permissions, err := repo.GetAllPermissions()
if err != nil {
return
}
// Load all policies
policies, err := repo.GetAllPolicyAttributes()
if err != nil {
return
}
// Update cache atomically
newPermCache := make(map[string]*models.Permission)
for i := range permissions {
perm := &permissions[i]
key := perm.Resource + ":" + perm.Action
newPermCache[key] = perm
}
cacheMutex := s.CacheMutex.(*sync.RWMutex)
cacheMutex.Lock()
s.PermissionCache = newPermCache
s.PolicyCache = policies
s.LastCacheRefresh = time.Now()
cacheMutex.Unlock()
}
// cleanUserAttributeCache removes old user attribute cache entries
func cleanUserAttributeCache(s *models.CachedAuthorizationService) {
userAttrMutex := s.UserAttrMutex.(*sync.RWMutex)
userAttrMutex.Lock()
defer userAttrMutex.Unlock()
// Clear all user attributes to prevent stale data
// In production, you might want a more sophisticated TTL approach
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(db *sql.DB) *models.CachedAuthorizationService {
service := &models.CachedAuthorizationService{
Repo: repository.NewPermissionRepository(db),
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: 5 * time.Minute,
LastCacheRefresh: time.Now(),
}
// Initial cache load
refreshCache(service)
// Background cache refresh
go cacheRefreshLoop(service)
return service
}
// AuthorizeWithCache performs cached RBAC + ABAC authorization
func AuthorizeWithCache(s *models.CachedAuthorizationService, ctx *models.AuthorizationContext) (*models.AuthorizationResult, error) {
startTime := time.Now()
// Step 1: Get permission from cache
cacheKey := ctx.Resource + ":" + ctx.Action
cacheMutex := s.CacheMutex.(*sync.RWMutex)
cacheMutex.RLock()
permission, exists := s.PermissionCache[cacheKey]
cacheMutex.RUnlock()
if !exists {
return &models.AuthorizationResult{
Allowed: false,
Message: "Permission not found",
}, nil
}
// Step 2: Get user attributes (with cache)
userAttrs, err := getCachedUserAttributes(s, ctx.UserID)
if err != nil {
return &models.AuthorizationResult{
Allowed: false,
Message: "Failed to get user attributes",
}, err
}
ctx.UserAttributes = userAttrs
// Step 3: Get policies from cache
cacheMutex.RLock()
policies := s.PolicyCache[permission.ID]
cacheMutex.RUnlock()
// Step 4: Evaluate policies
allowed, reason := EvaluatePolicies(policies, ctx)
result := &models.AuthorizationResult{
Allowed: allowed,
}
if allowed {
result.Message = "Access granted"
} else {
result.Message = reason
}
// Performance monitoring
evalTime := time.Since(startTime)
if evalTime > 50*time.Millisecond {
// Cached should be much faster
}
return result, nil
}
// InvalidateUserCache clears cache for a specific user
func InvalidateUserCache(s *models.CachedAuthorizationService, userID string) {
userAttrMutex := s.UserAttrMutex.(*sync.RWMutex)
userAttrMutex.Lock()
delete(s.UserAttrCache, userID)
userAttrMutex.Unlock()
}
// GetCacheStats returns cache statistics
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()
return 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(),
}
}
+180
View File
@@ -0,0 +1,180 @@
package services
import (
"authorization/models"
"fmt"
"regexp"
"strconv"
"strings"
)
// resolveVariables resolves variable references like ${resource.region}
func resolveVariables(value string, ctx *models.AuthorizationContext) string {
// Pattern: ${type.attribute}
re := regexp.MustCompile(`\$\{([^.]+)\.([^}]+)\}`)
return re.ReplaceAllStringFunc(value, func(match string) string {
parts := re.FindStringSubmatch(match)
if len(parts) != 3 {
return match
}
attrType := parts[1]
attrName := parts[2]
switch attrType {
case "user":
if val, ok := ctx.UserAttributes[attrName]; ok {
return val
}
case "resource":
if val, ok := ctx.ResourceData[attrName]; ok {
return val
}
case "environment":
if val, ok := ctx.Environment[attrName]; ok {
return val
}
}
return match
})
}
// compare performs the actual comparison based on operator
func compare(actual, expected, operator string) bool {
actual = strings.TrimSpace(actual)
expected = strings.TrimSpace(expected)
switch operator {
case "=":
return actual == expected
case "!=":
return actual != expected
case ">":
return numericCompare(actual, expected, func(a, e float64) bool { return a > e })
case "<":
return numericCompare(actual, expected, func(a, e float64) bool { return a < e })
case ">=":
return numericCompare(actual, expected, func(a, e float64) bool { return a >= e })
case "<=":
return numericCompare(actual, expected, func(a, e float64) bool { return a <= e })
case "IN":
return inComparison(actual, expected)
case "CONTAINS":
return strings.Contains(strings.ToLower(actual), strings.ToLower(expected))
case "STARTS_WITH":
return strings.HasPrefix(strings.ToLower(actual), strings.ToLower(expected))
case "ENDS_WITH":
return strings.HasSuffix(strings.ToLower(actual), strings.ToLower(expected))
default:
return false
}
}
// numericCompare performs numeric comparison
func numericCompare(actual, expected string, compareFn func(float64, float64) bool) bool {
actualNum, err1 := strconv.ParseFloat(actual, 64)
expectedNum, err2 := strconv.ParseFloat(expected, 64)
if err1 != nil || err2 != nil {
return false
}
return compareFn(actualNum, expectedNum)
}
// inComparison checks if actual value is in comma-separated list
func inComparison(actual, expected string) bool {
values := strings.Split(expected, ",")
actual = strings.ToLower(strings.TrimSpace(actual))
for _, val := range values {
if strings.ToLower(strings.TrimSpace(val)) == actual {
return true
}
}
return false
}
// evaluatePolicy evaluates a single policy attribute
func evaluatePolicy(
policy models.PolicyAttribute,
ctx *models.AuthorizationContext,
) (bool, string) {
// Get the actual value based on attribute type
var actualValue string
var exists bool
switch policy.AttributeType {
case "user":
actualValue, exists = ctx.UserAttributes[policy.AttributeName]
if !exists {
return false, fmt.Sprintf("User attribute '%s' not found", policy.AttributeName)
}
case "resource":
actualValue, exists = ctx.ResourceData[policy.AttributeName]
if !exists {
return false, fmt.Sprintf("Resource attribute '%s' not found", policy.AttributeName)
}
case "environment":
actualValue, exists = ctx.Environment[policy.AttributeName]
if !exists {
return false, fmt.Sprintf("Environment attribute '%s' not found", policy.AttributeName)
}
default:
return false, fmt.Sprintf("Unknown attribute type: %s", policy.AttributeType)
}
// Handle variable substitution (e.g., ${resource.region})
expectedValue := resolveVariables(policy.AttributeValue, ctx)
// Perform comparison
satisfied := compare(actualValue, expectedValue, policy.Comparison)
if !satisfied {
return false, fmt.Sprintf(
"Policy failed: %s %s %s (actual: %s)",
policy.AttributeName,
policy.Comparison,
expectedValue,
actualValue,
)
}
return true, ""
}
// EvaluatePolicies checks if all policy attributes are satisfied
func EvaluatePolicies(
policies []models.PolicyAttribute,
ctx *models.AuthorizationContext,
) (bool, string) {
if len(policies) == 0 {
// No policies means permission is granted by default (RBAC only)
return true, "No policies to evaluate"
}
for _, policy := range policies {
satisfied, reason := evaluatePolicy(policy, ctx)
if !satisfied {
return false, reason
}
}
return true, "All policies satisfied"
}