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