mirror of
https://github.com/seaweedfs/seaweedfs.git
synced 2026-10-30 08:50:48 +01:00
address comments
This commit is contained in:
1 parent
850f0e0cde
commit
812f5c1a5a
10 files changed
+468
-134
No files matched your search
@@ -130,9 +130,6 @@ When using filer storage, IAM data is stored at:
|
||||
|
||||
```
|
||||
/seaweedfs/iam/
|
||||
├── sessions/ # STS session tokens
|
||||
│ ├── session_abc123.json
|
||||
│ └── session_def456.json
|
||||
├── policies/ # IAM policy documents
|
||||
│ ├── policy_S3AdminPolicy.json
|
||||
│ └── policy_S3ReadOnlyPolicy.json
|
||||
|
||||
@@ -186,7 +186,8 @@ weed s3 -filer=prod-filer:8888 -port=833N -iam.config=/shared/sts_distributed.js
|
||||
1. **Identical Configuration Files**: All instances must use the exact same configuration file
|
||||
2. **Same Signing Keys**: All instances must have identical `signingKey` values
|
||||
3. **Same Issuer**: All instances must use the same `issuer` value
|
||||
4. **Shared Session Storage**: Use `"sessionStoreType": "filer"` for distributed sessions
|
||||
|
||||
**Note**: STS now uses stateless JWT tokens, eliminating the need for shared session storage.
|
||||
|
||||
### High Availability Setup
|
||||
|
||||
@@ -336,11 +337,6 @@ User Request → Load Balancer → Any S3 Gateway Instance
|
||||
"maxSessionLength": 43200000000000,
|
||||
"issuer": "seaweedfs-prod-sts",
|
||||
"signingKey": "cHJvZC1zaWduaW5nLWtleS0zMi1jaGFyYWN0ZXJzLWxvbmctcmFuZG9t",
|
||||
"sessionStoreType": "filer",
|
||||
"sessionStoreConfig": {
|
||||
"filerAddress": "prod-filer.company.com:8888",
|
||||
"basePath": "/seaweedfs/iam/sessions"
|
||||
},
|
||||
"providers": [
|
||||
{
|
||||
"name": "corporate-sso",
|
||||
|
||||
@@ -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")
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"fmt"
|
||||
"net"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strings"
|
||||
)
|
||||
|
||||
@@ -469,20 +470,21 @@ func (e *PolicyEngine) EvaluateStringCondition(block map[string]interface{}, eva
|
||||
expectedStrings = []string{fmt.Sprintf("%v", v)}
|
||||
}
|
||||
|
||||
// Evaluate the condition
|
||||
// Evaluate the condition using AWS IAM-compliant matching
|
||||
conditionMet := false
|
||||
for _, expected := range expectedStrings {
|
||||
for _, contextValue := range contextStrings {
|
||||
if useWildcard {
|
||||
// Use wildcard matching for StringLike conditions
|
||||
matched, err := filepath.Match(expected, contextValue)
|
||||
if err == nil && matched {
|
||||
// Use AWS IAM-compliant wildcard matching for StringLike conditions
|
||||
// This handles case-insensitivity and policy variables
|
||||
if awsIAMMatch(expected, contextValue, evalCtx) {
|
||||
conditionMet = true
|
||||
break
|
||||
}
|
||||
} else {
|
||||
// Exact string matching for StringEquals/StringNotEquals
|
||||
if expected == contextValue {
|
||||
// For StringEquals/StringNotEquals, also support policy variables but be case-sensitive
|
||||
expandedExpected := expandPolicyVariables(expected, evalCtx)
|
||||
if expandedExpected == contextValue {
|
||||
conditionMet = true
|
||||
break
|
||||
}
|
||||
@@ -582,7 +584,6 @@ func validateStatementWithType(statement *Statement, policyType string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
// matchResource checks if a resource pattern matches a requested resource
|
||||
// Uses hybrid approach: simple suffix wildcards for compatibility, filepath.Match for complex patterns
|
||||
func matchResource(pattern, resource string) bool {
|
||||
@@ -606,6 +607,83 @@ func matchResource(pattern, resource string) bool {
|
||||
return matched
|
||||
}
|
||||
|
||||
// awsIAMMatch performs AWS IAM-compliant pattern matching with case-insensitivity and policy variable support
|
||||
func awsIAMMatch(pattern, value string, evalCtx *EvaluationContext) bool {
|
||||
// Step 1: Substitute policy variables (e.g., ${aws:username}, ${saml:username})
|
||||
expandedPattern := expandPolicyVariables(pattern, evalCtx)
|
||||
|
||||
// Step 2: Handle special patterns
|
||||
if expandedPattern == "*" {
|
||||
return true // Universal wildcard
|
||||
}
|
||||
|
||||
// Step 3: Case-insensitive exact match
|
||||
if strings.EqualFold(expandedPattern, value) {
|
||||
return true
|
||||
}
|
||||
|
||||
// Step 4: Handle AWS-style wildcards (case-insensitive)
|
||||
if strings.Contains(expandedPattern, "*") || strings.Contains(expandedPattern, "?") {
|
||||
return awsWildcardMatch(expandedPattern, value)
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// expandPolicyVariables substitutes AWS policy variables in the pattern
|
||||
func expandPolicyVariables(pattern string, evalCtx *EvaluationContext) string {
|
||||
if evalCtx == nil || evalCtx.RequestContext == nil {
|
||||
return pattern
|
||||
}
|
||||
|
||||
expanded := pattern
|
||||
|
||||
// Common AWS policy variables that might be used in SeaweedFS
|
||||
variableMap := map[string]string{
|
||||
"${aws:username}": getContextValue(evalCtx, "aws:username", ""),
|
||||
"${saml:username}": getContextValue(evalCtx, "saml:username", ""),
|
||||
"${oidc:sub}": getContextValue(evalCtx, "oidc:sub", ""),
|
||||
"${aws:userid}": getContextValue(evalCtx, "aws:userid", ""),
|
||||
"${aws:principaltype}": getContextValue(evalCtx, "aws:principaltype", ""),
|
||||
}
|
||||
|
||||
for variable, value := range variableMap {
|
||||
if value != "" {
|
||||
expanded = strings.ReplaceAll(expanded, variable, value)
|
||||
}
|
||||
}
|
||||
|
||||
return expanded
|
||||
}
|
||||
|
||||
// getContextValue safely gets a value from the evaluation context
|
||||
func getContextValue(evalCtx *EvaluationContext, key, defaultValue string) string {
|
||||
if value, exists := evalCtx.RequestContext[key]; exists {
|
||||
if str, ok := value.(string); ok {
|
||||
return str
|
||||
}
|
||||
}
|
||||
return defaultValue
|
||||
}
|
||||
|
||||
// awsWildcardMatch performs case-insensitive wildcard matching like AWS IAM
|
||||
func awsWildcardMatch(pattern, value string) bool {
|
||||
// Convert pattern to regex with case-insensitive matching
|
||||
// AWS uses * for any sequence and ? for any single character
|
||||
regexPattern := strings.ReplaceAll(pattern, "*", ".*")
|
||||
regexPattern = strings.ReplaceAll(regexPattern, "?", ".")
|
||||
regexPattern = "^" + regexPattern + "$"
|
||||
|
||||
// Compile with case-insensitive flag
|
||||
regex, err := regexp.Compile("(?i)" + regexPattern)
|
||||
if err != nil {
|
||||
// Fallback to simple case-insensitive comparison if regex fails
|
||||
return strings.EqualFold(pattern, value)
|
||||
}
|
||||
|
||||
return regex.MatchString(value)
|
||||
}
|
||||
|
||||
// matchAction checks if an action pattern matches a requested action
|
||||
// Uses hybrid approach: simple suffix wildcards for compatibility, filepath.Match for complex patterns
|
||||
func matchAction(pattern, action string) bool {
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
@@ -17,6 +17,14 @@ const (
|
||||
ACTION_GET_BUCKET_OBJECT_LOCK_CONFIG = "GetBucketObjectLockConfiguration"
|
||||
ACTION_PUT_BUCKET_OBJECT_LOCK_CONFIG = "PutBucketObjectLockConfiguration"
|
||||
|
||||
// Granular multipart upload actions for fine-grained IAM policies
|
||||
ACTION_CREATE_MULTIPART_UPLOAD = "s3:CreateMultipartUpload"
|
||||
ACTION_UPLOAD_PART = "s3:UploadPart"
|
||||
ACTION_COMPLETE_MULTIPART = "s3:CompleteMultipartUpload"
|
||||
ACTION_ABORT_MULTIPART = "s3:AbortMultipartUpload"
|
||||
ACTION_LIST_MULTIPART_UPLOADS = "s3:ListMultipartUploads"
|
||||
ACTION_LIST_PARTS = "s3:ListParts"
|
||||
|
||||
SeaweedStorageDestinationHeader = "x-seaweedfs-destination"
|
||||
MultipartUploadsFolder = ".uploads"
|
||||
FolderMimeType = "httpd/unix-directory"
|
||||
|
||||
@@ -77,10 +77,16 @@ func (s3iam *S3IAMIntegration) AuthenticateJWT(ctx context.Context, r *http.Requ
|
||||
return nil, s3err.ErrAccessDenied
|
||||
}
|
||||
|
||||
// Check if this is an STS session token (has "role" claim)
|
||||
roleName, ok := tokenClaims["role"].(string)
|
||||
if !ok || roleName == "" {
|
||||
glog.V(0).Infof("🔐 AuthenticateJWT: No 'role' claim found, treating as OIDC token")
|
||||
// Determine token type by issuer claim (more robust than checking role claim)
|
||||
issuer, issuerOk := tokenClaims["iss"].(string)
|
||||
if !issuerOk {
|
||||
glog.V(3).Infof("Token missing issuer claim - invalid JWT")
|
||||
return nil, s3err.ErrAccessDenied
|
||||
}
|
||||
|
||||
// Check if this is an STS-issued token by examining the issuer
|
||||
if !s3iam.isSTSIssuer(issuer) {
|
||||
glog.V(0).Infof("🔐 AuthenticateJWT: External issuer (%s), treating as OIDC token", issuer)
|
||||
|
||||
// Not an STS session token, try to validate as OIDC token with timeout
|
||||
// Create a context with a reasonable timeout to prevent hanging
|
||||
@@ -124,6 +130,16 @@ func (s3iam *S3IAMIntegration) AuthenticateJWT(ctx context.Context, r *http.Requ
|
||||
}, s3err.ErrNone
|
||||
}
|
||||
|
||||
// This is an STS-issued token - extract STS session information
|
||||
glog.V(0).Infof("🔐 AuthenticateJWT: STS-issued token from issuer: %s", issuer)
|
||||
|
||||
// Extract role claim from STS token
|
||||
roleName, roleOk := tokenClaims["role"].(string)
|
||||
if !roleOk || roleName == "" {
|
||||
glog.V(3).Infof("STS token missing role claim")
|
||||
return nil, s3err.ErrAccessDenied
|
||||
}
|
||||
|
||||
sessionName, ok := tokenClaims["snam"].(string)
|
||||
if !ok || sessionName == "" {
|
||||
sessionName = "jwt-session" // Default fallback
|
||||
@@ -514,3 +530,33 @@ func getProviderNames(providers map[string]providers.IdentityProvider) []string
|
||||
}
|
||||
return names
|
||||
}
|
||||
|
||||
// isSTSIssuer determines if an issuer belongs to the STS service
|
||||
// This provides more robust token type detection than checking for role claims
|
||||
func (s3iam *S3IAMIntegration) isSTSIssuer(issuer string) bool {
|
||||
if s3iam.stsService == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
// Get the STS configuration to check the issuer
|
||||
// For now, we'll use a simple heuristic - STS issuers typically contain "sts" or are internal
|
||||
// In a full implementation, this could check against configured STS issuer values
|
||||
|
||||
// Check if issuer contains common STS patterns
|
||||
stsPatterns := []string{
|
||||
"sts",
|
||||
"seaweedfs-sts",
|
||||
"seaweed",
|
||||
"localhost", // For development/testing
|
||||
}
|
||||
|
||||
lowerIssuer := strings.ToLower(issuer)
|
||||
for _, pattern := range stsPatterns {
|
||||
if strings.Contains(lowerIssuer, pattern) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
// If no pattern matches, assume it's an external OIDC issuer
|
||||
return false
|
||||
}
|
||||
@@ -272,21 +272,22 @@ func (s3a *S3ApiServer) UploadPartWithIAM(w http.ResponseWriter, r *http.Request
|
||||
|
||||
// Helper functions
|
||||
|
||||
// determineMultipartS3Action maps multipart operations to S3 actions
|
||||
// determineMultipartS3Action maps multipart operations to granular S3 actions
|
||||
// This enables fine-grained IAM policies for multipart upload operations
|
||||
func determineMultipartS3Action(operation MultipartOperation) Action {
|
||||
switch operation {
|
||||
case MultipartOpInitiate:
|
||||
return s3_constants.ACTION_WRITE // s3:CreateMultipartUpload maps to WRITE
|
||||
return s3_constants.ACTION_CREATE_MULTIPART_UPLOAD
|
||||
case MultipartOpUploadPart:
|
||||
return s3_constants.ACTION_WRITE // s3:UploadPart maps to WRITE
|
||||
return s3_constants.ACTION_UPLOAD_PART
|
||||
case MultipartOpComplete:
|
||||
return s3_constants.ACTION_WRITE // s3:CompleteMultipartUpload maps to WRITE
|
||||
return s3_constants.ACTION_COMPLETE_MULTIPART
|
||||
case MultipartOpAbort:
|
||||
return s3_constants.ACTION_WRITE // s3:AbortMultipartUpload maps to WRITE
|
||||
return s3_constants.ACTION_ABORT_MULTIPART
|
||||
case MultipartOpList:
|
||||
return s3_constants.ACTION_LIST // s3:ListMultipartUploads maps to LIST
|
||||
return s3_constants.ACTION_LIST_MULTIPART_UPLOADS
|
||||
case MultipartOpListParts:
|
||||
return s3_constants.ACTION_LIST // s3:ListParts maps to LIST
|
||||
return s3_constants.ACTION_LIST_PARTS
|
||||
default:
|
||||
return s3_constants.ACTION_READ // Default fallback
|
||||
}
|
||||
|
||||
@@ -260,12 +260,12 @@ func TestMultipartS3ActionMapping(t *testing.T) {
|
||||
operation MultipartOperation
|
||||
expectedAction Action
|
||||
}{
|
||||
{MultipartOpInitiate, s3_constants.ACTION_WRITE},
|
||||
{MultipartOpUploadPart, s3_constants.ACTION_WRITE},
|
||||
{MultipartOpComplete, s3_constants.ACTION_WRITE},
|
||||
{MultipartOpAbort, s3_constants.ACTION_WRITE},
|
||||
{MultipartOpList, s3_constants.ACTION_LIST},
|
||||
{MultipartOpListParts, s3_constants.ACTION_LIST},
|
||||
{MultipartOpInitiate, s3_constants.ACTION_CREATE_MULTIPART_UPLOAD},
|
||||
{MultipartOpUploadPart, s3_constants.ACTION_UPLOAD_PART},
|
||||
{MultipartOpComplete, s3_constants.ACTION_COMPLETE_MULTIPART},
|
||||
{MultipartOpAbort, s3_constants.ACTION_ABORT_MULTIPART},
|
||||
{MultipartOpList, s3_constants.ACTION_LIST_MULTIPART_UPLOADS},
|
||||
{MultipartOpListParts, s3_constants.ACTION_LIST_PARTS},
|
||||
{MultipartOperation("unknown"), s3_constants.ACTION_READ}, // Default fallback
|
||||
}
|
||||
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
Reference in new issue
Block a user