You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
426 lines
9.7 KiB
426 lines
9.7 KiB
package policy
|
|
|
|
import (
|
|
"context"
|
|
"testing"
|
|
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
// TestPolicyEngineInitialization tests policy engine initialization
|
|
func TestPolicyEngineInitialization(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
config *PolicyEngineConfig
|
|
wantErr bool
|
|
}{
|
|
{
|
|
name: "valid config",
|
|
config: &PolicyEngineConfig{
|
|
DefaultEffect: "Deny",
|
|
StoreType: "memory",
|
|
},
|
|
wantErr: false,
|
|
},
|
|
{
|
|
name: "invalid default effect",
|
|
config: &PolicyEngineConfig{
|
|
DefaultEffect: "Invalid",
|
|
StoreType: "memory",
|
|
},
|
|
wantErr: true,
|
|
},
|
|
{
|
|
name: "nil config",
|
|
config: nil,
|
|
wantErr: true,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
engine := NewPolicyEngine()
|
|
|
|
err := engine.Initialize(tt.config)
|
|
|
|
if tt.wantErr {
|
|
assert.Error(t, err)
|
|
} else {
|
|
assert.NoError(t, err)
|
|
assert.True(t, engine.IsInitialized())
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestPolicyDocumentValidation tests policy document structure validation
|
|
func TestPolicyDocumentValidation(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
policy *PolicyDocument
|
|
wantErr bool
|
|
errorMsg string
|
|
}{
|
|
{
|
|
name: "valid policy document",
|
|
policy: &PolicyDocument{
|
|
Version: "2012-10-17",
|
|
Statement: []Statement{
|
|
{
|
|
Sid: "AllowS3Read",
|
|
Effect: "Allow",
|
|
Action: []string{"s3:GetObject", "s3:ListBucket"},
|
|
Resource: []string{"arn:seaweed:s3:::mybucket/*"},
|
|
},
|
|
},
|
|
},
|
|
wantErr: false,
|
|
},
|
|
{
|
|
name: "missing version",
|
|
policy: &PolicyDocument{
|
|
Statement: []Statement{
|
|
{
|
|
Effect: "Allow",
|
|
Action: []string{"s3:GetObject"},
|
|
Resource: []string{"arn:seaweed:s3:::mybucket/*"},
|
|
},
|
|
},
|
|
},
|
|
wantErr: true,
|
|
errorMsg: "version is required",
|
|
},
|
|
{
|
|
name: "empty statements",
|
|
policy: &PolicyDocument{
|
|
Version: "2012-10-17",
|
|
Statement: []Statement{},
|
|
},
|
|
wantErr: true,
|
|
errorMsg: "at least one statement is required",
|
|
},
|
|
{
|
|
name: "invalid effect",
|
|
policy: &PolicyDocument{
|
|
Version: "2012-10-17",
|
|
Statement: []Statement{
|
|
{
|
|
Effect: "Maybe",
|
|
Action: []string{"s3:GetObject"},
|
|
Resource: []string{"arn:seaweed:s3:::mybucket/*"},
|
|
},
|
|
},
|
|
},
|
|
wantErr: true,
|
|
errorMsg: "invalid effect",
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
err := ValidatePolicyDocument(tt.policy)
|
|
|
|
if tt.wantErr {
|
|
assert.Error(t, err)
|
|
if tt.errorMsg != "" {
|
|
assert.Contains(t, err.Error(), tt.errorMsg)
|
|
}
|
|
} else {
|
|
assert.NoError(t, err)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestPolicyEvaluation tests policy evaluation logic
|
|
func TestPolicyEvaluation(t *testing.T) {
|
|
engine := setupTestPolicyEngine(t)
|
|
|
|
// Add test policies
|
|
readPolicy := &PolicyDocument{
|
|
Version: "2012-10-17",
|
|
Statement: []Statement{
|
|
{
|
|
Sid: "AllowS3Read",
|
|
Effect: "Allow",
|
|
Action: []string{"s3:GetObject", "s3:ListBucket"},
|
|
Resource: []string{
|
|
"arn:seaweed:s3:::public-bucket/*", // For object operations
|
|
"arn:seaweed:s3:::public-bucket", // For bucket operations
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
err := engine.AddPolicy("", "read-policy", readPolicy)
|
|
require.NoError(t, err)
|
|
|
|
denyPolicy := &PolicyDocument{
|
|
Version: "2012-10-17",
|
|
Statement: []Statement{
|
|
{
|
|
Sid: "DenyS3Delete",
|
|
Effect: "Deny",
|
|
Action: []string{"s3:DeleteObject"},
|
|
Resource: []string{"arn:seaweed:s3:::*"},
|
|
},
|
|
},
|
|
}
|
|
|
|
err = engine.AddPolicy("", "deny-policy", denyPolicy)
|
|
require.NoError(t, err)
|
|
|
|
tests := []struct {
|
|
name string
|
|
context *EvaluationContext
|
|
policies []string
|
|
want Effect
|
|
}{
|
|
{
|
|
name: "allow read access",
|
|
context: &EvaluationContext{
|
|
Principal: "user:alice",
|
|
Action: "s3:GetObject",
|
|
Resource: "arn:seaweed:s3:::public-bucket/file.txt",
|
|
RequestContext: map[string]interface{}{
|
|
"sourceIP": "192.168.1.100",
|
|
},
|
|
},
|
|
policies: []string{"read-policy"},
|
|
want: EffectAllow,
|
|
},
|
|
{
|
|
name: "deny delete access (explicit deny)",
|
|
context: &EvaluationContext{
|
|
Principal: "user:alice",
|
|
Action: "s3:DeleteObject",
|
|
Resource: "arn:seaweed:s3:::public-bucket/file.txt",
|
|
},
|
|
policies: []string{"read-policy", "deny-policy"},
|
|
want: EffectDeny,
|
|
},
|
|
{
|
|
name: "deny by default (no matching policy)",
|
|
context: &EvaluationContext{
|
|
Principal: "user:alice",
|
|
Action: "s3:PutObject",
|
|
Resource: "arn:seaweed:s3:::public-bucket/file.txt",
|
|
},
|
|
policies: []string{"read-policy"},
|
|
want: EffectDeny,
|
|
},
|
|
{
|
|
name: "allow with wildcard action",
|
|
context: &EvaluationContext{
|
|
Principal: "user:admin",
|
|
Action: "s3:ListBucket",
|
|
Resource: "arn:seaweed:s3:::public-bucket",
|
|
},
|
|
policies: []string{"read-policy"},
|
|
want: EffectAllow,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
result, err := engine.Evaluate(context.Background(), "", tt.context, tt.policies)
|
|
|
|
assert.NoError(t, err)
|
|
assert.Equal(t, tt.want, result.Effect)
|
|
|
|
// Verify evaluation details
|
|
assert.NotNil(t, result.EvaluationDetails)
|
|
assert.Equal(t, tt.context.Action, result.EvaluationDetails.Action)
|
|
assert.Equal(t, tt.context.Resource, result.EvaluationDetails.Resource)
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestConditionEvaluation tests policy conditions
|
|
func TestConditionEvaluation(t *testing.T) {
|
|
engine := setupTestPolicyEngine(t)
|
|
|
|
// Policy with IP address condition
|
|
conditionalPolicy := &PolicyDocument{
|
|
Version: "2012-10-17",
|
|
Statement: []Statement{
|
|
{
|
|
Sid: "AllowFromOfficeIP",
|
|
Effect: "Allow",
|
|
Action: []string{"s3:*"},
|
|
Resource: []string{"arn:seaweed:s3:::*"},
|
|
Condition: map[string]map[string]interface{}{
|
|
"IpAddress": {
|
|
"seaweed:SourceIP": []string{"192.168.1.0/24", "10.0.0.0/8"},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
err := engine.AddPolicy("", "ip-conditional", conditionalPolicy)
|
|
require.NoError(t, err)
|
|
|
|
tests := []struct {
|
|
name string
|
|
context *EvaluationContext
|
|
want Effect
|
|
}{
|
|
{
|
|
name: "allow from office IP",
|
|
context: &EvaluationContext{
|
|
Principal: "user:alice",
|
|
Action: "s3:GetObject",
|
|
Resource: "arn:seaweed:s3:::mybucket/file.txt",
|
|
RequestContext: map[string]interface{}{
|
|
"sourceIP": "192.168.1.100",
|
|
},
|
|
},
|
|
want: EffectAllow,
|
|
},
|
|
{
|
|
name: "deny from external IP",
|
|
context: &EvaluationContext{
|
|
Principal: "user:alice",
|
|
Action: "s3:GetObject",
|
|
Resource: "arn:seaweed:s3:::mybucket/file.txt",
|
|
RequestContext: map[string]interface{}{
|
|
"sourceIP": "8.8.8.8",
|
|
},
|
|
},
|
|
want: EffectDeny,
|
|
},
|
|
{
|
|
name: "allow from internal IP",
|
|
context: &EvaluationContext{
|
|
Principal: "user:alice",
|
|
Action: "s3:PutObject",
|
|
Resource: "arn:seaweed:s3:::mybucket/newfile.txt",
|
|
RequestContext: map[string]interface{}{
|
|
"sourceIP": "10.1.2.3",
|
|
},
|
|
},
|
|
want: EffectAllow,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
result, err := engine.Evaluate(context.Background(), "", tt.context, []string{"ip-conditional"})
|
|
|
|
assert.NoError(t, err)
|
|
assert.Equal(t, tt.want, result.Effect)
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestResourceMatching tests resource ARN matching
|
|
func TestResourceMatching(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
policyResource string
|
|
requestResource string
|
|
want bool
|
|
}{
|
|
{
|
|
name: "exact match",
|
|
policyResource: "arn:seaweed:s3:::mybucket/file.txt",
|
|
requestResource: "arn:seaweed:s3:::mybucket/file.txt",
|
|
want: true,
|
|
},
|
|
{
|
|
name: "wildcard match",
|
|
policyResource: "arn:seaweed:s3:::mybucket/*",
|
|
requestResource: "arn:seaweed:s3:::mybucket/folder/file.txt",
|
|
want: true,
|
|
},
|
|
{
|
|
name: "bucket wildcard",
|
|
policyResource: "arn:seaweed:s3:::*",
|
|
requestResource: "arn:seaweed:s3:::anybucket/file.txt",
|
|
want: true,
|
|
},
|
|
{
|
|
name: "no match different bucket",
|
|
policyResource: "arn:seaweed:s3:::mybucket/*",
|
|
requestResource: "arn:seaweed:s3:::otherbucket/file.txt",
|
|
want: false,
|
|
},
|
|
{
|
|
name: "prefix match",
|
|
policyResource: "arn:seaweed:s3:::mybucket/documents/*",
|
|
requestResource: "arn:seaweed:s3:::mybucket/documents/secret.txt",
|
|
want: true,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
result := matchResource(tt.policyResource, tt.requestResource)
|
|
assert.Equal(t, tt.want, result)
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestActionMatching tests action pattern matching
|
|
func TestActionMatching(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
policyAction string
|
|
requestAction string
|
|
want bool
|
|
}{
|
|
{
|
|
name: "exact match",
|
|
policyAction: "s3:GetObject",
|
|
requestAction: "s3:GetObject",
|
|
want: true,
|
|
},
|
|
{
|
|
name: "wildcard service",
|
|
policyAction: "s3:*",
|
|
requestAction: "s3:PutObject",
|
|
want: true,
|
|
},
|
|
{
|
|
name: "wildcard all",
|
|
policyAction: "*",
|
|
requestAction: "filer:CreateEntry",
|
|
want: true,
|
|
},
|
|
{
|
|
name: "prefix match",
|
|
policyAction: "s3:Get*",
|
|
requestAction: "s3:GetObject",
|
|
want: true,
|
|
},
|
|
{
|
|
name: "no match different service",
|
|
policyAction: "s3:GetObject",
|
|
requestAction: "filer:GetEntry",
|
|
want: false,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
result := matchAction(tt.policyAction, tt.requestAction)
|
|
assert.Equal(t, tt.want, result)
|
|
})
|
|
}
|
|
}
|
|
|
|
// Helper function to set up test policy engine
|
|
func setupTestPolicyEngine(t *testing.T) *PolicyEngine {
|
|
engine := NewPolicyEngine()
|
|
config := &PolicyEngineConfig{
|
|
DefaultEffect: "Deny",
|
|
StoreType: "memory",
|
|
}
|
|
|
|
err := engine.Initialize(config)
|
|
require.NoError(t, err)
|
|
|
|
return engine
|
|
}
|