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