ae1831e61f
- Rename user_id → users_id across all models, handlers, services, and tests
- Add custom RoleIDs type supporting string/int/array unmarshaling (e.g., "1", 1, [1])
- Implement flexible JSON unmarshaling for JWT Claims to handle field name variants
- Support both user_id/users_id and email/email_address field names
- Enable role_id as string ("1"), int (1), or array ([1,2])
- Update AuthorizationContext to handle role_id type flexibility
- Add comprehensive logging to repository, service, and handler layers
- Entry/exit logs with full context
- Success (✓) and failure (✗) indicators
- Step-by-step authorization flow tracking
- Add containsRole helper for multi-role membership checks
- Fix database queries: user_id → users_id, id → permissions_id
- Update all tests to use models.RoleIDs{} syntax
- Change GetRole middleware return type: string → []int
- Maintain backward compatibility with legacy JWT tokens
This change improves integration with external services (MIS) that may send
role_id in different formats and standardizes field naming conventions
throughout the authorization microservice.
229 lines
6.6 KiB
Go
229 lines
6.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 "=":
|
|
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) {
|
|
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: %s (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"
|
|
}
|