S3: Enforce bucket policy (#7471)
* evaluate policies during authorization * cache bucket policy * refactor * matching with regex special characters * Case Sensitivity, pattern cache, Dead Code Removal * Fixed Typo, Restored []string Case, Added Cache Size Limit * hook up with policy engine * remove old implementation * action mapping * validate * if not specified, fall through to IAM checks * fmt * Fail-close on policy evaluation errors * Explicit `Allow` bypasses IAM checks * fix error message * arn:seaweed => arn:aws * remove legacy support * fix tests * Clean up bucket policy after this test * fix for tests * address comments * security fixes * fix tests * temp comment out
This commit is contained in:
@@ -34,23 +34,23 @@ func TestFullOIDCWorkflow(t *testing.T) {
|
||||
}{
|
||||
{
|
||||
name: "successful role assumption with policy validation",
|
||||
roleArn: "arn:seaweed:iam::role/S3ReadOnlyRole",
|
||||
roleArn: "arn:aws:iam::role/S3ReadOnlyRole",
|
||||
sessionName: "oidc-session",
|
||||
webToken: validJWTToken,
|
||||
expectedAllow: true,
|
||||
testAction: "s3:GetObject",
|
||||
testResource: "arn:seaweed:s3:::test-bucket/file.txt",
|
||||
testResource: "arn:aws:s3:::test-bucket/file.txt",
|
||||
},
|
||||
{
|
||||
name: "role assumption denied by trust policy",
|
||||
roleArn: "arn:seaweed:iam::role/RestrictedRole",
|
||||
roleArn: "arn:aws:iam::role/RestrictedRole",
|
||||
sessionName: "oidc-session",
|
||||
webToken: validJWTToken,
|
||||
expectedAllow: false,
|
||||
},
|
||||
{
|
||||
name: "invalid token rejected",
|
||||
roleArn: "arn:seaweed:iam::role/S3ReadOnlyRole",
|
||||
roleArn: "arn:aws:iam::role/S3ReadOnlyRole",
|
||||
sessionName: "oidc-session",
|
||||
webToken: invalidJWTToken,
|
||||
expectedAllow: false,
|
||||
@@ -113,17 +113,17 @@ func TestFullLDAPWorkflow(t *testing.T) {
|
||||
}{
|
||||
{
|
||||
name: "successful LDAP role assumption",
|
||||
roleArn: "arn:seaweed:iam::role/LDAPUserRole",
|
||||
roleArn: "arn:aws:iam::role/LDAPUserRole",
|
||||
sessionName: "ldap-session",
|
||||
username: "testuser",
|
||||
password: "testpass",
|
||||
expectedAllow: true,
|
||||
testAction: "filer:CreateEntry",
|
||||
testResource: "arn:seaweed:filer::path/user-docs/*",
|
||||
testResource: "arn:aws:filer::path/user-docs/*",
|
||||
},
|
||||
{
|
||||
name: "invalid LDAP credentials",
|
||||
roleArn: "arn:seaweed:iam::role/LDAPUserRole",
|
||||
roleArn: "arn:aws:iam::role/LDAPUserRole",
|
||||
sessionName: "ldap-session",
|
||||
username: "testuser",
|
||||
password: "wrongpass",
|
||||
@@ -181,7 +181,7 @@ func TestPolicyEnforcement(t *testing.T) {
|
||||
// Create a session for testing
|
||||
ctx := context.Background()
|
||||
assumeRequest := &sts.AssumeRoleWithWebIdentityRequest{
|
||||
RoleArn: "arn:seaweed:iam::role/S3ReadOnlyRole",
|
||||
RoleArn: "arn:aws:iam::role/S3ReadOnlyRole",
|
||||
WebIdentityToken: validJWTToken,
|
||||
RoleSessionName: "policy-test-session",
|
||||
}
|
||||
@@ -202,35 +202,35 @@ func TestPolicyEnforcement(t *testing.T) {
|
||||
{
|
||||
name: "allow read access",
|
||||
action: "s3:GetObject",
|
||||
resource: "arn:seaweed:s3:::test-bucket/file.txt",
|
||||
resource: "arn:aws:s3:::test-bucket/file.txt",
|
||||
shouldAllow: true,
|
||||
reason: "S3ReadOnlyRole should allow GetObject",
|
||||
},
|
||||
{
|
||||
name: "allow list bucket",
|
||||
action: "s3:ListBucket",
|
||||
resource: "arn:seaweed:s3:::test-bucket",
|
||||
resource: "arn:aws:s3:::test-bucket",
|
||||
shouldAllow: true,
|
||||
reason: "S3ReadOnlyRole should allow ListBucket",
|
||||
},
|
||||
{
|
||||
name: "deny write access",
|
||||
action: "s3:PutObject",
|
||||
resource: "arn:seaweed:s3:::test-bucket/newfile.txt",
|
||||
resource: "arn:aws:s3:::test-bucket/newfile.txt",
|
||||
shouldAllow: false,
|
||||
reason: "S3ReadOnlyRole should deny write operations",
|
||||
},
|
||||
{
|
||||
name: "deny delete access",
|
||||
action: "s3:DeleteObject",
|
||||
resource: "arn:seaweed:s3:::test-bucket/file.txt",
|
||||
resource: "arn:aws:s3:::test-bucket/file.txt",
|
||||
shouldAllow: false,
|
||||
reason: "S3ReadOnlyRole should deny delete operations",
|
||||
},
|
||||
{
|
||||
name: "deny filer access",
|
||||
action: "filer:CreateEntry",
|
||||
resource: "arn:seaweed:filer::path/test",
|
||||
resource: "arn:aws:filer::path/test",
|
||||
shouldAllow: false,
|
||||
reason: "S3ReadOnlyRole should not allow filer operations",
|
||||
},
|
||||
@@ -261,7 +261,7 @@ func TestSessionExpiration(t *testing.T) {
|
||||
|
||||
// Create a short-lived session
|
||||
assumeRequest := &sts.AssumeRoleWithWebIdentityRequest{
|
||||
RoleArn: "arn:seaweed:iam::role/S3ReadOnlyRole",
|
||||
RoleArn: "arn:aws:iam::role/S3ReadOnlyRole",
|
||||
WebIdentityToken: validJWTToken,
|
||||
RoleSessionName: "expiration-test",
|
||||
DurationSeconds: int64Ptr(900), // 15 minutes
|
||||
@@ -276,7 +276,7 @@ func TestSessionExpiration(t *testing.T) {
|
||||
allowed, err := iamManager.IsActionAllowed(ctx, &ActionRequest{
|
||||
Principal: response.AssumedRoleUser.Arn,
|
||||
Action: "s3:GetObject",
|
||||
Resource: "arn:seaweed:s3:::test-bucket/file.txt",
|
||||
Resource: "arn:aws:s3:::test-bucket/file.txt",
|
||||
SessionToken: sessionToken,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
@@ -296,7 +296,7 @@ func TestSessionExpiration(t *testing.T) {
|
||||
allowed, err = iamManager.IsActionAllowed(ctx, &ActionRequest{
|
||||
Principal: response.AssumedRoleUser.Arn,
|
||||
Action: "s3:GetObject",
|
||||
Resource: "arn:seaweed:s3:::test-bucket/file.txt",
|
||||
Resource: "arn:aws:s3:::test-bucket/file.txt",
|
||||
SessionToken: sessionToken,
|
||||
})
|
||||
require.NoError(t, err, "Session should still be valid in stateless system")
|
||||
@@ -318,7 +318,7 @@ func TestTrustPolicyValidation(t *testing.T) {
|
||||
}{
|
||||
{
|
||||
name: "OIDC user allowed by trust policy",
|
||||
roleArn: "arn:seaweed:iam::role/S3ReadOnlyRole",
|
||||
roleArn: "arn:aws:iam::role/S3ReadOnlyRole",
|
||||
provider: "oidc",
|
||||
userID: "test-user-id",
|
||||
shouldAllow: true,
|
||||
@@ -326,7 +326,7 @@ func TestTrustPolicyValidation(t *testing.T) {
|
||||
},
|
||||
{
|
||||
name: "LDAP user allowed by different role",
|
||||
roleArn: "arn:seaweed:iam::role/LDAPUserRole",
|
||||
roleArn: "arn:aws:iam::role/LDAPUserRole",
|
||||
provider: "ldap",
|
||||
userID: "testuser",
|
||||
shouldAllow: true,
|
||||
@@ -334,7 +334,7 @@ func TestTrustPolicyValidation(t *testing.T) {
|
||||
},
|
||||
{
|
||||
name: "Wrong provider for role",
|
||||
roleArn: "arn:seaweed:iam::role/S3ReadOnlyRole",
|
||||
roleArn: "arn:aws:iam::role/S3ReadOnlyRole",
|
||||
provider: "ldap",
|
||||
userID: "testuser",
|
||||
shouldAllow: false,
|
||||
@@ -442,8 +442,8 @@ func setupTestPoliciesAndRoles(t *testing.T, manager *IAMManager) {
|
||||
Effect: "Allow",
|
||||
Action: []string{"s3:GetObject", "s3:ListBucket"},
|
||||
Resource: []string{
|
||||
"arn:seaweed:s3:::*",
|
||||
"arn:seaweed:s3:::*/*",
|
||||
"arn:aws:s3:::*",
|
||||
"arn:aws:s3:::*/*",
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -461,7 +461,7 @@ func setupTestPoliciesAndRoles(t *testing.T, manager *IAMManager) {
|
||||
Effect: "Allow",
|
||||
Action: []string{"filer:*"},
|
||||
Resource: []string{
|
||||
"arn:seaweed:filer::path/user-docs/*",
|
||||
"arn:aws:filer::path/user-docs/*",
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -213,7 +213,7 @@ func (m *IAMManager) CreateRole(ctx context.Context, filerAddress string, roleNa
|
||||
|
||||
// Set role ARN if not provided
|
||||
if roleDef.RoleArn == "" {
|
||||
roleDef.RoleArn = fmt.Sprintf("arn:seaweed:iam::role/%s", roleName)
|
||||
roleDef.RoleArn = fmt.Sprintf("arn:aws:iam::role/%s", roleName)
|
||||
}
|
||||
|
||||
// Validate trust policy
|
||||
|
||||
@@ -18,7 +18,7 @@ func TestMemoryRoleStore(t *testing.T) {
|
||||
// Test storing a role
|
||||
roleDef := &RoleDefinition{
|
||||
RoleName: "TestRole",
|
||||
RoleArn: "arn:seaweed:iam::role/TestRole",
|
||||
RoleArn: "arn:aws:iam::role/TestRole",
|
||||
Description: "Test role for unit testing",
|
||||
AttachedPolicies: []string{"TestPolicy"},
|
||||
TrustPolicy: &policy.PolicyDocument{
|
||||
@@ -42,7 +42,7 @@ func TestMemoryRoleStore(t *testing.T) {
|
||||
retrievedRole, err := store.GetRole(ctx, "", "TestRole")
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "TestRole", retrievedRole.RoleName)
|
||||
assert.Equal(t, "arn:seaweed:iam::role/TestRole", retrievedRole.RoleArn)
|
||||
assert.Equal(t, "arn:aws:iam::role/TestRole", retrievedRole.RoleArn)
|
||||
assert.Equal(t, "Test role for unit testing", retrievedRole.Description)
|
||||
assert.Equal(t, []string{"TestPolicy"}, retrievedRole.AttachedPolicies)
|
||||
|
||||
@@ -112,7 +112,7 @@ func TestDistributedIAMManagerWithRoleStore(t *testing.T) {
|
||||
// Test creating a role
|
||||
roleDef := &RoleDefinition{
|
||||
RoleName: "DistributedTestRole",
|
||||
RoleArn: "arn:seaweed:iam::role/DistributedTestRole",
|
||||
RoleArn: "arn:aws:iam::role/DistributedTestRole",
|
||||
Description: "Test role for distributed IAM",
|
||||
AttachedPolicies: []string{"S3ReadOnlyPolicy"},
|
||||
}
|
||||
|
||||
@@ -210,15 +210,15 @@ func TestOIDCProviderAuthentication(t *testing.T) {
|
||||
{
|
||||
Claim: "email",
|
||||
Value: "*@example.com",
|
||||
Role: "arn:seaweed:iam::role/UserRole",
|
||||
Role: "arn:aws:iam::role/UserRole",
|
||||
},
|
||||
{
|
||||
Claim: "groups",
|
||||
Value: "admins",
|
||||
Role: "arn:seaweed:iam::role/AdminRole",
|
||||
Role: "arn:aws:iam::role/AdminRole",
|
||||
},
|
||||
},
|
||||
DefaultRole: "arn:seaweed:iam::role/GuestRole",
|
||||
DefaultRole: "arn:aws:iam::role/GuestRole",
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@@ -95,7 +95,7 @@ type EvaluationContext struct {
|
||||
// Action being requested (e.g., "s3:GetObject")
|
||||
Action string `json:"action"`
|
||||
|
||||
// Resource being accessed (e.g., "arn:seaweed:s3:::bucket/key")
|
||||
// Resource being accessed (e.g., "arn:aws:s3:::bucket/key")
|
||||
Resource string `json:"resource"`
|
||||
|
||||
// RequestContext contains additional request information
|
||||
|
||||
@@ -47,13 +47,13 @@ func TestDistributedPolicyEngine(t *testing.T) {
|
||||
Sid: "AllowS3Read",
|
||||
Effect: "Allow",
|
||||
Action: []string{"s3:GetObject", "s3:ListBucket"},
|
||||
Resource: []string{"arn:seaweed:s3:::test-bucket/*", "arn:seaweed:s3:::test-bucket"},
|
||||
Resource: []string{"arn:aws:s3:::test-bucket/*", "arn:aws:s3:::test-bucket"},
|
||||
},
|
||||
{
|
||||
Sid: "DenyS3Write",
|
||||
Effect: "Deny",
|
||||
Action: []string{"s3:PutObject", "s3:DeleteObject"},
|
||||
Resource: []string{"arn:seaweed:s3:::test-bucket/*"},
|
||||
Resource: []string{"arn:aws:s3:::test-bucket/*"},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -83,9 +83,9 @@ func TestDistributedPolicyEngine(t *testing.T) {
|
||||
t.Run("evaluation_consistency", func(t *testing.T) {
|
||||
// Create evaluation context
|
||||
evalCtx := &EvaluationContext{
|
||||
Principal: "arn:seaweed:sts::assumed-role/TestRole/session",
|
||||
Principal: "arn:aws:sts::assumed-role/TestRole/session",
|
||||
Action: "s3:GetObject",
|
||||
Resource: "arn:seaweed:s3:::test-bucket/file.txt",
|
||||
Resource: "arn:aws:s3:::test-bucket/file.txt",
|
||||
RequestContext: map[string]interface{}{
|
||||
"sourceIp": "192.168.1.100",
|
||||
},
|
||||
@@ -118,9 +118,9 @@ func TestDistributedPolicyEngine(t *testing.T) {
|
||||
// Test explicit deny precedence
|
||||
t.Run("deny_precedence_consistency", func(t *testing.T) {
|
||||
evalCtx := &EvaluationContext{
|
||||
Principal: "arn:seaweed:sts::assumed-role/TestRole/session",
|
||||
Principal: "arn:aws:sts::assumed-role/TestRole/session",
|
||||
Action: "s3:PutObject",
|
||||
Resource: "arn:seaweed:s3:::test-bucket/newfile.txt",
|
||||
Resource: "arn:aws:s3:::test-bucket/newfile.txt",
|
||||
}
|
||||
|
||||
// All instances should consistently apply deny precedence
|
||||
@@ -146,9 +146,9 @@ func TestDistributedPolicyEngine(t *testing.T) {
|
||||
// Test default effect consistency
|
||||
t.Run("default_effect_consistency", func(t *testing.T) {
|
||||
evalCtx := &EvaluationContext{
|
||||
Principal: "arn:seaweed:sts::assumed-role/TestRole/session",
|
||||
Principal: "arn:aws:sts::assumed-role/TestRole/session",
|
||||
Action: "filer:CreateEntry", // Action not covered by any policy
|
||||
Resource: "arn:seaweed:filer::path/test",
|
||||
Resource: "arn:aws:filer::path/test",
|
||||
}
|
||||
|
||||
result1, err1 := instance1.Evaluate(ctx, "", evalCtx, []string{"TestPolicy"})
|
||||
@@ -196,9 +196,9 @@ func TestPolicyEngineConfigurationConsistency(t *testing.T) {
|
||||
|
||||
// Test with an action not covered by any policy
|
||||
evalCtx := &EvaluationContext{
|
||||
Principal: "arn:seaweed:sts::assumed-role/TestRole/session",
|
||||
Principal: "arn:aws:sts::assumed-role/TestRole/session",
|
||||
Action: "uncovered:action",
|
||||
Resource: "arn:seaweed:test:::resource",
|
||||
Resource: "arn:aws:test:::resource",
|
||||
}
|
||||
|
||||
result1, _ := instance1.Evaluate(context.Background(), "", evalCtx, []string{})
|
||||
@@ -277,9 +277,9 @@ func TestPolicyStoreDistributed(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
|
||||
evalCtx := &EvaluationContext{
|
||||
Principal: "arn:seaweed:sts::assumed-role/TestRole/session",
|
||||
Principal: "arn:aws:sts::assumed-role/TestRole/session",
|
||||
Action: "s3:GetObject",
|
||||
Resource: "arn:seaweed:s3:::bucket/key",
|
||||
Resource: "arn:aws:s3:::bucket/key",
|
||||
}
|
||||
|
||||
// Evaluate with non-existent policies
|
||||
@@ -350,7 +350,7 @@ func TestPolicyEvaluationPerformance(t *testing.T) {
|
||||
Sid: fmt.Sprintf("Statement%d", i),
|
||||
Effect: "Allow",
|
||||
Action: []string{"s3:GetObject", "s3:ListBucket"},
|
||||
Resource: []string{fmt.Sprintf("arn:seaweed:s3:::bucket%d/*", i)},
|
||||
Resource: []string{fmt.Sprintf("arn:aws:s3:::bucket%d/*", i)},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -361,9 +361,9 @@ func TestPolicyEvaluationPerformance(t *testing.T) {
|
||||
|
||||
// Test evaluation performance
|
||||
evalCtx := &EvaluationContext{
|
||||
Principal: "arn:seaweed:sts::assumed-role/TestRole/session",
|
||||
Principal: "arn:aws:sts::assumed-role/TestRole/session",
|
||||
Action: "s3:GetObject",
|
||||
Resource: "arn:seaweed:s3:::bucket5/file.txt",
|
||||
Resource: "arn:aws:s3:::bucket5/file.txt",
|
||||
}
|
||||
|
||||
policyNames := make([]string, 10)
|
||||
|
||||
@@ -71,7 +71,7 @@ func TestPolicyDocumentValidation(t *testing.T) {
|
||||
Sid: "AllowS3Read",
|
||||
Effect: "Allow",
|
||||
Action: []string{"s3:GetObject", "s3:ListBucket"},
|
||||
Resource: []string{"arn:seaweed:s3:::mybucket/*"},
|
||||
Resource: []string{"arn:aws:s3:::mybucket/*"},
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -84,7 +84,7 @@ func TestPolicyDocumentValidation(t *testing.T) {
|
||||
{
|
||||
Effect: "Allow",
|
||||
Action: []string{"s3:GetObject"},
|
||||
Resource: []string{"arn:seaweed:s3:::mybucket/*"},
|
||||
Resource: []string{"arn:aws:s3:::mybucket/*"},
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -108,7 +108,7 @@ func TestPolicyDocumentValidation(t *testing.T) {
|
||||
{
|
||||
Effect: "Maybe",
|
||||
Action: []string{"s3:GetObject"},
|
||||
Resource: []string{"arn:seaweed:s3:::mybucket/*"},
|
||||
Resource: []string{"arn:aws:s3:::mybucket/*"},
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -146,8 +146,8 @@ func TestPolicyEvaluation(t *testing.T) {
|
||||
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
|
||||
"arn:aws:s3:::public-bucket/*", // For object operations
|
||||
"arn:aws:s3:::public-bucket", // For bucket operations
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -163,7 +163,7 @@ func TestPolicyEvaluation(t *testing.T) {
|
||||
Sid: "DenyS3Delete",
|
||||
Effect: "Deny",
|
||||
Action: []string{"s3:DeleteObject"},
|
||||
Resource: []string{"arn:seaweed:s3:::*"},
|
||||
Resource: []string{"arn:aws:s3:::*"},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -182,7 +182,7 @@ func TestPolicyEvaluation(t *testing.T) {
|
||||
context: &EvaluationContext{
|
||||
Principal: "user:alice",
|
||||
Action: "s3:GetObject",
|
||||
Resource: "arn:seaweed:s3:::public-bucket/file.txt",
|
||||
Resource: "arn:aws:s3:::public-bucket/file.txt",
|
||||
RequestContext: map[string]interface{}{
|
||||
"sourceIP": "192.168.1.100",
|
||||
},
|
||||
@@ -195,7 +195,7 @@ func TestPolicyEvaluation(t *testing.T) {
|
||||
context: &EvaluationContext{
|
||||
Principal: "user:alice",
|
||||
Action: "s3:DeleteObject",
|
||||
Resource: "arn:seaweed:s3:::public-bucket/file.txt",
|
||||
Resource: "arn:aws:s3:::public-bucket/file.txt",
|
||||
},
|
||||
policies: []string{"read-policy", "deny-policy"},
|
||||
want: EffectDeny,
|
||||
@@ -205,7 +205,7 @@ func TestPolicyEvaluation(t *testing.T) {
|
||||
context: &EvaluationContext{
|
||||
Principal: "user:alice",
|
||||
Action: "s3:PutObject",
|
||||
Resource: "arn:seaweed:s3:::public-bucket/file.txt",
|
||||
Resource: "arn:aws:s3:::public-bucket/file.txt",
|
||||
},
|
||||
policies: []string{"read-policy"},
|
||||
want: EffectDeny,
|
||||
@@ -215,7 +215,7 @@ func TestPolicyEvaluation(t *testing.T) {
|
||||
context: &EvaluationContext{
|
||||
Principal: "user:admin",
|
||||
Action: "s3:ListBucket",
|
||||
Resource: "arn:seaweed:s3:::public-bucket",
|
||||
Resource: "arn:aws:s3:::public-bucket",
|
||||
},
|
||||
policies: []string{"read-policy"},
|
||||
want: EffectAllow,
|
||||
@@ -249,7 +249,7 @@ func TestConditionEvaluation(t *testing.T) {
|
||||
Sid: "AllowFromOfficeIP",
|
||||
Effect: "Allow",
|
||||
Action: []string{"s3:*"},
|
||||
Resource: []string{"arn:seaweed:s3:::*"},
|
||||
Resource: []string{"arn:aws:s3:::*"},
|
||||
Condition: map[string]map[string]interface{}{
|
||||
"IpAddress": {
|
||||
"seaweed:SourceIP": []string{"192.168.1.0/24", "10.0.0.0/8"},
|
||||
@@ -272,7 +272,7 @@ func TestConditionEvaluation(t *testing.T) {
|
||||
context: &EvaluationContext{
|
||||
Principal: "user:alice",
|
||||
Action: "s3:GetObject",
|
||||
Resource: "arn:seaweed:s3:::mybucket/file.txt",
|
||||
Resource: "arn:aws:s3:::mybucket/file.txt",
|
||||
RequestContext: map[string]interface{}{
|
||||
"sourceIP": "192.168.1.100",
|
||||
},
|
||||
@@ -284,7 +284,7 @@ func TestConditionEvaluation(t *testing.T) {
|
||||
context: &EvaluationContext{
|
||||
Principal: "user:alice",
|
||||
Action: "s3:GetObject",
|
||||
Resource: "arn:seaweed:s3:::mybucket/file.txt",
|
||||
Resource: "arn:aws:s3:::mybucket/file.txt",
|
||||
RequestContext: map[string]interface{}{
|
||||
"sourceIP": "8.8.8.8",
|
||||
},
|
||||
@@ -296,7 +296,7 @@ func TestConditionEvaluation(t *testing.T) {
|
||||
context: &EvaluationContext{
|
||||
Principal: "user:alice",
|
||||
Action: "s3:PutObject",
|
||||
Resource: "arn:seaweed:s3:::mybucket/newfile.txt",
|
||||
Resource: "arn:aws:s3:::mybucket/newfile.txt",
|
||||
RequestContext: map[string]interface{}{
|
||||
"sourceIP": "10.1.2.3",
|
||||
},
|
||||
@@ -325,32 +325,32 @@ func TestResourceMatching(t *testing.T) {
|
||||
}{
|
||||
{
|
||||
name: "exact match",
|
||||
policyResource: "arn:seaweed:s3:::mybucket/file.txt",
|
||||
requestResource: "arn:seaweed:s3:::mybucket/file.txt",
|
||||
policyResource: "arn:aws:s3:::mybucket/file.txt",
|
||||
requestResource: "arn:aws:s3:::mybucket/file.txt",
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "wildcard match",
|
||||
policyResource: "arn:seaweed:s3:::mybucket/*",
|
||||
requestResource: "arn:seaweed:s3:::mybucket/folder/file.txt",
|
||||
policyResource: "arn:aws:s3:::mybucket/*",
|
||||
requestResource: "arn:aws:s3:::mybucket/folder/file.txt",
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "bucket wildcard",
|
||||
policyResource: "arn:seaweed:s3:::*",
|
||||
requestResource: "arn:seaweed:s3:::anybucket/file.txt",
|
||||
policyResource: "arn:aws:s3:::*",
|
||||
requestResource: "arn:aws:s3:::anybucket/file.txt",
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "no match different bucket",
|
||||
policyResource: "arn:seaweed:s3:::mybucket/*",
|
||||
requestResource: "arn:seaweed:s3:::otherbucket/file.txt",
|
||||
policyResource: "arn:aws:s3:::mybucket/*",
|
||||
requestResource: "arn:aws:s3:::otherbucket/file.txt",
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "prefix match",
|
||||
policyResource: "arn:seaweed:s3:::mybucket/documents/*",
|
||||
requestResource: "arn:seaweed:s3:::mybucket/documents/secret.txt",
|
||||
policyResource: "arn:aws:s3:::mybucket/documents/*",
|
||||
requestResource: "arn:aws:s3:::mybucket/documents/secret.txt",
|
||||
want: true,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -153,7 +153,7 @@ func TestCrossInstanceTokenUsage(t *testing.T) {
|
||||
mockToken := createMockJWT(t, "http://test-mock:9999", "test-user")
|
||||
|
||||
assumeRequest := &AssumeRoleWithWebIdentityRequest{
|
||||
RoleArn: "arn:seaweed:iam::role/CrossInstanceTestRole",
|
||||
RoleArn: "arn:aws:iam::role/CrossInstanceTestRole",
|
||||
WebIdentityToken: mockToken, // JWT token for mock provider
|
||||
RoleSessionName: "cross-instance-test-session",
|
||||
DurationSeconds: int64ToPtr(3600),
|
||||
@@ -198,7 +198,7 @@ func TestCrossInstanceTokenUsage(t *testing.T) {
|
||||
mockToken := createMockJWT(t, "http://test-mock:9999", "test-user")
|
||||
|
||||
assumeRequest := &AssumeRoleWithWebIdentityRequest{
|
||||
RoleArn: "arn:seaweed:iam::role/RevocationTestRole",
|
||||
RoleArn: "arn:aws:iam::role/RevocationTestRole",
|
||||
WebIdentityToken: mockToken,
|
||||
RoleSessionName: "revocation-test-session",
|
||||
}
|
||||
@@ -240,7 +240,7 @@ func TestCrossInstanceTokenUsage(t *testing.T) {
|
||||
|
||||
// Try to assume role with same token on different instances
|
||||
assumeRequest := &AssumeRoleWithWebIdentityRequest{
|
||||
RoleArn: "arn:seaweed:iam::role/ProviderTestRole",
|
||||
RoleArn: "arn:aws:iam::role/ProviderTestRole",
|
||||
WebIdentityToken: testToken,
|
||||
RoleSessionName: "provider-consistency-test",
|
||||
}
|
||||
@@ -452,7 +452,7 @@ func TestSTSRealWorldDistributedScenarios(t *testing.T) {
|
||||
mockToken := createMockJWT(t, "http://test-mock:9999", "production-user")
|
||||
|
||||
assumeRequest := &AssumeRoleWithWebIdentityRequest{
|
||||
RoleArn: "arn:seaweed:iam::role/ProductionS3User",
|
||||
RoleArn: "arn:aws:iam::role/ProductionS3User",
|
||||
WebIdentityToken: mockToken, // JWT token from mock provider
|
||||
RoleSessionName: "user-production-session",
|
||||
DurationSeconds: int64ToPtr(7200), // 2 hours
|
||||
@@ -470,7 +470,7 @@ func TestSTSRealWorldDistributedScenarios(t *testing.T) {
|
||||
sessionInfo2, err := gateway2.ValidateSessionToken(ctx, sessionToken)
|
||||
require.NoError(t, err, "Gateway 2 should validate session from Gateway 1")
|
||||
assert.Equal(t, "user-production-session", sessionInfo2.SessionName)
|
||||
assert.Equal(t, "arn:seaweed:iam::role/ProductionS3User", sessionInfo2.RoleArn)
|
||||
assert.Equal(t, "arn:aws:iam::role/ProductionS3User", sessionInfo2.RoleArn)
|
||||
|
||||
// Simulate S3 request validation on Gateway 3
|
||||
sessionInfo3, err := gateway3.ValidateSessionToken(ctx, sessionToken)
|
||||
|
||||
@@ -47,7 +47,7 @@ func TestAssumeRoleWithWebIdentity_SessionPolicy(t *testing.T) {
|
||||
testToken := createSessionPolicyTestJWT(t, "test-issuer", "test-user")
|
||||
|
||||
request := &AssumeRoleWithWebIdentityRequest{
|
||||
RoleArn: "arn:seaweed:iam::role/TestRole",
|
||||
RoleArn: "arn:aws:iam::role/TestRole",
|
||||
WebIdentityToken: testToken,
|
||||
RoleSessionName: "test-session",
|
||||
DurationSeconds: nil, // Use default
|
||||
@@ -69,7 +69,7 @@ func TestAssumeRoleWithWebIdentity_SessionPolicy(t *testing.T) {
|
||||
testToken := createSessionPolicyTestJWT(t, "test-issuer", "test-user")
|
||||
|
||||
request := &AssumeRoleWithWebIdentityRequest{
|
||||
RoleArn: "arn:seaweed:iam::role/TestRole",
|
||||
RoleArn: "arn:aws:iam::role/TestRole",
|
||||
WebIdentityToken: testToken,
|
||||
RoleSessionName: "test-session",
|
||||
DurationSeconds: nil, // Use default
|
||||
@@ -93,7 +93,7 @@ func TestAssumeRoleWithWebIdentity_SessionPolicy(t *testing.T) {
|
||||
testToken := createSessionPolicyTestJWT(t, "test-issuer", "test-user")
|
||||
|
||||
request := &AssumeRoleWithWebIdentityRequest{
|
||||
RoleArn: "arn:seaweed:iam::role/TestRole",
|
||||
RoleArn: "arn:aws:iam::role/TestRole",
|
||||
WebIdentityToken: testToken,
|
||||
RoleSessionName: "test-session",
|
||||
Policy: nil, // ← Explicitly nil
|
||||
@@ -113,7 +113,7 @@ func TestAssumeRoleWithWebIdentity_SessionPolicy(t *testing.T) {
|
||||
emptyPolicy := "" // Empty string, but still a non-nil pointer
|
||||
|
||||
request := &AssumeRoleWithWebIdentityRequest{
|
||||
RoleArn: "arn:seaweed:iam::role/TestRole",
|
||||
RoleArn: "arn:aws:iam::role/TestRole",
|
||||
WebIdentityToken: createSessionPolicyTestJWT(t, "test-issuer", "test-user"),
|
||||
RoleSessionName: "test-session",
|
||||
Policy: &emptyPolicy, // ← Non-nil pointer to empty string
|
||||
@@ -160,7 +160,7 @@ func TestAssumeRoleWithWebIdentity_SessionPolicy_ErrorMessage(t *testing.T) {
|
||||
testToken := createSessionPolicyTestJWT(t, "test-issuer", "test-user")
|
||||
|
||||
request := &AssumeRoleWithWebIdentityRequest{
|
||||
RoleArn: "arn:seaweed:iam::role/TestRole",
|
||||
RoleArn: "arn:aws:iam::role/TestRole",
|
||||
WebIdentityToken: testToken,
|
||||
RoleSessionName: "test-session-with-complex-policy",
|
||||
Policy: &complexPolicy,
|
||||
@@ -196,7 +196,7 @@ func TestAssumeRoleWithWebIdentity_SessionPolicy_EdgeCases(t *testing.T) {
|
||||
malformedPolicy := `{"Version": "2012-10-17", "Statement": [` // Incomplete JSON
|
||||
|
||||
request := &AssumeRoleWithWebIdentityRequest{
|
||||
RoleArn: "arn:seaweed:iam::role/TestRole",
|
||||
RoleArn: "arn:aws:iam::role/TestRole",
|
||||
WebIdentityToken: createSessionPolicyTestJWT(t, "test-issuer", "test-user"),
|
||||
RoleSessionName: "test-session",
|
||||
Policy: &malformedPolicy,
|
||||
@@ -215,7 +215,7 @@ func TestAssumeRoleWithWebIdentity_SessionPolicy_EdgeCases(t *testing.T) {
|
||||
whitespacePolicy := " \t\n " // Only whitespace
|
||||
|
||||
request := &AssumeRoleWithWebIdentityRequest{
|
||||
RoleArn: "arn:seaweed:iam::role/TestRole",
|
||||
RoleArn: "arn:aws:iam::role/TestRole",
|
||||
WebIdentityToken: createSessionPolicyTestJWT(t, "test-issuer", "test-user"),
|
||||
RoleSessionName: "test-session",
|
||||
Policy: &whitespacePolicy,
|
||||
@@ -260,7 +260,7 @@ func TestAssumeRoleWithCredentials_NoSessionPolicySupport(t *testing.T) {
|
||||
// This is the expected behavior since session policies are typically only
|
||||
// supported with web identity (OIDC/SAML) flows in AWS STS
|
||||
request := &AssumeRoleWithCredentialsRequest{
|
||||
RoleArn: "arn:seaweed:iam::role/TestRole",
|
||||
RoleArn: "arn:aws:iam::role/TestRole",
|
||||
Username: "testuser",
|
||||
Password: "testpass",
|
||||
RoleSessionName: "test-session",
|
||||
@@ -269,7 +269,7 @@ func TestAssumeRoleWithCredentials_NoSessionPolicySupport(t *testing.T) {
|
||||
|
||||
// The struct should compile and work without a Policy field
|
||||
assert.NotNil(t, request)
|
||||
assert.Equal(t, "arn:seaweed:iam::role/TestRole", request.RoleArn)
|
||||
assert.Equal(t, "arn:aws:iam::role/TestRole", request.RoleArn)
|
||||
assert.Equal(t, "testuser", request.Username)
|
||||
|
||||
// This documents that credential-based assume role does NOT support session policies
|
||||
|
||||
@@ -683,7 +683,7 @@ func (s *STSService) validateRoleAssumptionForWebIdentity(ctx context.Context, r
|
||||
}
|
||||
|
||||
// Basic role ARN format validation
|
||||
expectedPrefix := "arn:seaweed:iam::role/"
|
||||
expectedPrefix := "arn:aws:iam::role/"
|
||||
if len(roleArn) < len(expectedPrefix) || roleArn[:len(expectedPrefix)] != expectedPrefix {
|
||||
return fmt.Errorf("invalid role ARN format: got %s, expected format: %s*", roleArn, expectedPrefix)
|
||||
}
|
||||
@@ -720,7 +720,7 @@ func (s *STSService) validateRoleAssumptionForCredentials(ctx context.Context, r
|
||||
}
|
||||
|
||||
// Basic role ARN format validation
|
||||
expectedPrefix := "arn:seaweed:iam::role/"
|
||||
expectedPrefix := "arn:aws:iam::role/"
|
||||
if len(roleArn) < len(expectedPrefix) || roleArn[:len(expectedPrefix)] != expectedPrefix {
|
||||
return fmt.Errorf("invalid role ARN format: got %s, expected format: %s*", roleArn, expectedPrefix)
|
||||
}
|
||||
|
||||
@@ -95,7 +95,7 @@ func TestAssumeRoleWithWebIdentity(t *testing.T) {
|
||||
}{
|
||||
{
|
||||
name: "successful role assumption",
|
||||
roleArn: "arn:seaweed:iam::role/TestRole",
|
||||
roleArn: "arn:aws:iam::role/TestRole",
|
||||
webIdentityToken: createSTSTestJWT(t, "test-issuer", "test-user-id"),
|
||||
sessionName: "test-session",
|
||||
durationSeconds: nil, // Use default
|
||||
@@ -104,21 +104,21 @@ func TestAssumeRoleWithWebIdentity(t *testing.T) {
|
||||
},
|
||||
{
|
||||
name: "invalid web identity token",
|
||||
roleArn: "arn:seaweed:iam::role/TestRole",
|
||||
roleArn: "arn:aws:iam::role/TestRole",
|
||||
webIdentityToken: "invalid-token",
|
||||
sessionName: "test-session",
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "non-existent role",
|
||||
roleArn: "arn:seaweed:iam::role/NonExistentRole",
|
||||
roleArn: "arn:aws:iam::role/NonExistentRole",
|
||||
webIdentityToken: createSTSTestJWT(t, "test-issuer", "test-user"),
|
||||
sessionName: "test-session",
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "custom session duration",
|
||||
roleArn: "arn:seaweed:iam::role/TestRole",
|
||||
roleArn: "arn:aws:iam::role/TestRole",
|
||||
webIdentityToken: createSTSTestJWT(t, "test-issuer", "test-user"),
|
||||
sessionName: "test-session",
|
||||
durationSeconds: int64Ptr(7200), // 2 hours
|
||||
@@ -182,7 +182,7 @@ func TestAssumeRoleWithLDAP(t *testing.T) {
|
||||
}{
|
||||
{
|
||||
name: "successful LDAP role assumption",
|
||||
roleArn: "arn:seaweed:iam::role/LDAPRole",
|
||||
roleArn: "arn:aws:iam::role/LDAPRole",
|
||||
username: "testuser",
|
||||
password: "testpass",
|
||||
sessionName: "ldap-session",
|
||||
@@ -190,7 +190,7 @@ func TestAssumeRoleWithLDAP(t *testing.T) {
|
||||
},
|
||||
{
|
||||
name: "invalid LDAP credentials",
|
||||
roleArn: "arn:seaweed:iam::role/LDAPRole",
|
||||
roleArn: "arn:aws:iam::role/LDAPRole",
|
||||
username: "testuser",
|
||||
password: "wrongpass",
|
||||
sessionName: "ldap-session",
|
||||
@@ -231,7 +231,7 @@ func TestSessionTokenValidation(t *testing.T) {
|
||||
|
||||
// First, create a session
|
||||
request := &AssumeRoleWithWebIdentityRequest{
|
||||
RoleArn: "arn:seaweed:iam::role/TestRole",
|
||||
RoleArn: "arn:aws:iam::role/TestRole",
|
||||
WebIdentityToken: createSTSTestJWT(t, "test-issuer", "test-user"),
|
||||
RoleSessionName: "test-session",
|
||||
}
|
||||
@@ -275,7 +275,7 @@ func TestSessionTokenValidation(t *testing.T) {
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, session)
|
||||
assert.Equal(t, "test-session", session.SessionName)
|
||||
assert.Equal(t, "arn:seaweed:iam::role/TestRole", session.RoleArn)
|
||||
assert.Equal(t, "arn:aws:iam::role/TestRole", session.RoleArn)
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -289,7 +289,7 @@ func TestSessionTokenPersistence(t *testing.T) {
|
||||
|
||||
// Create a session first
|
||||
request := &AssumeRoleWithWebIdentityRequest{
|
||||
RoleArn: "arn:seaweed:iam::role/TestRole",
|
||||
RoleArn: "arn:aws:iam::role/TestRole",
|
||||
WebIdentityToken: createSTSTestJWT(t, "test-issuer", "test-user"),
|
||||
RoleSessionName: "test-session",
|
||||
}
|
||||
|
||||
@@ -207,11 +207,11 @@ func GenerateSessionId() (string, error) {
|
||||
// generateAssumedRoleArn generates the ARN for an assumed role user
|
||||
func GenerateAssumedRoleArn(roleArn, sessionName string) string {
|
||||
// Convert role ARN to assumed role user ARN
|
||||
// arn:seaweed:iam::role/RoleName -> arn:seaweed:sts::assumed-role/RoleName/SessionName
|
||||
// arn:aws:iam::role/RoleName -> arn:aws:sts::assumed-role/RoleName/SessionName
|
||||
roleName := utils.ExtractRoleNameFromArn(roleArn)
|
||||
if roleName == "" {
|
||||
// This should not happen if validation is done properly upstream
|
||||
return fmt.Sprintf("arn:seaweed:sts::assumed-role/INVALID-ARN/%s", sessionName)
|
||||
return fmt.Sprintf("arn:aws:sts::assumed-role/INVALID-ARN/%s", sessionName)
|
||||
}
|
||||
return fmt.Sprintf("arn:seaweed:sts::assumed-role/%s/%s", roleName, sessionName)
|
||||
return fmt.Sprintf("arn:aws:sts::assumed-role/%s/%s", roleName, sessionName)
|
||||
}
|
||||
|
||||
@@ -5,8 +5,8 @@ import "strings"
|
||||
// ExtractRoleNameFromPrincipal extracts role name from principal ARN
|
||||
// Handles both STS assumed role and IAM role formats
|
||||
func ExtractRoleNameFromPrincipal(principal string) string {
|
||||
// Handle STS assumed role format: arn:seaweed:sts::assumed-role/RoleName/SessionName
|
||||
stsPrefix := "arn:seaweed:sts::assumed-role/"
|
||||
// Handle STS assumed role format: arn:aws:sts::assumed-role/RoleName/SessionName
|
||||
stsPrefix := "arn:aws:sts::assumed-role/"
|
||||
if strings.HasPrefix(principal, stsPrefix) {
|
||||
remainder := principal[len(stsPrefix):]
|
||||
// Split on first '/' to get role name
|
||||
@@ -17,8 +17,8 @@ func ExtractRoleNameFromPrincipal(principal string) string {
|
||||
return remainder
|
||||
}
|
||||
|
||||
// Handle IAM role format: arn:seaweed:iam::role/RoleName
|
||||
iamPrefix := "arn:seaweed:iam::role/"
|
||||
// Handle IAM role format: arn:aws:iam::role/RoleName
|
||||
iamPrefix := "arn:aws:iam::role/"
|
||||
if strings.HasPrefix(principal, iamPrefix) {
|
||||
return principal[len(iamPrefix):]
|
||||
}
|
||||
@@ -29,9 +29,9 @@ func ExtractRoleNameFromPrincipal(principal string) string {
|
||||
}
|
||||
|
||||
// ExtractRoleNameFromArn extracts role name from an IAM role ARN
|
||||
// Specifically handles: arn:seaweed:iam::role/RoleName
|
||||
// Specifically handles: arn:aws:iam::role/RoleName
|
||||
func ExtractRoleNameFromArn(roleArn string) string {
|
||||
prefix := "arn:seaweed:iam::role/"
|
||||
prefix := "arn:aws:iam::role/"
|
||||
if strings.HasPrefix(roleArn, prefix) && len(roleArn) > len(prefix) {
|
||||
return roleArn[len(prefix):]
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user