fixed authorization
This commit is contained in:
+74
-27
@@ -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
|
||||
}
|
||||
|
||||
@@ -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(),
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
Reference in New Issue
Block a user