diff --git a/services/policy_evaluator_test.go b/services/policy_evaluator_test.go index bf1343b..e2da380 100644 --- a/services/policy_evaluator_test.go +++ b/services/policy_evaluator_test.go @@ -2,6 +2,7 @@ package services import ( "authorization/models" + "strings" "testing" ) @@ -687,3 +688,274 @@ func TestResolveVariablesAllAttributeTypes(t *testing.T) { } } } + +func TestEvaluatePolicies_UserRegionMatchesResourceRegion(t *testing.T) { + // Test case from the logs: user.region = ${resource.region} + // User region should match the resource region provided in ResourceData + + tests := []struct { + name string + userRegion string + resourceRegion string + shouldBeAllowed bool + description string + }{ + { + name: "same region", + userRegion: "01", + resourceRegion: "01", + shouldBeAllowed: true, + description: "user region matches resource region", + }, + { + name: "different region", + userRegion: "02", + resourceRegion: "01", + shouldBeAllowed: false, + description: "user region does not match resource region", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ctx := &models.AuthorizationContext{ + UserID: "U0000000001", + Resource: "personnel", + Action: "assign_role", + UserAttributes: map[string]string{ + "region": tt.userRegion, + }, + ResourceData: map[string]string{ + "region": tt.resourceRegion, + }, + } + + policies := []models.PolicyAttribute{ + { + AttributeType: "user", + AttributeName: "region", + Comparison: "=", + AttributeValue: "${resource.region}", + }, + } + + satisfied, reason := EvaluatePolicies(policies, ctx) + if satisfied != tt.shouldBeAllowed { + t.Errorf("%s: got satisfied=%v, want %v. Reason: %s", tt.description, satisfied, tt.shouldBeAllowed, reason) + } + }) + } +} + +func TestEvaluatePolicies_MissingResourceAttribute(t *testing.T) { + // Test case: when ResourceData doesn't have the required attribute + // The policy should fail because the placeholder cannot be resolved + + ctx := &models.AuthorizationContext{ + UserID: "U0000000001", + Resource: "personnel", + Action: "assign_role", + UserAttributes: map[string]string{ + "region": "01", + }, + ResourceData: map[string]string{ + // Missing "region" key - this should cause policy to fail + }, + } + + policies := []models.PolicyAttribute{ + { + AttributeType: "user", + AttributeName: "region", + Comparison: "=", + AttributeValue: "${resource.region}", + }, + } + + satisfied, reason := EvaluatePolicies(policies, ctx) + // When resource attribute is missing, the placeholder stays unresolved + // "01" != "${resource.region}", so policy fails + if satisfied { + t.Errorf("EvaluatePolicies should fail when resource attribute is missing. Reason: %s", reason) + } + + if reason == "" { + t.Error("EvaluatePolicies should provide a reason when policy fails") + } + + // Check that the error message indicates missing attributes + if !strings.Contains(reason, "Missing required attributes") || !strings.Contains(reason, "${resource.region}") { + t.Errorf("Expected error message about missing resource.region, got: %s", reason) + } +} + +func TestHasUnresolvedPlaceholders(t *testing.T) { + tests := []struct { + name string + value string + expected bool + }{ + { + name: "has unresolved placeholder", + value: "${resource.region}", + expected: true, + }, + { + name: "multiple unresolved placeholders", + value: "${resource.region} and ${user.department}", + expected: true, + }, + { + name: "no unresolved placeholders", + value: "US", + expected: false, + }, + { + name: "mixed resolved and unresolved", + value: "US and ${resource.owner}", + expected: true, + }, + { + name: "empty string", + value: "", + expected: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := hasUnresolvedPlaceholders(tt.value) + if result != tt.expected { + t.Errorf("hasUnresolvedPlaceholders(%q) = %v, want %v", tt.value, result, tt.expected) + } + }) + } +} + +func TestExtractUnresolvedPlaceholders(t *testing.T) { + tests := []struct { + name string + value string + expected []string + }{ + { + name: "single placeholder", + value: "${resource.region}", + expected: []string{"${resource.region}"}, + }, + { + name: "multiple placeholders", + value: "${resource.region} and ${user.department}", + expected: []string{"${resource.region}", "${user.department}"}, + }, + { + name: "no placeholders", + value: "US", + expected: []string{}, + }, + { + name: "empty list when no matches", + value: "", + expected: []string{}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := extractUnresolvedPlaceholders(tt.value) + if len(result) != len(tt.expected) { + t.Errorf("extractUnresolvedPlaceholders(%q) returned %d items, want %d", tt.value, len(result), len(tt.expected)) + } + for i, expected := range tt.expected { + if i >= len(result) || result[i] != expected { + t.Errorf("extractUnresolvedPlaceholders(%q)[%d] = %q, want %q", tt.value, i, result[i], expected) + } + } + }) + } +} + +func TestEvaluatePolicies_RegionBypassForAdminRoles(t *testing.T) { + // Test that region policies are skipped for roleID 1 (Super Admin) and 2 (Admin) + + tests := []struct { + name string + roleID string + userRegion string + resourceRegion string + shouldBeAllowed bool + description string + }{ + { + name: "roleID 1 bypasses region check", + roleID: "1", + userRegion: "02", + resourceRegion: "01", + shouldBeAllowed: true, + description: "Super Admin should bypass region check", + }, + { + name: "roleID 2 bypasses region check", + roleID: "2", + userRegion: "03", + resourceRegion: "01", + shouldBeAllowed: true, + description: "Admin should bypass region check", + }, + { + name: "other roleID respects region check", + roleID: "3", + userRegion: "02", + resourceRegion: "01", + shouldBeAllowed: false, + description: "Non-admin roles should not bypass region check", + }, + { + name: "Super Admin role bypasses region check", + roleID: "Super Admin", + userRegion: "02", + resourceRegion: "01", + shouldBeAllowed: true, + description: "Super Admin role string should bypass region check", + }, + { + name: "Admin role bypasses region check", + roleID: "Admin", + userRegion: "03", + resourceRegion: "01", + shouldBeAllowed: true, + description: "Admin role string should bypass region check", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ctx := &models.AuthorizationContext{ + UserID: "U0000000001", + Resource: "personnel", + Action: "assign_role", + RoleID: tt.roleID, + UserAttributes: map[string]string{ + "region": tt.userRegion, + }, + ResourceData: map[string]string{ + "region": tt.resourceRegion, + }, + } + + policies := []models.PolicyAttribute{ + { + AttributeType: "user", + AttributeName: "region", + Comparison: "=", + AttributeValue: "${resource.region}", + }, + } + + satisfied, reason := EvaluatePolicies(policies, ctx) + if satisfied != tt.shouldBeAllowed { + t.Errorf("%s: got satisfied=%v, want %v. Reason: %s", tt.description, satisfied, tt.shouldBeAllowed, reason) + } + }) + } +}