| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716 |
- package policy_engine
- import (
- "net/http"
- "net/url"
- "testing"
- "github.com/seaweedfs/seaweedfs/weed/s3api/s3err"
- )
- func TestPolicyEngine(t *testing.T) {
- engine := NewPolicyEngine()
- // Test policy JSON
- policyJSON := `{
- "Version": "2012-10-17",
- "Statement": [
- {
- "Effect": "Allow",
- "Action": ["s3:GetObject", "s3:PutObject"],
- "Resource": ["arn:aws:s3:::test-bucket/*"]
- },
- {
- "Effect": "Deny",
- "Action": ["s3:DeleteObject"],
- "Resource": ["arn:aws:s3:::test-bucket/*"],
- "Condition": {
- "StringEquals": {
- "s3:RequestMethod": ["DELETE"]
- }
- }
- }
- ]
- }`
- // Set bucket policy
- err := engine.SetBucketPolicy("test-bucket", policyJSON)
- if err != nil {
- t.Fatalf("Failed to set bucket policy: %v", err)
- }
- // Test Allow case
- args := &PolicyEvaluationArgs{
- Action: "s3:GetObject",
- Resource: "arn:aws:s3:::test-bucket/test-object",
- Principal: "user1",
- Conditions: map[string][]string{},
- }
- result := engine.EvaluatePolicy("test-bucket", args)
- if result != PolicyResultAllow {
- t.Errorf("Expected Allow, got %v", result)
- }
- // Test Deny case
- args = &PolicyEvaluationArgs{
- Action: "s3:DeleteObject",
- Resource: "arn:aws:s3:::test-bucket/test-object",
- Principal: "user1",
- Conditions: map[string][]string{
- "s3:RequestMethod": {"DELETE"},
- },
- }
- result = engine.EvaluatePolicy("test-bucket", args)
- if result != PolicyResultDeny {
- t.Errorf("Expected Deny, got %v", result)
- }
- // Test non-matching action
- args = &PolicyEvaluationArgs{
- Action: "s3:ListBucket",
- Resource: "arn:aws:s3:::test-bucket",
- Principal: "user1",
- Conditions: map[string][]string{},
- }
- result = engine.EvaluatePolicy("test-bucket", args)
- if result != PolicyResultDeny {
- t.Errorf("Expected Deny for non-matching action, got %v", result)
- }
- // Test GetBucketPolicy
- policy, err := engine.GetBucketPolicy("test-bucket")
- if err != nil {
- t.Fatalf("Failed to get bucket policy: %v", err)
- }
- if policy.Version != "2012-10-17" {
- t.Errorf("Expected version 2012-10-17, got %s", policy.Version)
- }
- // Test DeleteBucketPolicy
- err = engine.DeleteBucketPolicy("test-bucket")
- if err != nil {
- t.Fatalf("Failed to delete bucket policy: %v", err)
- }
- // Test policy is gone
- result = engine.EvaluatePolicy("test-bucket", args)
- if result != PolicyResultIndeterminate {
- t.Errorf("Expected Indeterminate after policy deletion, got %v", result)
- }
- }
- func TestConditionEvaluators(t *testing.T) {
- tests := []struct {
- name string
- operator string
- conditionValue interface{}
- contextValues []string
- expected bool
- }{
- {
- name: "StringEquals - match",
- operator: "StringEquals",
- conditionValue: "test-value",
- contextValues: []string{"test-value"},
- expected: true,
- },
- {
- name: "StringEquals - no match",
- operator: "StringEquals",
- conditionValue: "test-value",
- contextValues: []string{"other-value"},
- expected: false,
- },
- {
- name: "StringLike - wildcard match",
- operator: "StringLike",
- conditionValue: "test-*",
- contextValues: []string{"test-value"},
- expected: true,
- },
- {
- name: "StringLike - wildcard no match",
- operator: "StringLike",
- conditionValue: "test-*",
- contextValues: []string{"other-value"},
- expected: false,
- },
- {
- name: "NumericEquals - match",
- operator: "NumericEquals",
- conditionValue: "42",
- contextValues: []string{"42"},
- expected: true,
- },
- {
- name: "NumericLessThan - match",
- operator: "NumericLessThan",
- conditionValue: "100",
- contextValues: []string{"50"},
- expected: true,
- },
- {
- name: "NumericLessThan - no match",
- operator: "NumericLessThan",
- conditionValue: "100",
- contextValues: []string{"150"},
- expected: false,
- },
- {
- name: "IpAddress - CIDR match",
- operator: "IpAddress",
- conditionValue: "192.168.1.0/24",
- contextValues: []string{"192.168.1.100"},
- expected: true,
- },
- {
- name: "IpAddress - CIDR no match",
- operator: "IpAddress",
- conditionValue: "192.168.1.0/24",
- contextValues: []string{"10.0.0.1"},
- expected: false,
- },
- {
- name: "Bool - true match",
- operator: "Bool",
- conditionValue: "true",
- contextValues: []string{"true"},
- expected: true,
- },
- {
- name: "Bool - false match",
- operator: "Bool",
- conditionValue: "false",
- contextValues: []string{"false"},
- expected: true,
- },
- {
- name: "Bool - no match",
- operator: "Bool",
- conditionValue: "true",
- contextValues: []string{"false"},
- expected: false,
- },
- }
- for _, tt := range tests {
- t.Run(tt.name, func(t *testing.T) {
- evaluator, err := GetConditionEvaluator(tt.operator)
- if err != nil {
- t.Fatalf("Failed to get condition evaluator: %v", err)
- }
- result := evaluator.Evaluate(tt.conditionValue, tt.contextValues)
- if result != tt.expected {
- t.Errorf("Expected %v, got %v", tt.expected, result)
- }
- })
- }
- }
- func TestConvertIdentityToPolicy(t *testing.T) {
- identityActions := []string{
- "Read:bucket1/*",
- "Write:bucket1/*",
- "Admin:bucket2",
- }
- policy, err := ConvertIdentityToPolicy(identityActions, "bucket1")
- if err != nil {
- t.Fatalf("Failed to convert identity to policy: %v", err)
- }
- if policy.Version != "2012-10-17" {
- t.Errorf("Expected version 2012-10-17, got %s", policy.Version)
- }
- if len(policy.Statement) != 3 {
- t.Errorf("Expected 3 statements, got %d", len(policy.Statement))
- }
- // Check first statement (Read)
- stmt := policy.Statement[0]
- if stmt.Effect != PolicyEffectAllow {
- t.Errorf("Expected Allow effect, got %s", stmt.Effect)
- }
- actions := normalizeToStringSlice(stmt.Action)
- if len(actions) != 3 {
- t.Errorf("Expected 3 read actions, got %d", len(actions))
- }
- resources := normalizeToStringSlice(stmt.Resource)
- if len(resources) != 2 {
- t.Errorf("Expected 2 resources, got %d", len(resources))
- }
- }
- func TestPolicyValidation(t *testing.T) {
- tests := []struct {
- name string
- policyJSON string
- expectError bool
- }{
- {
- name: "Valid policy",
- policyJSON: `{
- "Version": "2012-10-17",
- "Statement": [
- {
- "Effect": "Allow",
- "Action": "s3:GetObject",
- "Resource": "arn:aws:s3:::test-bucket/*"
- }
- ]
- }`,
- expectError: false,
- },
- {
- name: "Invalid version",
- policyJSON: `{
- "Version": "2008-10-17",
- "Statement": [
- {
- "Effect": "Allow",
- "Action": "s3:GetObject",
- "Resource": "arn:aws:s3:::test-bucket/*"
- }
- ]
- }`,
- expectError: true,
- },
- {
- name: "Missing action",
- policyJSON: `{
- "Version": "2012-10-17",
- "Statement": [
- {
- "Effect": "Allow",
- "Resource": "arn:aws:s3:::test-bucket/*"
- }
- ]
- }`,
- expectError: true,
- },
- {
- name: "Invalid JSON",
- policyJSON: `{
- "Version": "2012-10-17",
- "Statement": [
- {
- "Effect": "Allow",
- "Action": "s3:GetObject",
- "Resource": "arn:aws:s3:::test-bucket/*"
- }
- ]
- }extra`,
- expectError: true,
- },
- }
- for _, tt := range tests {
- t.Run(tt.name, func(t *testing.T) {
- _, err := ParsePolicy(tt.policyJSON)
- if (err != nil) != tt.expectError {
- t.Errorf("Expected error: %v, got error: %v", tt.expectError, err)
- }
- })
- }
- }
- func TestPatternMatching(t *testing.T) {
- tests := []struct {
- name string
- pattern string
- value string
- expected bool
- }{
- {
- name: "Exact match",
- pattern: "s3:GetObject",
- value: "s3:GetObject",
- expected: true,
- },
- {
- name: "Wildcard match",
- pattern: "s3:Get*",
- value: "s3:GetObject",
- expected: true,
- },
- {
- name: "Wildcard no match",
- pattern: "s3:Put*",
- value: "s3:GetObject",
- expected: false,
- },
- {
- name: "Full wildcard",
- pattern: "*",
- value: "anything",
- expected: true,
- },
- {
- name: "Question mark wildcard",
- pattern: "s3:GetObjec?",
- value: "s3:GetObject",
- expected: true,
- },
- }
- for _, tt := range tests {
- t.Run(tt.name, func(t *testing.T) {
- compiled, err := compilePattern(tt.pattern)
- if err != nil {
- t.Fatalf("Failed to compile pattern %s: %v", tt.pattern, err)
- }
- result := compiled.MatchString(tt.value)
- if result != tt.expected {
- t.Errorf("Pattern %s against %s: expected %v, got %v", tt.pattern, tt.value, tt.expected, result)
- }
- })
- }
- }
- func TestExtractConditionValuesFromRequest(t *testing.T) {
- // Create a test request
- req := &http.Request{
- Method: "GET",
- URL: &url.URL{
- Path: "/test-bucket/test-object",
- RawQuery: "prefix=test&delimiter=/",
- },
- Header: map[string][]string{
- "User-Agent": {"test-agent"},
- "X-Amz-Copy-Source": {"source-bucket/source-object"},
- },
- RemoteAddr: "192.168.1.100:12345",
- }
- values := ExtractConditionValuesFromRequest(req)
- // Check extracted values
- if len(values["aws:SourceIp"]) != 1 || values["aws:SourceIp"][0] != "192.168.1.100" {
- t.Errorf("Expected SourceIp to be 192.168.1.100, got %v", values["aws:SourceIp"])
- }
- if len(values["aws:UserAgent"]) != 1 || values["aws:UserAgent"][0] != "test-agent" {
- t.Errorf("Expected UserAgent to be test-agent, got %v", values["aws:UserAgent"])
- }
- if len(values["s3:prefix"]) != 1 || values["s3:prefix"][0] != "test" {
- t.Errorf("Expected prefix to be test, got %v", values["s3:prefix"])
- }
- if len(values["s3:delimiter"]) != 1 || values["s3:delimiter"][0] != "/" {
- t.Errorf("Expected delimiter to be /, got %v", values["s3:delimiter"])
- }
- if len(values["s3:RequestMethod"]) != 1 || values["s3:RequestMethod"][0] != "GET" {
- t.Errorf("Expected RequestMethod to be GET, got %v", values["s3:RequestMethod"])
- }
- if len(values["x-amz-copy-source"]) != 1 || values["x-amz-copy-source"][0] != "source-bucket/source-object" {
- t.Errorf("Expected X-Amz-Copy-Source header to be extracted, got %v", values["x-amz-copy-source"])
- }
- // Check that aws:CurrentTime is properly set
- if len(values["aws:CurrentTime"]) != 1 {
- t.Errorf("Expected aws:CurrentTime to be set, got %v", values["aws:CurrentTime"])
- }
- // Check that aws:RequestTime is still available for backward compatibility
- if len(values["aws:RequestTime"]) != 1 {
- t.Errorf("Expected aws:RequestTime to be set for backward compatibility, got %v", values["aws:RequestTime"])
- }
- }
- func TestPolicyEvaluationWithConditions(t *testing.T) {
- engine := NewPolicyEngine()
- // Policy with IP condition
- policyJSON := `{
- "Version": "2012-10-17",
- "Statement": [
- {
- "Effect": "Allow",
- "Action": "s3:GetObject",
- "Resource": "arn:aws:s3:::test-bucket/*",
- "Condition": {
- "IpAddress": {
- "aws:SourceIp": "192.168.1.0/24"
- }
- }
- }
- ]
- }`
- err := engine.SetBucketPolicy("test-bucket", policyJSON)
- if err != nil {
- t.Fatalf("Failed to set bucket policy: %v", err)
- }
- // Test matching IP
- args := &PolicyEvaluationArgs{
- Action: "s3:GetObject",
- Resource: "arn:aws:s3:::test-bucket/test-object",
- Principal: "user1",
- Conditions: map[string][]string{
- "aws:SourceIp": {"192.168.1.100"},
- },
- }
- result := engine.EvaluatePolicy("test-bucket", args)
- if result != PolicyResultAllow {
- t.Errorf("Expected Allow for matching IP, got %v", result)
- }
- // Test non-matching IP
- args.Conditions["aws:SourceIp"] = []string{"10.0.0.1"}
- result = engine.EvaluatePolicy("test-bucket", args)
- if result != PolicyResultDeny {
- t.Errorf("Expected Deny for non-matching IP, got %v", result)
- }
- }
- func TestResourceArn(t *testing.T) {
- tests := []struct {
- name string
- bucketName string
- objectName string
- expected string
- }{
- {
- name: "Bucket only",
- bucketName: "test-bucket",
- objectName: "",
- expected: "arn:aws:s3:::test-bucket",
- },
- {
- name: "Bucket and object",
- bucketName: "test-bucket",
- objectName: "test-object",
- expected: "arn:aws:s3:::test-bucket/test-object",
- },
- {
- name: "Bucket and nested object",
- bucketName: "test-bucket",
- objectName: "folder/subfolder/test-object",
- expected: "arn:aws:s3:::test-bucket/folder/subfolder/test-object",
- },
- }
- for _, tt := range tests {
- t.Run(tt.name, func(t *testing.T) {
- result := BuildResourceArn(tt.bucketName, tt.objectName)
- if result != tt.expected {
- t.Errorf("Expected %s, got %s", tt.expected, result)
- }
- })
- }
- }
- func TestActionConversion(t *testing.T) {
- tests := []struct {
- name string
- action string
- expected string
- }{
- {
- name: "Already has s3 prefix",
- action: "s3:GetObject",
- expected: "s3:GetObject",
- },
- {
- name: "Add s3 prefix",
- action: "GetObject",
- expected: "s3:GetObject",
- },
- }
- for _, tt := range tests {
- t.Run(tt.name, func(t *testing.T) {
- result := BuildActionName(tt.action)
- if result != tt.expected {
- t.Errorf("Expected %s, got %s", tt.expected, result)
- }
- })
- }
- }
- func TestPolicyEngineForRequest(t *testing.T) {
- engine := NewPolicyEngine()
- // Set up a policy
- policyJSON := `{
- "Version": "2012-10-17",
- "Statement": [
- {
- "Effect": "Allow",
- "Action": "s3:GetObject",
- "Resource": "arn:aws:s3:::test-bucket/*",
- "Condition": {
- "StringEquals": {
- "s3:RequestMethod": "GET"
- }
- }
- }
- ]
- }`
- err := engine.SetBucketPolicy("test-bucket", policyJSON)
- if err != nil {
- t.Fatalf("Failed to set bucket policy: %v", err)
- }
- // Create test request
- req := &http.Request{
- Method: "GET",
- URL: &url.URL{
- Path: "/test-bucket/test-object",
- },
- Header: make(map[string][]string),
- RemoteAddr: "192.168.1.100:12345",
- }
- // Test the request
- result := engine.EvaluatePolicyForRequest("test-bucket", "test-object", "GetObject", "user1", req)
- if result != PolicyResultAllow {
- t.Errorf("Expected Allow for matching request, got %v", result)
- }
- }
- func TestWildcardMatching(t *testing.T) {
- tests := []struct {
- name string
- pattern string
- str string
- expected bool
- }{
- {
- name: "Exact match",
- pattern: "test",
- str: "test",
- expected: true,
- },
- {
- name: "Single wildcard",
- pattern: "*",
- str: "anything",
- expected: true,
- },
- {
- name: "Prefix wildcard",
- pattern: "test*",
- str: "test123",
- expected: true,
- },
- {
- name: "Suffix wildcard",
- pattern: "*test",
- str: "123test",
- expected: true,
- },
- {
- name: "Middle wildcard",
- pattern: "test*123",
- str: "testABC123",
- expected: true,
- },
- {
- name: "No match",
- pattern: "test*",
- str: "other",
- expected: false,
- },
- {
- name: "Multiple wildcards",
- pattern: "test*abc*123",
- str: "testXYZabcDEF123",
- expected: true,
- },
- }
- for _, tt := range tests {
- t.Run(tt.name, func(t *testing.T) {
- result := MatchesWildcard(tt.pattern, tt.str)
- if result != tt.expected {
- t.Errorf("Pattern %s against %s: expected %v, got %v", tt.pattern, tt.str, tt.expected, result)
- }
- })
- }
- }
- func TestCompilePolicy(t *testing.T) {
- policyJSON := `{
- "Version": "2012-10-17",
- "Statement": [
- {
- "Effect": "Allow",
- "Action": ["s3:GetObject", "s3:PutObject"],
- "Resource": "arn:aws:s3:::test-bucket/*"
- }
- ]
- }`
- policy, err := ParsePolicy(policyJSON)
- if err != nil {
- t.Fatalf("Failed to parse policy: %v", err)
- }
- compiled, err := CompilePolicy(policy)
- if err != nil {
- t.Fatalf("Failed to compile policy: %v", err)
- }
- if len(compiled.Statements) != 1 {
- t.Errorf("Expected 1 compiled statement, got %d", len(compiled.Statements))
- }
- stmt := compiled.Statements[0]
- if len(stmt.ActionPatterns) != 2 {
- t.Errorf("Expected 2 action patterns, got %d", len(stmt.ActionPatterns))
- }
- if len(stmt.ResourcePatterns) != 1 {
- t.Errorf("Expected 1 resource pattern, got %d", len(stmt.ResourcePatterns))
- }
- }
- // TestNewPolicyBackedIAMWithLegacy tests the constructor overload
- func TestNewPolicyBackedIAMWithLegacy(t *testing.T) {
- // Mock legacy IAM
- mockLegacyIAM := &MockLegacyIAM{}
- // Test the new constructor
- policyBackedIAM := NewPolicyBackedIAMWithLegacy(mockLegacyIAM)
- // Verify that the legacy IAM is set
- if policyBackedIAM.legacyIAM != mockLegacyIAM {
- t.Errorf("Expected legacy IAM to be set, but it wasn't")
- }
- // Verify that the policy engine is initialized
- if policyBackedIAM.policyEngine == nil {
- t.Errorf("Expected policy engine to be initialized, but it wasn't")
- }
- // Compare with the traditional approach
- traditionalIAM := NewPolicyBackedIAM()
- traditionalIAM.SetLegacyIAM(mockLegacyIAM)
- // Both should behave the same
- if policyBackedIAM.legacyIAM != traditionalIAM.legacyIAM {
- t.Errorf("Expected both approaches to result in the same legacy IAM")
- }
- }
- // MockLegacyIAM implements the LegacyIAM interface for testing
- type MockLegacyIAM struct{}
- func (m *MockLegacyIAM) authRequest(r *http.Request, action Action) (Identity, s3err.ErrorCode) {
- return nil, s3err.ErrNone
- }
|