Files
Authorization/services/policy_evaluator.go
T
2026-01-27 10:13:15 +08:00

208 lines
5.8 KiB
Go

package services
import (
"authorization/models"
"fmt"
"log"
"regexp"
"strconv"
"strings"
)
func resolveVariables(value string, ctx *models.AuthorizationContext) string {
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
})
}
// hasUnresolvedPlaceholders checks if a string still contains placeholders that couldn't be resolved
func hasUnresolvedPlaceholders(value string) bool {
re := regexp.MustCompile(`\$\{[^}]+\}`)
return re.MatchString(value)
}
// extractUnresolvedPlaceholders returns a list of unresolved placeholders
func extractUnresolvedPlaceholders(value string) []string {
re := regexp.MustCompile(`\$\{[^}]+\}`)
return re.FindAllString(value, -1)
}
// compare evaluates comparison operators between actual and expected values
// Note: "=" and "!=" are case-sensitive, while IN/CONTAINS/STARTS_WITH/ENDS_WITH are case-insensitive
func compare(actual, expected, operator string) bool {
actual = strings.TrimSpace(actual)
expected = strings.TrimSpace(expected)
switch operator {
case "=":
return actual == expected // case-sensitive
case "!=":
return actual != expected // case-sensitive
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
}
}
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)
}
func inComparison(actual, expected string) bool {
values := strings.Split(expected, ",")
actual = strings.ToLower(strings.TrimSpace(actual))
log.Print("IN comparison values: ", values)
log.Print("Actual value: ", actual)
log.Print("Expected values: ", expected)
for _, val := range values {
if strings.ToLower(strings.TrimSpace(val)) == actual {
return true
}
}
return false
}
func evaluatePolicy(policyAttribute models.PolicyAttribute, ctx *models.AuthorizationContext) (bool, string) {
var actualValue string
var exists bool
log.Print("Attribute Type: ", policyAttribute.AttributeType)
switch policyAttribute.AttributeType {
case "user":
log.Print("Fetching from User Attributes")
log.Print("User Attributes: ", ctx.UserAttributes)
actualValue, exists = ctx.UserAttributes[policyAttribute.AttributeName]
if !exists {
return false, fmt.Sprintf("User attribute '%s' not found", policyAttribute.AttributeName)
}
log.Print("Found User Attribute: ", actualValue)
case "resource":
actualValue, exists = ctx.ResourceData[policyAttribute.AttributeName]
if !exists {
return false, fmt.Sprintf("Resource attribute '%s' not found", policyAttribute.AttributeName)
}
case "environment":
actualValue, exists = ctx.Environment[policyAttribute.AttributeName]
if !exists {
return false, fmt.Sprintf("Environment attribute '%s' not found", policyAttribute.AttributeName)
}
default:
return false, fmt.Sprintf("Unknown attribute type: %s", policyAttribute.AttributeType)
}
expectedValue := resolveVariables(policyAttribute.AttributeValue, ctx)
fmt.Printf("[POLICY EVALUATION] Type: %s, Attribute: %s\n", policyAttribute.AttributeType, policyAttribute.AttributeName)
fmt.Printf(" Expected: %s %s %s\n", policyAttribute.AttributeName, policyAttribute.Comparison, expectedValue)
fmt.Printf(" Actual: %s = %s\n", policyAttribute.AttributeName, actualValue)
log.Print("Comparison: ", policyAttribute.Comparison)
satisfied := compare(actualValue, expectedValue, policyAttribute.Comparison)
if !satisfied {
fmt.Printf(" Result: ❌ FAILED\n\n")
// Check if the failure is due to unresolved placeholders
if hasUnresolvedPlaceholders(expectedValue) {
unresolvedPlaceholders := extractUnresolvedPlaceholders(expectedValue)
return false, fmt.Sprintf(
"Policy failed: %s %s %s (actual: %s) - Missing required attributes: %v",
policyAttribute.AttributeName,
policyAttribute.Comparison,
expectedValue,
actualValue,
unresolvedPlaceholders,
)
}
return false, fmt.Sprintf(
"Policy failed: %s %s %s (actual: %s)",
policyAttribute.AttributeName,
policyAttribute.Comparison,
expectedValue,
actualValue,
)
}
fmt.Printf(" Result: ✔️ PASSED\n\n")
return true, ""
}
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"
}