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:
Chris Lu
2025-11-12 22:14:50 -08:00
committed by GitHub
parent 50f067bcfd
commit 508d06d9a5
47 changed files with 1104 additions and 749 deletions

View File

@@ -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/*",
},
},
},

View File

@@ -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

View File

@@ -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"},
}

View File

@@ -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",
},
}

View File

@@ -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

View File

@@ -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)

View File

@@ -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,
},
}

View File

@@ -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)

View File

@@ -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

View File

@@ -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)
}

View File

@@ -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",
}

View File

@@ -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)
}

View File

@@ -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):]
}