962 lines
26 KiB
Go
962 lines
26 KiB
Go
package services
|
|
|
|
import (
|
|
"authorization/models"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
const (
|
|
testCompareFormat = "compare(%q, %q, %q) = %v, want %v"
|
|
testAdminUserGuest = "admin,user,guest"
|
|
testHelloWorld = "hello world"
|
|
testHelloWorldCased = "Hello World"
|
|
)
|
|
|
|
func TestResolveVariables(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
value string
|
|
ctx *models.AuthorizationContext
|
|
expected string
|
|
}{
|
|
{
|
|
name: "resolves user attribute",
|
|
value: "${user.department}",
|
|
ctx: &models.AuthorizationContext{
|
|
UserAttributes: map[string]string{"department": "Engineering"},
|
|
},
|
|
expected: "Engineering",
|
|
},
|
|
{
|
|
name: "resolves resource attribute",
|
|
value: "${resource.owner}",
|
|
ctx: &models.AuthorizationContext{
|
|
ResourceData: map[string]string{"owner": "user123"},
|
|
},
|
|
expected: "user123",
|
|
},
|
|
{
|
|
name: "resolves environment attribute",
|
|
value: "${environment.time}",
|
|
ctx: &models.AuthorizationContext{
|
|
Environment: map[string]string{"time": "12:00"},
|
|
},
|
|
expected: "12:00",
|
|
},
|
|
{
|
|
name: "resolves multiple variables",
|
|
value: "${user.name} from ${user.department}",
|
|
ctx: &models.AuthorizationContext{
|
|
UserAttributes: map[string]string{
|
|
"name": "John",
|
|
"department": "IT",
|
|
},
|
|
},
|
|
expected: "John from IT",
|
|
},
|
|
{
|
|
name: "leaves unresolved variables unchanged",
|
|
value: "${user.nonexistent}",
|
|
ctx: &models.AuthorizationContext{UserAttributes: map[string]string{}},
|
|
expected: "${user.nonexistent}",
|
|
},
|
|
{
|
|
name: "handles no variables",
|
|
value: "plain text",
|
|
ctx: &models.AuthorizationContext{},
|
|
expected: "plain text",
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
got := resolveVariables(tt.value, tt.ctx)
|
|
if got != tt.expected {
|
|
t.Errorf("resolveVariables() = %v, want %v", got, tt.expected)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestCompareEquality(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
actual string
|
|
expected string
|
|
operator string
|
|
want bool
|
|
}{
|
|
{"equal strings", "admin", "admin", "=", true},
|
|
{"not equal strings", "admin", "user", "=", false},
|
|
{"not equal operator true", "admin", "user", "!=", true},
|
|
{"not equal operator false", "admin", "admin", "!=", false},
|
|
{"equal with whitespace", " admin ", "admin", "=", true},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
got := compare(tt.actual, tt.expected, tt.operator)
|
|
if got != tt.want {
|
|
t.Errorf(testCompareFormat, tt.actual, tt.expected, tt.operator, got, tt.want)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestCompareNumeric(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
actual string
|
|
expected string
|
|
operator string
|
|
want bool
|
|
}{
|
|
{"greater than true", "10", "5", ">", true},
|
|
{"greater than false", "5", "10", ">", false},
|
|
{"less than true", "5", "10", "<", true},
|
|
{"less than false", "10", "5", "<", false},
|
|
{"greater or equal true equal", "10", "10", ">=", true},
|
|
{"greater or equal true greater", "11", "10", ">=", true},
|
|
{"greater or equal false", "9", "10", ">=", false},
|
|
{"less or equal true equal", "10", "10", "<=", true},
|
|
{"less or equal true less", "9", "10", "<=", true},
|
|
{"less or equal false", "11", "10", "<=", false},
|
|
{"invalid number returns false", "abc", "10", ">", false},
|
|
{"float comparison", "10.5", "10.2", ">", true},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
got := compare(tt.actual, tt.expected, tt.operator)
|
|
if got != tt.want {
|
|
t.Errorf(testCompareFormat, tt.actual, tt.expected, tt.operator, got, tt.want)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestCompareIN(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
actual string
|
|
expected string
|
|
want bool
|
|
}{
|
|
{"value in list", "admin", testAdminUserGuest, true},
|
|
{"value not in list", "superuser", testAdminUserGuest, false},
|
|
{"value in list with spaces", "admin", " admin , user , guest ", true},
|
|
{"case insensitive match", "ADMIN", testAdminUserGuest, true},
|
|
{"single value match", "admin", "admin", true},
|
|
{"empty list", "admin", "", false},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
got := compare(tt.actual, tt.expected, "IN")
|
|
if got != tt.want {
|
|
t.Errorf("compare(%q, %q, IN) = %v, want %v", tt.actual, tt.expected, got, tt.want)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestCompareStringOperations(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
actual string
|
|
expected string
|
|
operator string
|
|
want bool
|
|
}{
|
|
{"contains true", testHelloWorld, "world", "CONTAINS", true},
|
|
{"contains false", testHelloWorld, "xyz", "CONTAINS", false},
|
|
{"contains case insensitive", testHelloWorldCased, "WORLD", "CONTAINS", true},
|
|
{"starts with true", testHelloWorld, "hello", "STARTS_WITH", true},
|
|
{"starts with false", testHelloWorld, "world", "STARTS_WITH", false},
|
|
{"starts with case insensitive", testHelloWorldCased, "HELLO", "STARTS_WITH", true},
|
|
{"ends with true", testHelloWorld, "world", "ENDS_WITH", true},
|
|
{"ends with false", testHelloWorld, "hello", "ENDS_WITH", false},
|
|
{"ends with case insensitive", testHelloWorldCased, "WORLD", "ENDS_WITH", true},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
got := compare(tt.actual, tt.expected, tt.operator)
|
|
if got != tt.want {
|
|
t.Errorf(testCompareFormat, tt.actual, tt.expected, tt.operator, got, tt.want)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestCompareUnknownOperator(t *testing.T) {
|
|
got := compare("value", "value", "UNKNOWN")
|
|
if got != false {
|
|
t.Errorf("compare with unknown operator should return false, got %v", got)
|
|
}
|
|
}
|
|
|
|
func TestNumericCompare(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
actual string
|
|
expected string
|
|
compareFn func(float64, float64) bool
|
|
want bool
|
|
}{
|
|
{
|
|
name: "valid numbers greater",
|
|
actual: "10",
|
|
expected: "5",
|
|
compareFn: func(a, e float64) bool { return a > e },
|
|
want: true,
|
|
},
|
|
{
|
|
name: "valid numbers less",
|
|
actual: "5",
|
|
expected: "10",
|
|
compareFn: func(a, e float64) bool { return a < e },
|
|
want: true,
|
|
},
|
|
{
|
|
name: "invalid actual number",
|
|
actual: "abc",
|
|
expected: "10",
|
|
compareFn: func(a, e float64) bool { return a > e },
|
|
want: false,
|
|
},
|
|
{
|
|
name: "invalid expected number",
|
|
actual: "10",
|
|
expected: "xyz",
|
|
compareFn: func(a, e float64) bool { return a > e },
|
|
want: false,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
got := numericCompare(tt.actual, tt.expected, tt.compareFn)
|
|
if got != tt.want {
|
|
t.Errorf("numericCompare(%q, %q) = %v, want %v", tt.actual, tt.expected, got, tt.want)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestInComparison(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
actual string
|
|
expected string
|
|
want bool
|
|
}{
|
|
{"match in list", "admin", testAdminUserGuest, true},
|
|
{"no match in list", "superuser", testAdminUserGuest, false},
|
|
{"case insensitive", "ADMIN", "admin,user", true},
|
|
{"with whitespace", " admin ", " admin , user ", true},
|
|
{"single item match", "admin", "admin", true},
|
|
{"empty expected", "admin", "", false},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
got := inComparison(tt.actual, tt.expected)
|
|
if got != tt.want {
|
|
t.Errorf("inComparison(%q, %q) = %v, want %v", tt.actual, tt.expected, got, tt.want)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestEvaluatePolicy(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
policy models.PolicyAttribute
|
|
ctx *models.AuthorizationContext
|
|
wantSatisfied bool
|
|
wantReasonEmpty bool
|
|
}{
|
|
{
|
|
name: "user attribute satisfied",
|
|
policy: models.PolicyAttribute{
|
|
AttributeType: "user",
|
|
AttributeName: "department",
|
|
Comparison: "=",
|
|
AttributeValue: "Engineering",
|
|
},
|
|
ctx: &models.AuthorizationContext{
|
|
UserAttributes: map[string]string{"department": "Engineering"},
|
|
},
|
|
wantSatisfied: true,
|
|
wantReasonEmpty: true,
|
|
},
|
|
{
|
|
name: "user attribute not satisfied",
|
|
policy: models.PolicyAttribute{
|
|
AttributeType: "user",
|
|
AttributeName: "department",
|
|
Comparison: "=",
|
|
AttributeValue: "HR",
|
|
},
|
|
ctx: &models.AuthorizationContext{
|
|
UserAttributes: map[string]string{"department": "Engineering"},
|
|
},
|
|
wantSatisfied: false,
|
|
wantReasonEmpty: false,
|
|
},
|
|
{
|
|
name: "user attribute not found",
|
|
policy: models.PolicyAttribute{
|
|
AttributeType: "user",
|
|
AttributeName: "nonexistent",
|
|
Comparison: "=",
|
|
AttributeValue: "value",
|
|
},
|
|
ctx: &models.AuthorizationContext{
|
|
UserAttributes: map[string]string{},
|
|
},
|
|
wantSatisfied: false,
|
|
wantReasonEmpty: false,
|
|
},
|
|
{
|
|
name: "resource attribute satisfied",
|
|
policy: models.PolicyAttribute{
|
|
AttributeType: "resource",
|
|
AttributeName: "owner",
|
|
Comparison: "=",
|
|
AttributeValue: "user123",
|
|
},
|
|
ctx: &models.AuthorizationContext{
|
|
ResourceData: map[string]string{"owner": "user123"},
|
|
},
|
|
wantSatisfied: true,
|
|
wantReasonEmpty: true,
|
|
},
|
|
{
|
|
name: "environment attribute satisfied",
|
|
policy: models.PolicyAttribute{
|
|
AttributeType: "environment",
|
|
AttributeName: "location",
|
|
Comparison: "=",
|
|
AttributeValue: "US",
|
|
},
|
|
ctx: &models.AuthorizationContext{
|
|
Environment: map[string]string{"location": "US"},
|
|
},
|
|
wantSatisfied: true,
|
|
wantReasonEmpty: true,
|
|
},
|
|
{
|
|
name: "unknown attribute type",
|
|
policy: models.PolicyAttribute{
|
|
AttributeType: "unknown",
|
|
AttributeName: "attr",
|
|
Comparison: "=",
|
|
AttributeValue: "value",
|
|
},
|
|
ctx: &models.AuthorizationContext{},
|
|
wantSatisfied: false,
|
|
wantReasonEmpty: false,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
satisfied, reason := evaluatePolicy(tt.policy, tt.ctx)
|
|
|
|
if satisfied != tt.wantSatisfied {
|
|
t.Errorf("evaluatePolicy() satisfied = %v, want %v", satisfied, tt.wantSatisfied)
|
|
}
|
|
|
|
if tt.wantReasonEmpty && reason != "" {
|
|
t.Errorf("evaluatePolicy() reason = %q, want empty", reason)
|
|
}
|
|
|
|
if !tt.wantReasonEmpty && reason == "" {
|
|
t.Errorf("evaluatePolicy() reason is empty, want non-empty")
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestEvaluatePolicies(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
policies []models.PolicyAttribute
|
|
ctx *models.AuthorizationContext
|
|
wantSatisfied bool
|
|
wantReasonEmpty bool
|
|
}{
|
|
{
|
|
name: "no policies returns true",
|
|
policies: []models.PolicyAttribute{},
|
|
ctx: &models.AuthorizationContext{},
|
|
wantSatisfied: true,
|
|
wantReasonEmpty: false,
|
|
},
|
|
{
|
|
name: "all policies satisfied",
|
|
policies: []models.PolicyAttribute{
|
|
{
|
|
AttributeType: "user",
|
|
AttributeName: "department",
|
|
Comparison: "=",
|
|
AttributeValue: "Engineering",
|
|
},
|
|
{
|
|
AttributeType: "user",
|
|
AttributeName: "level",
|
|
Comparison: ">=",
|
|
AttributeValue: "3",
|
|
},
|
|
},
|
|
ctx: &models.AuthorizationContext{
|
|
UserAttributes: map[string]string{
|
|
"department": "Engineering",
|
|
"level": "5",
|
|
},
|
|
},
|
|
wantSatisfied: true,
|
|
wantReasonEmpty: false,
|
|
},
|
|
{
|
|
name: "one policy fails",
|
|
policies: []models.PolicyAttribute{
|
|
{
|
|
AttributeType: "user",
|
|
AttributeName: "department",
|
|
Comparison: "=",
|
|
AttributeValue: "Engineering",
|
|
},
|
|
{
|
|
AttributeType: "user",
|
|
AttributeName: "level",
|
|
Comparison: ">=",
|
|
AttributeValue: "5",
|
|
},
|
|
},
|
|
ctx: &models.AuthorizationContext{
|
|
UserAttributes: map[string]string{
|
|
"department": "Engineering",
|
|
"level": "3",
|
|
},
|
|
},
|
|
wantSatisfied: false,
|
|
wantReasonEmpty: false,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
satisfied, reason := EvaluatePolicies(tt.policies, tt.ctx)
|
|
|
|
if satisfied != tt.wantSatisfied {
|
|
t.Errorf("EvaluatePolicies() satisfied = %v, want %v", satisfied, tt.wantSatisfied)
|
|
}
|
|
|
|
if tt.wantReasonEmpty && reason != "" {
|
|
t.Errorf("EvaluatePolicies() reason = %q, want empty", reason)
|
|
}
|
|
|
|
if !tt.wantReasonEmpty && reason == "" {
|
|
t.Errorf("EvaluatePolicies() reason is empty, want non-empty")
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestResolveVariablesEdgeCases tests variable resolution indirectly through EvaluatePolicies
|
|
func TestResolveVariablesEdgeCases(t *testing.T) {
|
|
// Instead of testing the private function directly, test it through EvaluatePolicies
|
|
testCases := []struct {
|
|
name string
|
|
policy models.PolicyAttribute
|
|
ctx *models.AuthorizationContext
|
|
expectedResult bool
|
|
}{
|
|
{
|
|
name: "Empty string attribute",
|
|
policy: models.PolicyAttribute{
|
|
AttributeName: "empty",
|
|
AttributeType: "user",
|
|
Comparison: "=",
|
|
AttributeValue: "",
|
|
},
|
|
ctx: &models.AuthorizationContext{
|
|
UserAttributes: map[string]string{"empty": ""},
|
|
},
|
|
expectedResult: true,
|
|
},
|
|
{
|
|
name: "Missing attribute",
|
|
policy: models.PolicyAttribute{
|
|
AttributeName: "missing",
|
|
AttributeType: "user",
|
|
Comparison: "=",
|
|
AttributeValue: "value",
|
|
},
|
|
ctx: &models.AuthorizationContext{
|
|
UserAttributes: map[string]string{},
|
|
},
|
|
expectedResult: false,
|
|
},
|
|
{
|
|
name: "Special characters in value",
|
|
policy: models.PolicyAttribute{
|
|
AttributeName: "special",
|
|
AttributeType: "user",
|
|
Comparison: "=",
|
|
AttributeValue: "<>&\"'",
|
|
},
|
|
ctx: &models.AuthorizationContext{
|
|
UserAttributes: map[string]string{"special": "<>&\"'"},
|
|
},
|
|
expectedResult: true,
|
|
},
|
|
}
|
|
|
|
for _, tc := range testCases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
result, _ := EvaluatePolicies([]models.PolicyAttribute{tc.policy}, tc.ctx)
|
|
if result != tc.expectedResult {
|
|
t.Errorf("EvaluatePolicies() = %v, want %v", result, tc.expectedResult)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestCompareCaseSensitivity tests comparison through EvaluatePolicies
|
|
func TestCompareCaseSensitivity(t *testing.T) {
|
|
testCases := []struct {
|
|
name string
|
|
attributeValue string
|
|
userAttrValue string
|
|
operator string
|
|
expected bool
|
|
}{
|
|
{"Equals case sensitive", "Test", "test", "=", false},
|
|
{"Equals same case", "Test", "Test", "=", true},
|
|
{"Not equals case", "Test", "test", "!=", true},
|
|
}
|
|
|
|
for _, tc := range testCases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
policy := models.PolicyAttribute{
|
|
AttributeName: "value",
|
|
AttributeType: "user",
|
|
Comparison: tc.operator,
|
|
AttributeValue: tc.attributeValue,
|
|
}
|
|
ctx := &models.AuthorizationContext{
|
|
UserAttributes: map[string]string{"value": tc.userAttrValue},
|
|
}
|
|
result, _ := EvaluatePolicies([]models.PolicyAttribute{policy}, ctx)
|
|
if result != tc.expected {
|
|
t.Errorf("comparison(%q, %q, %q) = %v, want %v", tc.operator, tc.userAttrValue, tc.attributeValue, result, tc.expected)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestCompareEmptyStrings tests empty string comparisons through EvaluatePolicies
|
|
func TestCompareEmptyStrings(t *testing.T) {
|
|
testCases := []struct {
|
|
name string
|
|
operator string
|
|
userValue string
|
|
expectedValue string
|
|
expectedResult bool
|
|
}{
|
|
{"equals both empty", "=", "", "", true},
|
|
{"equals one empty", "=", "", "value", false},
|
|
{"not_equals both empty", "!=", "", "", false},
|
|
{"not_equals one empty", "!=", "", "value", true},
|
|
{"contains value in empty", "CONTAINS", "", "test", false},
|
|
{"contains empty in value", "CONTAINS", "test", "", true},
|
|
}
|
|
|
|
for _, tc := range testCases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
policy := models.PolicyAttribute{
|
|
AttributeName: "value",
|
|
AttributeType: "user",
|
|
Comparison: tc.operator,
|
|
AttributeValue: tc.expectedValue,
|
|
}
|
|
ctx := &models.AuthorizationContext{
|
|
UserAttributes: map[string]string{"value": tc.userValue},
|
|
}
|
|
result, _ := EvaluatePolicies([]models.PolicyAttribute{policy}, ctx)
|
|
if result != tc.expectedResult {
|
|
t.Errorf("comparison(%q, %q, %q) = %v, want %v", tc.operator, tc.userValue, tc.expectedValue, result, tc.expectedResult)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// Note: Tests for numericCompare removed as it's an internal function.
|
|
// It's tested indirectly through public Compare and EvaluatePolicies functions.
|
|
|
|
// Note: Tests for inComparison removed as it's an internal function.
|
|
// It's tested indirectly through public Compare and Evaluate Policies functions.
|
|
|
|
func TestEvaluatePoliciesNilContext(t *testing.T) {
|
|
policies := []models.PolicyAttribute{
|
|
{AttributeName: "department", Comparison: "equals", AttributeValue: "IT"},
|
|
}
|
|
|
|
satisfied, _ := EvaluatePolicies(policies, nil)
|
|
if satisfied {
|
|
t.Error("EvaluatePolicies should return false for nil context")
|
|
}
|
|
}
|
|
|
|
func TestEvaluatePoliciesEmptyPoliciesList(t *testing.T) {
|
|
ctx := &models.AuthorizationContext{
|
|
UserAttributes: map[string]string{"department": "IT"},
|
|
}
|
|
|
|
satisfied, _ := EvaluatePolicies([]models.PolicyAttribute{}, ctx)
|
|
if !satisfied {
|
|
t.Error("EvaluatePolicies should return true for empty policies list")
|
|
}
|
|
// Note: The function returns "No policies to evaluate" as the reason even when successful
|
|
}
|
|
|
|
func TestEvaluatePoliciesComplexConditions(t *testing.T) {
|
|
ctx := &models.AuthorizationContext{
|
|
UserAttributes: map[string]string{
|
|
"department": "IT",
|
|
"level": "5",
|
|
"location": "US",
|
|
},
|
|
ResourceData: map[string]string{
|
|
"classification": "public",
|
|
},
|
|
Environment: map[string]string{
|
|
"time": "14:00",
|
|
},
|
|
}
|
|
|
|
policies := []models.PolicyAttribute{
|
|
{AttributeName: "department", AttributeType: "user", Comparison: "=", AttributeValue: "IT"},
|
|
{AttributeName: "level", AttributeType: "user", Comparison: ">=", AttributeValue: "3"},
|
|
{AttributeName: "location", AttributeType: "user", Comparison: "IN", AttributeValue: "US,UK,CA"},
|
|
}
|
|
|
|
satisfied, reason := EvaluatePolicies(policies, ctx)
|
|
if !satisfied {
|
|
t.Errorf("EvaluatePolicies should satisfy all conditions, reason: %s", reason)
|
|
}
|
|
}
|
|
|
|
// Note: Tests for compare removed as it's an internal function.
|
|
// It's tested indirectly through public EvaluatePolicies functions.
|
|
|
|
func TestResolveVariablesAllAttributeTypes(t *testing.T) {
|
|
ctx := &models.AuthorizationContext{
|
|
UserID: "user123",
|
|
Resource: "document",
|
|
Action: "read",
|
|
UserAttributes: map[string]string{
|
|
"dept": "IT",
|
|
},
|
|
ResourceData: map[string]string{
|
|
"owner": "user456",
|
|
},
|
|
Environment: map[string]string{
|
|
"ip": "192.168.1.1",
|
|
},
|
|
}
|
|
|
|
testCases := []struct {
|
|
input string
|
|
expected string
|
|
}{
|
|
{"User: ${user.dept}", "User: IT"},
|
|
{"Resource: ${resource.owner}", "Resource: user456"},
|
|
{"Env: ${environment.ip}", "Env: 192.168.1.1"},
|
|
{"Mixed: ${user.dept} ${resource.owner}", "Mixed: IT user456"},
|
|
}
|
|
|
|
for _, tc := range testCases {
|
|
result := resolveVariables(tc.input, ctx)
|
|
if result != tc.expected {
|
|
t.Errorf("resolveVariables(%q) = %q, want %q", tc.input, result, tc.expected)
|
|
}
|
|
}
|
|
}
|
|
|
|
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)
|
|
}
|
|
})
|
|
}
|
|
}
|