Files
Authorization/services/policy_evaluator.go
T

262 lines
7.6 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 "=":
// Special logic for region: allow '1' and '01' to match
if isRegionComparison(actual, expected) {
return normalizeRegion(actual) == normalizeRegion(expected)
}
return actual == expected // case-sensitive
case "!=":
// Special logic for region: allow '1' and '01' to match
if isRegionComparison(actual, expected) {
return normalizeRegion(actual) != normalizeRegion(expected)
}
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
}
}
// Checks if the comparison is for region attribute
func isRegionComparison(actual, expected string) bool {
// Only trigger for region values that are numeric or zero-padded numeric
// This is a heuristic: if both are digits or zero-padded digits, treat as region
return isRegionValue(actual) && isRegionValue(expected)
}
func isRegionValue(val string) bool {
val = strings.TrimLeft(val, "0")
return len(val) > 0 && isDigits(val)
}
func isDigits(val string) bool {
for _, r := range val {
if r < '0' || r > '9' {
return false
}
}
return true
}
func normalizeRegion(val string) string {
return strings.TrimLeft(val, "0")
}
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) {
if ctx == nil {
return false, "Authorization context is nil"
}
var actualValue string
var exists bool
log.Print("Attribute Type: ", policyAttribute.AttributeType)
// Skip region checks for roleID 1, 2, or Admin roles
log.Print("Role ID!!!!!: ", ctx.RoleID)
if policyAttribute.AttributeType == "user" &&
policyAttribute.AttributeName == "region" &&
(ctx.RoleID == 1 || ctx.RoleID == 2) {
fmt.Printf("[POLICY EVALUATION] Type: %s, Attribute: %s\n", policyAttribute.AttributeType, policyAttribute.AttributeName)
fmt.Printf(" Skipped for roleID: %d (Super | System Admin bypass)\n\n", ctx.RoleID)
return true, ""
}
// Always check user attributes in context if type is user
switch policyAttribute.AttributeType {
case "user":
log.Print("Checking user attribute in context for: ", policyAttribute.AttributeName)
if ctx.UserAttributes == nil {
return false, "User attributes missing in context"
}
actualValue, exists = ctx.UserAttributes[policyAttribute.AttributeName]
if !exists {
return false, fmt.Sprintf("User attribute '%s' not found in context", policyAttribute.AttributeName)
}
log.Print("Found User Attribute: ", actualValue)
case "resource":
if ctx.ResourceData == nil {
return false, "Resource data missing in context"
}
actualValue, exists = ctx.ResourceData[policyAttribute.AttributeName]
if !exists {
return false, fmt.Sprintf("Resource attribute '%s' not found in context", policyAttribute.AttributeName)
}
case "environment":
if ctx.Environment == nil {
return false, "Environment data missing in context"
}
actualValue, exists = ctx.Environment[policyAttribute.AttributeName]
if !exists {
return false, fmt.Sprintf("Environment attribute '%s' not found in context", 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"
}