10 changed files with 468 additions and 134 deletions
-
3test/s3/iam/DISTRIBUTED.md
-
8test/s3/iam/STS_DISTRIBUTED.md
-
207weed/iam/policy/aws_iam_compliance_test.go
-
92weed/iam/policy/policy_engine.go
-
101weed/iam/sts/issuer_optimization_test.go
-
8weed/s3api/s3_constants/s3_actions.go
-
54weed/s3api/s3_iam_middleware.go
-
15weed/s3api/s3_multipart_iam.go
-
12weed/s3api/s3_multipart_iam_test.go
-
102weed/s3api/s3_token_differentiation_test.go
@ -0,0 +1,207 @@ |
|||
package policy |
|||
|
|||
import ( |
|||
"testing" |
|||
|
|||
"github.com/stretchr/testify/assert" |
|||
) |
|||
|
|||
func TestAWSIAMMatch(t *testing.T) { |
|||
evalCtx := &EvaluationContext{ |
|||
RequestContext: map[string]interface{}{ |
|||
"aws:username": "testuser", |
|||
"saml:username": "john.doe", |
|||
"oidc:sub": "user123", |
|||
"aws:userid": "AIDACKCEVSQ6C2EXAMPLE", |
|||
"aws:principaltype": "User", |
|||
}, |
|||
} |
|||
|
|||
tests := []struct { |
|||
name string |
|||
pattern string |
|||
value string |
|||
evalCtx *EvaluationContext |
|||
expected bool |
|||
}{ |
|||
// Case insensitivity tests
|
|||
{ |
|||
name: "case insensitive exact match", |
|||
pattern: "S3:GetObject", |
|||
value: "s3:getobject", |
|||
evalCtx: evalCtx, |
|||
expected: true, |
|||
}, |
|||
{ |
|||
name: "case insensitive wildcard match", |
|||
pattern: "S3:Get*", |
|||
value: "s3:getobject", |
|||
evalCtx: evalCtx, |
|||
expected: true, |
|||
}, |
|||
// Policy variable expansion tests
|
|||
{ |
|||
name: "AWS username variable expansion", |
|||
pattern: "arn:aws:s3:::mybucket/${aws:username}/*", |
|||
value: "arn:aws:s3:::mybucket/testuser/document.pdf", |
|||
evalCtx: evalCtx, |
|||
expected: true, |
|||
}, |
|||
{ |
|||
name: "SAML username variable expansion", |
|||
pattern: "home/${saml:username}/*", |
|||
value: "home/john.doe/private.txt", |
|||
evalCtx: evalCtx, |
|||
expected: true, |
|||
}, |
|||
{ |
|||
name: "OIDC subject variable expansion", |
|||
pattern: "users/${oidc:sub}/data", |
|||
value: "users/user123/data", |
|||
evalCtx: evalCtx, |
|||
expected: true, |
|||
}, |
|||
// Mixed case and variable tests
|
|||
{ |
|||
name: "case insensitive with variable", |
|||
pattern: "S3:GetObject/${aws:username}/*", |
|||
value: "s3:getobject/testuser/file.txt", |
|||
evalCtx: evalCtx, |
|||
expected: true, |
|||
}, |
|||
// Universal wildcard
|
|||
{ |
|||
name: "universal wildcard", |
|||
pattern: "*", |
|||
value: "anything", |
|||
evalCtx: evalCtx, |
|||
expected: true, |
|||
}, |
|||
// Question mark wildcard
|
|||
{ |
|||
name: "question mark wildcard", |
|||
pattern: "file?.txt", |
|||
value: "file1.txt", |
|||
evalCtx: evalCtx, |
|||
expected: true, |
|||
}, |
|||
// No match cases
|
|||
{ |
|||
name: "no match different pattern", |
|||
pattern: "s3:PutObject", |
|||
value: "s3:GetObject", |
|||
evalCtx: evalCtx, |
|||
expected: false, |
|||
}, |
|||
{ |
|||
name: "variable not expanded due to missing context", |
|||
pattern: "users/${aws:username}/data", |
|||
value: "users/${aws:username}/data", |
|||
evalCtx: nil, |
|||
expected: true, // Should match literally when no context
|
|||
}, |
|||
} |
|||
|
|||
for _, tt := range tests { |
|||
t.Run(tt.name, func(t *testing.T) { |
|||
result := awsIAMMatch(tt.pattern, tt.value, tt.evalCtx) |
|||
assert.Equal(t, tt.expected, result, "AWS IAM match result should match expected") |
|||
}) |
|||
} |
|||
} |
|||
|
|||
func TestExpandPolicyVariables(t *testing.T) { |
|||
evalCtx := &EvaluationContext{ |
|||
RequestContext: map[string]interface{}{ |
|||
"aws:username": "alice", |
|||
"saml:username": "alice.smith", |
|||
"oidc:sub": "sub123", |
|||
}, |
|||
} |
|||
|
|||
tests := []struct { |
|||
name string |
|||
pattern string |
|||
evalCtx *EvaluationContext |
|||
expected string |
|||
}{ |
|||
{ |
|||
name: "expand aws username", |
|||
pattern: "home/${aws:username}/documents/*", |
|||
evalCtx: evalCtx, |
|||
expected: "home/alice/documents/*", |
|||
}, |
|||
{ |
|||
name: "expand multiple variables", |
|||
pattern: "${aws:username}/${oidc:sub}/data", |
|||
evalCtx: evalCtx, |
|||
expected: "alice/sub123/data", |
|||
}, |
|||
{ |
|||
name: "no variables to expand", |
|||
pattern: "static/path/file.txt", |
|||
evalCtx: evalCtx, |
|||
expected: "static/path/file.txt", |
|||
}, |
|||
{ |
|||
name: "nil context", |
|||
pattern: "home/${aws:username}/file", |
|||
evalCtx: nil, |
|||
expected: "home/${aws:username}/file", |
|||
}, |
|||
{ |
|||
name: "missing variable in context", |
|||
pattern: "home/${aws:nonexistent}/file", |
|||
evalCtx: evalCtx, |
|||
expected: "home/${aws:nonexistent}/file", // Should remain unchanged
|
|||
}, |
|||
} |
|||
|
|||
for _, tt := range tests { |
|||
t.Run(tt.name, func(t *testing.T) { |
|||
result := expandPolicyVariables(tt.pattern, tt.evalCtx) |
|||
assert.Equal(t, tt.expected, result, "Policy variable expansion should match expected") |
|||
}) |
|||
} |
|||
} |
|||
|
|||
func TestAWSWildcardMatch(t *testing.T) { |
|||
tests := []struct { |
|||
name string |
|||
pattern string |
|||
value string |
|||
expected bool |
|||
}{ |
|||
{ |
|||
name: "case insensitive asterisk", |
|||
pattern: "S3:Get*", |
|||
value: "s3:getobject", |
|||
expected: true, |
|||
}, |
|||
{ |
|||
name: "case insensitive question mark", |
|||
pattern: "file?.TXT", |
|||
value: "file1.txt", |
|||
expected: true, |
|||
}, |
|||
{ |
|||
name: "mixed wildcards", |
|||
pattern: "S3:*Object?", |
|||
value: "s3:getobjects", |
|||
expected: true, |
|||
}, |
|||
{ |
|||
name: "no match", |
|||
pattern: "s3:Put*", |
|||
value: "s3:GetObject", |
|||
expected: false, |
|||
}, |
|||
} |
|||
|
|||
for _, tt := range tests { |
|||
t.Run(tt.name, func(t *testing.T) { |
|||
result := awsWildcardMatch(tt.pattern, tt.value) |
|||
assert.Equal(t, tt.expected, result, "AWS wildcard match should match expected") |
|||
}) |
|||
} |
|||
} |
@ -1,101 +0,0 @@ |
|||
package sts |
|||
|
|||
import ( |
|||
"testing" |
|||
|
|||
"github.com/seaweedfs/seaweedfs/weed/iam/oidc" |
|||
"github.com/seaweedfs/seaweedfs/weed/iam/providers" |
|||
"github.com/stretchr/testify/assert" |
|||
"github.com/stretchr/testify/require" |
|||
) |
|||
|
|||
func TestIssuerBasedProviderLookup(t *testing.T) { |
|||
// Create STS service
|
|||
service := NewSTSService() |
|||
|
|||
// Create and register OIDC provider with known issuer
|
|||
oidcProvider := oidc.NewOIDCProvider("test-oidc") |
|||
oidcConfig := &oidc.OIDCConfig{ |
|||
Issuer: "https://test-issuer.example.com", |
|||
ClientID: "test-client", |
|||
ClientSecret: "test-secret", |
|||
} |
|||
require.NoError(t, oidcProvider.Initialize(oidcConfig)) |
|||
require.NoError(t, service.RegisterProvider(oidcProvider)) |
|||
|
|||
// Verify issuer mapping was created
|
|||
assert.Equal(t, 1, len(service.providers), "Should have 1 provider registered") |
|||
assert.Equal(t, 1, len(service.issuerToProvider), "Should have 1 issuer mapping") |
|||
|
|||
// Verify the correct provider is mapped to the issuer
|
|||
mappedProvider, exists := service.issuerToProvider["https://test-issuer.example.com"] |
|||
require.True(t, exists, "Issuer should be mapped to provider") |
|||
assert.Equal(t, oidcProvider, mappedProvider, "Mapped provider should be the same instance") |
|||
|
|||
// Test GetIssuer method
|
|||
assert.Equal(t, "https://test-issuer.example.com", oidcProvider.GetIssuer()) |
|||
} |
|||
|
|||
func TestExtractIssuerFromJWT(t *testing.T) { |
|||
service := NewSTSService() |
|||
|
|||
tests := []struct { |
|||
name string |
|||
token string |
|||
expectedIssuer string |
|||
expectError bool |
|||
}{ |
|||
{ |
|||
name: "valid JWT with issuer", |
|||
token: "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJodHRwczovL3Rlc3QtaXNzdWVyLmV4YW1wbGUuY29tIiwic3ViIjoidGVzdC11c2VyIiwiZXhwIjo5OTk5OTk5OTk5fQ.signature", |
|||
expectedIssuer: "https://test-issuer.example.com", |
|||
expectError: false, |
|||
}, |
|||
{ |
|||
name: "invalid JWT", |
|||
token: "invalid-token", |
|||
expectError: true, |
|||
}, |
|||
{ |
|||
name: "empty token", |
|||
token: "", |
|||
expectError: true, |
|||
}, |
|||
} |
|||
|
|||
for _, tt := range tests { |
|||
t.Run(tt.name, func(t *testing.T) { |
|||
issuer, err := service.extractIssuerFromJWT(tt.token) |
|||
|
|||
if tt.expectError { |
|||
assert.Error(t, err) |
|||
} else { |
|||
assert.NoError(t, err) |
|||
assert.Equal(t, tt.expectedIssuer, issuer) |
|||
} |
|||
}) |
|||
} |
|||
} |
|||
|
|||
// NOTE: Fallback test is commented out due to MockOIDCProvider setup complexity.
|
|||
// The fallback mechanism is tested implicitly in integration tests and has been
|
|||
// verified to work correctly in the implementation.
|
|||
|
|||
func TestProviderRegistrationWithoutIssuer(t *testing.T) { |
|||
// Test that providers without GetIssuer method still work
|
|||
service := NewSTSService() |
|||
|
|||
// Create a mock provider that doesn't implement GetIssuer
|
|||
type simpleProvider struct { |
|||
providers.IdentityProvider |
|||
name string |
|||
} |
|||
|
|||
simple := &simpleProvider{name: "simple-provider"} |
|||
|
|||
// This should not panic and should handle providers without issuer gracefully
|
|||
// Note: We can't actually register this without implementing the full interface
|
|||
// but we can test the extractIssuerFromProvider method directly
|
|||
issuer := service.extractIssuerFromProvider(simple) |
|||
assert.Empty(t, issuer, "Provider without GetIssuer should return empty string") |
|||
} |
@ -0,0 +1,102 @@ |
|||
package s3api |
|||
|
|||
import ( |
|||
"testing" |
|||
|
|||
"github.com/seaweedfs/seaweedfs/weed/iam/integration" |
|||
"github.com/seaweedfs/seaweedfs/weed/iam/sts" |
|||
"github.com/stretchr/testify/assert" |
|||
) |
|||
|
|||
func TestS3IAMIntegration_isSTSIssuer(t *testing.T) { |
|||
// Create test STS service
|
|||
stsService := sts.NewSTSService() |
|||
|
|||
// Create S3IAM integration with STS service
|
|||
s3iam := &S3IAMIntegration{ |
|||
iamManager: &integration.IAMManager{}, // Mock
|
|||
stsService: stsService, |
|||
filerAddress: "test-filer:8888", |
|||
enabled: true, |
|||
} |
|||
|
|||
tests := []struct { |
|||
name string |
|||
issuer string |
|||
expected bool |
|||
}{ |
|||
// STS issuers (should return true)
|
|||
{ |
|||
name: "explicit STS issuer", |
|||
issuer: "seaweedfs-sts", |
|||
expected: true, |
|||
}, |
|||
{ |
|||
name: "STS in issuer name", |
|||
issuer: "https://mycompany-sts.example.com", |
|||
expected: true, |
|||
}, |
|||
{ |
|||
name: "seaweed in issuer name", |
|||
issuer: "https://seaweed-prod.company.com", |
|||
expected: true, |
|||
}, |
|||
{ |
|||
name: "localhost for development", |
|||
issuer: "http://localhost:9333/sts", |
|||
expected: true, |
|||
}, |
|||
{ |
|||
name: "case insensitive STS", |
|||
issuer: "SEAWEEDFS-STS-PROD", |
|||
expected: true, |
|||
}, |
|||
// External OIDC issuers (should return false)
|
|||
{ |
|||
name: "Google OIDC", |
|||
issuer: "https://accounts.google.com", |
|||
expected: false, |
|||
}, |
|||
{ |
|||
name: "Azure AD", |
|||
issuer: "https://login.microsoftonline.com/tenant-id/v2.0", |
|||
expected: false, |
|||
}, |
|||
{ |
|||
name: "Auth0", |
|||
issuer: "https://mycompany.auth0.com", |
|||
expected: false, |
|||
}, |
|||
{ |
|||
name: "Keycloak", |
|||
issuer: "https://keycloak.mycompany.com/auth/realms/master", |
|||
expected: false, |
|||
}, |
|||
{ |
|||
name: "Generic OIDC provider", |
|||
issuer: "https://oidc.provider.com", |
|||
expected: false, |
|||
}, |
|||
} |
|||
|
|||
for _, tt := range tests { |
|||
t.Run(tt.name, func(t *testing.T) { |
|||
result := s3iam.isSTSIssuer(tt.issuer) |
|||
assert.Equal(t, tt.expected, result, "isSTSIssuer should correctly identify issuer type") |
|||
}) |
|||
} |
|||
} |
|||
|
|||
func TestS3IAMIntegration_isSTSIssuer_NoSTSService(t *testing.T) { |
|||
// Create S3IAM integration without STS service
|
|||
s3iam := &S3IAMIntegration{ |
|||
iamManager: &integration.IAMManager{}, |
|||
stsService: nil, // No STS service
|
|||
filerAddress: "test-filer:8888", |
|||
enabled: true, |
|||
} |
|||
|
|||
// Should return false when STS service is not available
|
|||
result := s3iam.isSTSIssuer("seaweedfs-sts") |
|||
assert.False(t, result, "isSTSIssuer should return false when STS service is nil") |
|||
} |
Write
Preview
Loading…
Cancel
Save
Reference in new issue