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" }