mirror of
https://github.com/seaweedfs/seaweedfs.git
synced 2026-09-08 15:41:15 +02:00
* feat(iam): implement group inline policy actions Add PutGroupPolicy, GetGroupPolicy, DeleteGroupPolicy, and ListGroupPolicies to both embedded and standalone IAM servers. The standalone IAM stores group inline policies in a new GroupInlinePolicies field in the Policies JSON, mirroring the existing user inline policy pattern. DeleteGroup now also checks for inline policies before allowing deletion. * fix: address review feedback for group inline policies - Embedded IAM: return NotImplemented for group inline policies instead of silently succeeding as no-ops (Gemini + CodeRabbit) - Standalone IAM: recompute member actions after PutGroupPolicy and DeleteGroupPolicy (Gemini) - Add parameter validation for GroupName/PolicyName/PolicyDocument on PutGroupPolicy, DeleteGroupPolicy, ListGroupPolicies (Gemini) - Add UserName validation for ListUserPolicies in standalone IAM - Call cleanupGroupInlinePolicies from DeleteGroup (Gemini) - Migrate GroupInlinePolicies on group rename in UpdateGroup (CodeRabbit) - Fix integration test cleanup order (CodeRabbit) * fix: persist recomputed actions and improve error handling - Set changed=true for PutGroupPolicy/DeleteGroupPolicy in standalone IAM DoActions so recomputed member actions are persisted (Gemini critical) - Make cleanupGroupInlinePolicies accept policies parameter to avoid redundant I/O, return error (Gemini) - Make migrateGroupInlinePolicies return error, handle in caller (Gemini) * fix: include group policies in action recomputation Extend computeAllActionsForUser to also aggregate group inline policies and group managed policies when s3cfg is provided. Previously, group inline policies were stored but never reflected in member Identity.Actions. (CodeRabbit critical) * perf: use identity index in recomputeActionsForGroupMembers for O(N+M) * fix: skip group inline policy integration test on embedded IAM The embedded IAM returns NotImplemented for group inline policies. Skip TestIAMGroupInlinePolicy when running against embedded mode to avoid CI failures in the group integration test matrix.
892 lines
30 KiB
Go
892 lines
30 KiB
Go
package iam
|
|
|
|
import (
|
|
"encoding/xml"
|
|
"io"
|
|
"net/http"
|
|
"net/url"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/aws/aws-sdk-go/aws"
|
|
"github.com/aws/aws-sdk-go/aws/awserr"
|
|
"github.com/aws/aws-sdk-go/aws/credentials"
|
|
"github.com/aws/aws-sdk-go/aws/session"
|
|
"github.com/aws/aws-sdk-go/service/iam"
|
|
"github.com/aws/aws-sdk-go/service/s3"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
// TestIAMGroupLifecycle tests the full lifecycle of group management:
|
|
// CreateGroup, GetGroup, ListGroups, DeleteGroup
|
|
func TestIAMGroupLifecycle(t *testing.T) {
|
|
framework := NewS3IAMTestFramework(t)
|
|
defer framework.Cleanup()
|
|
|
|
iamClient, err := framework.CreateIAMClientWithJWT("admin-user", "TestAdminRole")
|
|
require.NoError(t, err)
|
|
|
|
groupName := "test-group-lifecycle"
|
|
|
|
t.Run("create_group", func(t *testing.T) {
|
|
resp, err := iamClient.CreateGroup(&iam.CreateGroupInput{
|
|
GroupName: aws.String(groupName),
|
|
})
|
|
require.NoError(t, err)
|
|
assert.Equal(t, groupName, *resp.Group.GroupName)
|
|
})
|
|
|
|
t.Run("get_group", func(t *testing.T) {
|
|
resp, err := iamClient.GetGroup(&iam.GetGroupInput{
|
|
GroupName: aws.String(groupName),
|
|
})
|
|
require.NoError(t, err)
|
|
assert.Equal(t, groupName, *resp.Group.GroupName)
|
|
})
|
|
|
|
t.Run("list_groups_contains_created", func(t *testing.T) {
|
|
resp, err := iamClient.ListGroups(&iam.ListGroupsInput{})
|
|
require.NoError(t, err)
|
|
found := false
|
|
for _, g := range resp.Groups {
|
|
if *g.GroupName == groupName {
|
|
found = true
|
|
break
|
|
}
|
|
}
|
|
assert.True(t, found, "Created group should appear in ListGroups")
|
|
})
|
|
|
|
t.Run("create_duplicate_group_fails", func(t *testing.T) {
|
|
_, err := iamClient.CreateGroup(&iam.CreateGroupInput{
|
|
GroupName: aws.String(groupName),
|
|
})
|
|
assert.Error(t, err, "Creating a duplicate group should fail")
|
|
})
|
|
|
|
t.Run("delete_group", func(t *testing.T) {
|
|
_, err := iamClient.DeleteGroup(&iam.DeleteGroupInput{
|
|
GroupName: aws.String(groupName),
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
// Verify it's gone
|
|
resp, err := iamClient.ListGroups(&iam.ListGroupsInput{})
|
|
require.NoError(t, err)
|
|
for _, g := range resp.Groups {
|
|
assert.NotEqual(t, groupName, *g.GroupName,
|
|
"Deleted group should not appear in ListGroups")
|
|
}
|
|
})
|
|
|
|
t.Run("delete_nonexistent_group_fails", func(t *testing.T) {
|
|
_, err := iamClient.DeleteGroup(&iam.DeleteGroupInput{
|
|
GroupName: aws.String("nonexistent-group-xyz"),
|
|
})
|
|
assert.Error(t, err)
|
|
})
|
|
}
|
|
|
|
// TestIAMGroupMembership tests adding and removing users from groups
|
|
func TestIAMGroupMembership(t *testing.T) {
|
|
framework := NewS3IAMTestFramework(t)
|
|
defer framework.Cleanup()
|
|
|
|
iamClient, err := framework.CreateIAMClientWithJWT("admin-user", "TestAdminRole")
|
|
require.NoError(t, err)
|
|
|
|
groupName := "test-group-members"
|
|
userName := "test-user-for-group"
|
|
|
|
// Setup: create group and user
|
|
_, err = iamClient.CreateGroup(&iam.CreateGroupInput{
|
|
GroupName: aws.String(groupName),
|
|
})
|
|
require.NoError(t, err)
|
|
defer iamClient.DeleteGroup(&iam.DeleteGroupInput{GroupName: aws.String(groupName)})
|
|
|
|
_, err = iamClient.CreateUser(&iam.CreateUserInput{
|
|
UserName: aws.String(userName),
|
|
})
|
|
require.NoError(t, err)
|
|
defer iamClient.DeleteUser(&iam.DeleteUserInput{UserName: aws.String(userName)})
|
|
|
|
t.Run("add_user_to_group", func(t *testing.T) {
|
|
_, err := iamClient.AddUserToGroup(&iam.AddUserToGroupInput{
|
|
GroupName: aws.String(groupName),
|
|
UserName: aws.String(userName),
|
|
})
|
|
require.NoError(t, err)
|
|
})
|
|
|
|
t.Run("get_group_shows_member", func(t *testing.T) {
|
|
resp, err := iamClient.GetGroup(&iam.GetGroupInput{
|
|
GroupName: aws.String(groupName),
|
|
})
|
|
require.NoError(t, err)
|
|
found := false
|
|
for _, u := range resp.Users {
|
|
if *u.UserName == userName {
|
|
found = true
|
|
break
|
|
}
|
|
}
|
|
assert.True(t, found, "Added user should appear in GetGroup members")
|
|
})
|
|
|
|
t.Run("list_groups_for_user", func(t *testing.T) {
|
|
resp, err := iamClient.ListGroupsForUser(&iam.ListGroupsForUserInput{
|
|
UserName: aws.String(userName),
|
|
})
|
|
require.NoError(t, err)
|
|
found := false
|
|
for _, g := range resp.Groups {
|
|
if *g.GroupName == groupName {
|
|
found = true
|
|
break
|
|
}
|
|
}
|
|
assert.True(t, found, "Group should appear in ListGroupsForUser")
|
|
})
|
|
|
|
t.Run("add_duplicate_member_is_idempotent", func(t *testing.T) {
|
|
_, err := iamClient.AddUserToGroup(&iam.AddUserToGroupInput{
|
|
GroupName: aws.String(groupName),
|
|
UserName: aws.String(userName),
|
|
})
|
|
// Should succeed (idempotent) or return a benign error
|
|
// AWS IAM allows duplicate add without error
|
|
assert.NoError(t, err)
|
|
})
|
|
|
|
t.Run("remove_user_from_group", func(t *testing.T) {
|
|
_, err := iamClient.RemoveUserFromGroup(&iam.RemoveUserFromGroupInput{
|
|
GroupName: aws.String(groupName),
|
|
UserName: aws.String(userName),
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
// Verify removal
|
|
resp, err := iamClient.GetGroup(&iam.GetGroupInput{
|
|
GroupName: aws.String(groupName),
|
|
})
|
|
require.NoError(t, err)
|
|
for _, u := range resp.Users {
|
|
assert.NotEqual(t, userName, *u.UserName,
|
|
"Removed user should not appear in group members")
|
|
}
|
|
})
|
|
}
|
|
|
|
// TestIAMGroupPolicyAttachment tests attaching and detaching policies from groups
|
|
func TestIAMGroupPolicyAttachment(t *testing.T) {
|
|
framework := NewS3IAMTestFramework(t)
|
|
defer framework.Cleanup()
|
|
|
|
iamClient, err := framework.CreateIAMClientWithJWT("admin-user", "TestAdminRole")
|
|
require.NoError(t, err)
|
|
|
|
groupName := "test-group-policies"
|
|
policyName := "test-group-attach-policy"
|
|
policyDoc := `{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Action":"s3:ListBucket","Resource":"*"}]}`
|
|
|
|
// Setup: create group and policy
|
|
_, err = iamClient.CreateGroup(&iam.CreateGroupInput{
|
|
GroupName: aws.String(groupName),
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
createPolicyResp, err := iamClient.CreatePolicy(&iam.CreatePolicyInput{
|
|
PolicyName: aws.String(policyName),
|
|
PolicyDocument: aws.String(policyDoc),
|
|
})
|
|
require.NoError(t, err)
|
|
policyArn := createPolicyResp.Policy.Arn
|
|
|
|
// Cleanup in correct order: detach policy, delete group, delete policy
|
|
t.Cleanup(func() {
|
|
if _, err := iamClient.DetachGroupPolicy(&iam.DetachGroupPolicyInput{
|
|
GroupName: aws.String(groupName),
|
|
PolicyArn: policyArn,
|
|
}); err != nil {
|
|
t.Logf("cleanup: failed to detach group policy: %v", err)
|
|
}
|
|
if _, err := iamClient.DeleteGroup(&iam.DeleteGroupInput{GroupName: aws.String(groupName)}); err != nil {
|
|
t.Logf("cleanup: failed to delete group: %v", err)
|
|
}
|
|
if _, err := iamClient.DeletePolicy(&iam.DeletePolicyInput{PolicyArn: policyArn}); err != nil {
|
|
t.Logf("cleanup: failed to delete policy: %v", err)
|
|
}
|
|
})
|
|
|
|
t.Run("attach_group_policy", func(t *testing.T) {
|
|
_, err := iamClient.AttachGroupPolicy(&iam.AttachGroupPolicyInput{
|
|
GroupName: aws.String(groupName),
|
|
PolicyArn: policyArn,
|
|
})
|
|
require.NoError(t, err)
|
|
})
|
|
|
|
t.Run("list_attached_group_policies", func(t *testing.T) {
|
|
resp, err := iamClient.ListAttachedGroupPolicies(&iam.ListAttachedGroupPoliciesInput{
|
|
GroupName: aws.String(groupName),
|
|
})
|
|
require.NoError(t, err)
|
|
found := false
|
|
for _, p := range resp.AttachedPolicies {
|
|
if *p.PolicyName == policyName {
|
|
found = true
|
|
break
|
|
}
|
|
}
|
|
assert.True(t, found, "Attached policy should appear in ListAttachedGroupPolicies")
|
|
})
|
|
|
|
t.Run("detach_group_policy", func(t *testing.T) {
|
|
_, err := iamClient.DetachGroupPolicy(&iam.DetachGroupPolicyInput{
|
|
GroupName: aws.String(groupName),
|
|
PolicyArn: policyArn,
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
// Verify detachment
|
|
resp, err := iamClient.ListAttachedGroupPolicies(&iam.ListAttachedGroupPoliciesInput{
|
|
GroupName: aws.String(groupName),
|
|
})
|
|
require.NoError(t, err)
|
|
for _, p := range resp.AttachedPolicies {
|
|
assert.NotEqual(t, policyName, *p.PolicyName,
|
|
"Detached policy should not appear in ListAttachedGroupPolicies")
|
|
}
|
|
})
|
|
}
|
|
|
|
// TestIAMGroupPolicyEnforcement tests that group policies are enforced during S3 operations.
|
|
// Creates a user with no direct policies, adds them to a group with S3 access,
|
|
// and verifies they can access S3 through the group policy.
|
|
func TestIAMGroupPolicyEnforcement(t *testing.T) {
|
|
framework := NewS3IAMTestFramework(t)
|
|
defer framework.Cleanup()
|
|
|
|
iamClient, err := framework.CreateIAMClientWithJWT("admin-user", "TestAdminRole")
|
|
require.NoError(t, err)
|
|
|
|
groupName := "test-enforcement-group"
|
|
userName := "test-enforcement-user"
|
|
policyName := "test-enforcement-policy"
|
|
bucketName := "test-group-enforce-bucket"
|
|
policyDoc := `{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Action":["s3:*"],"Resource":["arn:aws:s3:::` + bucketName + `","arn:aws:s3:::` + bucketName + `/*"]}]}`
|
|
|
|
// Create user
|
|
_, err = iamClient.CreateUser(&iam.CreateUserInput{
|
|
UserName: aws.String(userName),
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
// Create access key for the user
|
|
keyResp, err := iamClient.CreateAccessKey(&iam.CreateAccessKeyInput{
|
|
UserName: aws.String(userName),
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
accessKeyId := *keyResp.AccessKey.AccessKeyId
|
|
secretKey := *keyResp.AccessKey.SecretAccessKey
|
|
|
|
// Create an S3 client with the user's credentials
|
|
userS3Client := createS3Client(t, accessKeyId, secretKey)
|
|
|
|
// Create group
|
|
_, err = iamClient.CreateGroup(&iam.CreateGroupInput{
|
|
GroupName: aws.String(groupName),
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
// Create policy
|
|
createPolicyResp, err := iamClient.CreatePolicy(&iam.CreatePolicyInput{
|
|
PolicyName: aws.String(policyName),
|
|
PolicyDocument: aws.String(policyDoc),
|
|
})
|
|
require.NoError(t, err)
|
|
policyArn := createPolicyResp.Policy.Arn
|
|
|
|
// Cleanup in correct order: remove user from group, detach policy,
|
|
// delete access key, delete user, delete group, delete policy
|
|
t.Cleanup(func() {
|
|
if _, err := iamClient.RemoveUserFromGroup(&iam.RemoveUserFromGroupInput{
|
|
GroupName: aws.String(groupName),
|
|
UserName: aws.String(userName),
|
|
}); err != nil {
|
|
t.Logf("cleanup: failed to remove user from group: %v", err)
|
|
}
|
|
if _, err := iamClient.DetachGroupPolicy(&iam.DetachGroupPolicyInput{
|
|
GroupName: aws.String(groupName),
|
|
PolicyArn: policyArn,
|
|
}); err != nil {
|
|
t.Logf("cleanup: failed to detach group policy: %v", err)
|
|
}
|
|
if _, err := iamClient.DeleteAccessKey(&iam.DeleteAccessKeyInput{
|
|
UserName: aws.String(userName),
|
|
AccessKeyId: keyResp.AccessKey.AccessKeyId,
|
|
}); err != nil {
|
|
t.Logf("cleanup: failed to delete access key: %v", err)
|
|
}
|
|
if _, err := iamClient.DeleteUser(&iam.DeleteUserInput{UserName: aws.String(userName)}); err != nil {
|
|
t.Logf("cleanup: failed to delete user: %v", err)
|
|
}
|
|
if _, err := iamClient.DeleteGroup(&iam.DeleteGroupInput{GroupName: aws.String(groupName)}); err != nil {
|
|
t.Logf("cleanup: failed to delete group: %v", err)
|
|
}
|
|
if _, err := iamClient.DeletePolicy(&iam.DeletePolicyInput{PolicyArn: policyArn}); err != nil {
|
|
t.Logf("cleanup: failed to delete policy: %v", err)
|
|
}
|
|
})
|
|
|
|
// Register bucket cleanup on parent test with admin credentials
|
|
// (userS3Client may lack permissions by cleanup time)
|
|
adminS3, err := framework.CreateS3ClientWithJWT("admin-user", "TestAdminRole")
|
|
require.NoError(t, err)
|
|
t.Cleanup(func() {
|
|
if _, err := adminS3.DeleteObject(&s3.DeleteObjectInput{
|
|
Bucket: aws.String(bucketName),
|
|
Key: aws.String("test-key"),
|
|
}); err != nil {
|
|
t.Logf("cleanup: failed to delete object: %v", err)
|
|
}
|
|
if _, err := adminS3.DeleteBucket(&s3.DeleteBucketInput{Bucket: aws.String(bucketName)}); err != nil {
|
|
t.Logf("cleanup: failed to delete bucket: %v", err)
|
|
}
|
|
})
|
|
|
|
t.Run("user_without_group_denied", func(t *testing.T) {
|
|
// User has no policies and is not in any group — should be denied
|
|
_, err := userS3Client.CreateBucket(&s3.CreateBucketInput{
|
|
Bucket: aws.String(bucketName),
|
|
})
|
|
require.Error(t, err, "User without any policies should be denied")
|
|
awsErr, ok := err.(awserr.Error)
|
|
require.True(t, ok, "Expected awserr.Error")
|
|
assert.Equal(t, "AccessDenied", awsErr.Code())
|
|
})
|
|
|
|
t.Run("user_with_group_policy_allowed", func(t *testing.T) {
|
|
// Attach policy to group
|
|
_, err := iamClient.AttachGroupPolicy(&iam.AttachGroupPolicyInput{
|
|
GroupName: aws.String(groupName),
|
|
PolicyArn: policyArn,
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
// Add user to group
|
|
_, err = iamClient.AddUserToGroup(&iam.AddUserToGroupInput{
|
|
GroupName: aws.String(groupName),
|
|
UserName: aws.String(userName),
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
// Wait for policy propagation, then create bucket
|
|
require.Eventually(t, func() bool {
|
|
_, err = userS3Client.CreateBucket(&s3.CreateBucketInput{
|
|
Bucket: aws.String(bucketName),
|
|
})
|
|
return err == nil
|
|
}, 10*time.Second, 500*time.Millisecond, "User with group policy should be allowed")
|
|
|
|
// Should also be able to put/get objects
|
|
_, err = userS3Client.PutObject(&s3.PutObjectInput{
|
|
Bucket: aws.String(bucketName),
|
|
Key: aws.String("test-key"),
|
|
Body: aws.ReadSeekCloser(strings.NewReader("test-data")),
|
|
})
|
|
require.NoError(t, err, "User should be able to put objects through group policy")
|
|
})
|
|
|
|
t.Run("user_removed_from_group_denied", func(t *testing.T) {
|
|
// Remove user from group
|
|
_, err := iamClient.RemoveUserFromGroup(&iam.RemoveUserFromGroupInput{
|
|
GroupName: aws.String(groupName),
|
|
UserName: aws.String(userName),
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
// Wait for policy propagation — user should now be denied
|
|
var lastErr error
|
|
require.Eventually(t, func() bool {
|
|
_, lastErr = userS3Client.ListObjects(&s3.ListObjectsInput{
|
|
Bucket: aws.String(bucketName),
|
|
})
|
|
return lastErr != nil
|
|
}, 10*time.Second, 500*time.Millisecond, "User removed from group should be denied")
|
|
awsErr, ok := lastErr.(awserr.Error)
|
|
require.True(t, ok, "Expected awserr.Error")
|
|
assert.Equal(t, "AccessDenied", awsErr.Code())
|
|
})
|
|
}
|
|
|
|
// TestIAMGroupDisabledPolicyEnforcement tests that disabled groups do not contribute policies.
|
|
// Uses the raw IAM API (callIAMAPI) since the AWS SDK doesn't support custom group status.
|
|
func TestIAMGroupDisabledPolicyEnforcement(t *testing.T) {
|
|
if testing.Short() {
|
|
t.Skip("Skipping integration test in short mode")
|
|
}
|
|
if !isSeaweedFSRunning(t) {
|
|
t.Skip("SeaweedFS is not running at", TestIAMEndpoint)
|
|
}
|
|
|
|
framework := NewS3IAMTestFramework(t)
|
|
defer framework.Cleanup()
|
|
|
|
iamClient, err := framework.CreateIAMClientWithJWT("admin-user", "TestAdminRole")
|
|
require.NoError(t, err)
|
|
|
|
groupName := "test-disabled-group"
|
|
userName := "test-disabled-grp-user"
|
|
policyName := "test-disabled-grp-policy"
|
|
bucketName := "test-disabled-grp-bucket"
|
|
policyDoc := `{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Action":["s3:*"],"Resource":["arn:aws:s3:::` + bucketName + `","arn:aws:s3:::` + bucketName + `/*"]}]}`
|
|
|
|
// Create user, group, policy
|
|
_, err = iamClient.CreateUser(&iam.CreateUserInput{UserName: aws.String(userName)})
|
|
require.NoError(t, err)
|
|
|
|
keyResp, err := iamClient.CreateAccessKey(&iam.CreateAccessKeyInput{UserName: aws.String(userName)})
|
|
require.NoError(t, err)
|
|
|
|
_, err = iamClient.CreateGroup(&iam.CreateGroupInput{GroupName: aws.String(groupName)})
|
|
require.NoError(t, err)
|
|
|
|
createPolicyResp, err := iamClient.CreatePolicy(&iam.CreatePolicyInput{
|
|
PolicyName: aws.String(policyName), PolicyDocument: aws.String(policyDoc),
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
// Cleanup in correct order: remove user from group, detach policy,
|
|
// delete access key, delete user, delete group, delete policy
|
|
t.Cleanup(func() {
|
|
if _, err := iamClient.RemoveUserFromGroup(&iam.RemoveUserFromGroupInput{
|
|
GroupName: aws.String(groupName), UserName: aws.String(userName),
|
|
}); err != nil {
|
|
t.Logf("cleanup: failed to remove user from group: %v", err)
|
|
}
|
|
if _, err := iamClient.DetachGroupPolicy(&iam.DetachGroupPolicyInput{
|
|
GroupName: aws.String(groupName),
|
|
PolicyArn: aws.String("arn:aws:iam:::policy/" + policyName),
|
|
}); err != nil {
|
|
t.Logf("cleanup: failed to detach group policy: %v", err)
|
|
}
|
|
if _, err := iamClient.DeleteAccessKey(&iam.DeleteAccessKeyInput{
|
|
UserName: aws.String(userName), AccessKeyId: keyResp.AccessKey.AccessKeyId,
|
|
}); err != nil {
|
|
t.Logf("cleanup: failed to delete access key: %v", err)
|
|
}
|
|
if _, err := iamClient.DeleteUser(&iam.DeleteUserInput{UserName: aws.String(userName)}); err != nil {
|
|
t.Logf("cleanup: failed to delete user: %v", err)
|
|
}
|
|
if _, err := iamClient.DeleteGroup(&iam.DeleteGroupInput{GroupName: aws.String(groupName)}); err != nil {
|
|
t.Logf("cleanup: failed to delete group: %v", err)
|
|
}
|
|
if _, err := iamClient.DeletePolicy(&iam.DeletePolicyInput{PolicyArn: createPolicyResp.Policy.Arn}); err != nil {
|
|
t.Logf("cleanup: failed to delete policy: %v", err)
|
|
}
|
|
})
|
|
|
|
// Setup: attach policy, add user, create bucket with admin
|
|
_, err = iamClient.AttachGroupPolicy(&iam.AttachGroupPolicyInput{
|
|
GroupName: aws.String(groupName), PolicyArn: createPolicyResp.Policy.Arn,
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
_, err = iamClient.AddUserToGroup(&iam.AddUserToGroupInput{
|
|
GroupName: aws.String(groupName), UserName: aws.String(userName),
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
userS3Client := createS3Client(t, *keyResp.AccessKey.AccessKeyId, *keyResp.AccessKey.SecretAccessKey)
|
|
|
|
// Create bucket using admin first so we can test listing
|
|
adminS3, err := framework.CreateS3ClientWithJWT("admin-user", "TestAdminRole")
|
|
require.NoError(t, err)
|
|
_, err = adminS3.CreateBucket(&s3.CreateBucketInput{Bucket: aws.String(bucketName)})
|
|
require.NoError(t, err)
|
|
defer adminS3.DeleteBucket(&s3.DeleteBucketInput{Bucket: aws.String(bucketName)})
|
|
|
|
t.Run("enabled_group_allows_access", func(t *testing.T) {
|
|
require.Eventually(t, func() bool {
|
|
_, err := userS3Client.ListObjects(&s3.ListObjectsInput{
|
|
Bucket: aws.String(bucketName),
|
|
})
|
|
return err == nil
|
|
}, 10*time.Second, 500*time.Millisecond, "User in enabled group should have access")
|
|
})
|
|
|
|
t.Run("disabled_group_denies_access", func(t *testing.T) {
|
|
// Disable group via raw IAM API (no SDK support for this extension)
|
|
resp, err := callIAMAPIAuthenticated(t, framework, "UpdateGroup", url.Values{
|
|
"GroupName": {groupName},
|
|
"Disabled": {"true"},
|
|
})
|
|
require.NoError(t, err)
|
|
defer resp.Body.Close()
|
|
require.Equal(t, http.StatusOK, resp.StatusCode, "UpdateGroup (disable) should return 200")
|
|
|
|
// Wait for propagation — user should be denied
|
|
var lastErr error
|
|
require.Eventually(t, func() bool {
|
|
_, lastErr = userS3Client.ListObjects(&s3.ListObjectsInput{
|
|
Bucket: aws.String(bucketName),
|
|
})
|
|
return lastErr != nil
|
|
}, 10*time.Second, 500*time.Millisecond, "User in disabled group should be denied access")
|
|
awsErr, ok := lastErr.(awserr.Error)
|
|
require.True(t, ok, "Expected awserr.Error")
|
|
assert.Equal(t, "AccessDenied", awsErr.Code())
|
|
})
|
|
|
|
t.Run("re_enabled_group_restores_access", func(t *testing.T) {
|
|
// Re-enable the group
|
|
resp, err := callIAMAPIAuthenticated(t, framework, "UpdateGroup", url.Values{
|
|
"GroupName": {groupName},
|
|
"Disabled": {"false"},
|
|
})
|
|
require.NoError(t, err)
|
|
defer resp.Body.Close()
|
|
require.Equal(t, http.StatusOK, resp.StatusCode, "UpdateGroup (re-enable) should return 200")
|
|
|
|
// Wait for propagation — user should have access again
|
|
require.Eventually(t, func() bool {
|
|
_, err = userS3Client.ListObjects(&s3.ListObjectsInput{
|
|
Bucket: aws.String(bucketName),
|
|
})
|
|
return err == nil
|
|
}, 10*time.Second, 500*time.Millisecond, "User in re-enabled group should have access again")
|
|
})
|
|
}
|
|
|
|
// TestIAMGroupUserDeletionSideEffect tests that deleting a user removes them from all groups.
|
|
func TestIAMGroupUserDeletionSideEffect(t *testing.T) {
|
|
framework := NewS3IAMTestFramework(t)
|
|
defer framework.Cleanup()
|
|
|
|
iamClient, err := framework.CreateIAMClientWithJWT("admin-user", "TestAdminRole")
|
|
require.NoError(t, err)
|
|
|
|
groupName := "test-deletion-group"
|
|
userName := "test-deletion-user"
|
|
|
|
// Create group and user
|
|
_, err = iamClient.CreateGroup(&iam.CreateGroupInput{GroupName: aws.String(groupName)})
|
|
require.NoError(t, err)
|
|
defer iamClient.DeleteGroup(&iam.DeleteGroupInput{GroupName: aws.String(groupName)})
|
|
|
|
_, err = iamClient.CreateUser(&iam.CreateUserInput{UserName: aws.String(userName)})
|
|
require.NoError(t, err)
|
|
t.Cleanup(func() {
|
|
// Best-effort: user may already be deleted by the test
|
|
iamClient.DeleteUser(&iam.DeleteUserInput{UserName: aws.String(userName)})
|
|
})
|
|
|
|
// Add user to group
|
|
_, err = iamClient.AddUserToGroup(&iam.AddUserToGroupInput{
|
|
GroupName: aws.String(groupName),
|
|
UserName: aws.String(userName),
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
// Verify user is in group
|
|
getResp, err := iamClient.GetGroup(&iam.GetGroupInput{GroupName: aws.String(groupName)})
|
|
require.NoError(t, err)
|
|
assert.Len(t, getResp.Users, 1, "Group should have 1 member before deletion")
|
|
|
|
// Delete the user
|
|
_, err = iamClient.DeleteUser(&iam.DeleteUserInput{UserName: aws.String(userName)})
|
|
require.NoError(t, err)
|
|
|
|
// Verify user was removed from the group
|
|
getResp, err = iamClient.GetGroup(&iam.GetGroupInput{GroupName: aws.String(groupName)})
|
|
require.NoError(t, err)
|
|
assert.Empty(t, getResp.Users, "Group should have no members after user deletion")
|
|
}
|
|
|
|
// TestIAMGroupMultipleGroups tests that a user can belong to multiple groups
|
|
// and inherits policies from all of them.
|
|
func TestIAMGroupMultipleGroups(t *testing.T) {
|
|
framework := NewS3IAMTestFramework(t)
|
|
defer framework.Cleanup()
|
|
|
|
iamClient, err := framework.CreateIAMClientWithJWT("admin-user", "TestAdminRole")
|
|
require.NoError(t, err)
|
|
|
|
group1 := "test-multi-group-1"
|
|
group2 := "test-multi-group-2"
|
|
userName := "test-multi-group-user"
|
|
|
|
// Create two groups
|
|
_, err = iamClient.CreateGroup(&iam.CreateGroupInput{GroupName: aws.String(group1)})
|
|
require.NoError(t, err)
|
|
defer iamClient.DeleteGroup(&iam.DeleteGroupInput{GroupName: aws.String(group1)})
|
|
|
|
_, err = iamClient.CreateGroup(&iam.CreateGroupInput{GroupName: aws.String(group2)})
|
|
require.NoError(t, err)
|
|
defer iamClient.DeleteGroup(&iam.DeleteGroupInput{GroupName: aws.String(group2)})
|
|
|
|
// Create user
|
|
_, err = iamClient.CreateUser(&iam.CreateUserInput{UserName: aws.String(userName)})
|
|
require.NoError(t, err)
|
|
defer func() {
|
|
iamClient.RemoveUserFromGroup(&iam.RemoveUserFromGroupInput{
|
|
GroupName: aws.String(group1), UserName: aws.String(userName),
|
|
})
|
|
iamClient.RemoveUserFromGroup(&iam.RemoveUserFromGroupInput{
|
|
GroupName: aws.String(group2), UserName: aws.String(userName),
|
|
})
|
|
iamClient.DeleteUser(&iam.DeleteUserInput{UserName: aws.String(userName)})
|
|
}()
|
|
|
|
// Add user to both groups
|
|
_, err = iamClient.AddUserToGroup(&iam.AddUserToGroupInput{
|
|
GroupName: aws.String(group1), UserName: aws.String(userName),
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
_, err = iamClient.AddUserToGroup(&iam.AddUserToGroupInput{
|
|
GroupName: aws.String(group2), UserName: aws.String(userName),
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
// Verify user appears in both groups
|
|
resp, err := iamClient.ListGroupsForUser(&iam.ListGroupsForUserInput{
|
|
UserName: aws.String(userName),
|
|
})
|
|
require.NoError(t, err)
|
|
groupNames := make(map[string]bool)
|
|
for _, g := range resp.Groups {
|
|
groupNames[*g.GroupName] = true
|
|
}
|
|
assert.True(t, groupNames[group1], "User should be in group 1")
|
|
assert.True(t, groupNames[group2], "User should be in group 2")
|
|
}
|
|
|
|
// --- Response types for raw IAM API calls ---
|
|
|
|
type CreateGroupResponse struct {
|
|
XMLName xml.Name `xml:"CreateGroupResponse"`
|
|
CreateGroupResult struct {
|
|
Group struct {
|
|
GroupName string `xml:"GroupName"`
|
|
} `xml:"Group"`
|
|
} `xml:"CreateGroupResult"`
|
|
}
|
|
|
|
type ListGroupsResponse struct {
|
|
XMLName xml.Name `xml:"ListGroupsResponse"`
|
|
ListGroupsResult struct {
|
|
Groups []struct {
|
|
GroupName string `xml:"GroupName"`
|
|
} `xml:"Groups>member"`
|
|
} `xml:"ListGroupsResult"`
|
|
}
|
|
|
|
// callIAMAPIAuthenticated sends an authenticated raw IAM API request using the
|
|
// framework's JWT token. This is needed for custom extensions not in the AWS SDK
|
|
// (like UpdateGroup with Disabled parameter).
|
|
func callIAMAPIAuthenticated(_ *testing.T, framework *S3IAMTestFramework, action string, params url.Values) (*http.Response, error) {
|
|
params.Set("Action", action)
|
|
|
|
req, err := http.NewRequest(http.MethodPost, TestIAMEndpoint+"/",
|
|
strings.NewReader(params.Encode()))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
|
|
|
token, err := framework.generateSTSSessionToken("admin-user", "TestAdminRole", time.Hour, "", nil)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
client := &http.Client{
|
|
Timeout: 30 * time.Second,
|
|
Transport: &BearerTokenTransport{Token: token},
|
|
}
|
|
return client.Do(req)
|
|
}
|
|
|
|
// TestIAMGroupRawAPI tests group operations using raw HTTP IAM API calls,
|
|
// verifying XML response format for group operations.
|
|
func TestIAMGroupRawAPI(t *testing.T) {
|
|
if testing.Short() {
|
|
t.Skip("Skipping integration test in short mode")
|
|
}
|
|
if !isSeaweedFSRunning(t) {
|
|
t.Skip("SeaweedFS is not running at", TestIAMEndpoint)
|
|
}
|
|
|
|
framework := NewS3IAMTestFramework(t)
|
|
defer framework.Cleanup()
|
|
|
|
groupName := "test-raw-api-group"
|
|
|
|
t.Run("create_group_raw", func(t *testing.T) {
|
|
resp, err := callIAMAPIAuthenticated(t, framework, "CreateGroup", url.Values{
|
|
"GroupName": {groupName},
|
|
})
|
|
require.NoError(t, err)
|
|
defer resp.Body.Close()
|
|
assert.Equal(t, http.StatusOK, resp.StatusCode)
|
|
|
|
body, err := io.ReadAll(resp.Body)
|
|
require.NoError(t, err)
|
|
|
|
var createResp CreateGroupResponse
|
|
err = xml.Unmarshal(body, &createResp)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, groupName, createResp.CreateGroupResult.Group.GroupName)
|
|
})
|
|
|
|
t.Run("list_groups_raw", func(t *testing.T) {
|
|
resp, err := callIAMAPIAuthenticated(t, framework, "ListGroups", url.Values{})
|
|
require.NoError(t, err)
|
|
defer resp.Body.Close()
|
|
assert.Equal(t, http.StatusOK, resp.StatusCode)
|
|
|
|
body, err := io.ReadAll(resp.Body)
|
|
require.NoError(t, err)
|
|
|
|
var listResp ListGroupsResponse
|
|
err = xml.Unmarshal(body, &listResp)
|
|
require.NoError(t, err)
|
|
|
|
found := false
|
|
for _, g := range listResp.ListGroupsResult.Groups {
|
|
if g.GroupName == groupName {
|
|
found = true
|
|
break
|
|
}
|
|
}
|
|
assert.True(t, found, "Created group should appear in raw ListGroups")
|
|
})
|
|
|
|
t.Run("delete_group_raw", func(t *testing.T) {
|
|
resp, err := callIAMAPIAuthenticated(t, framework, "DeleteGroup", url.Values{
|
|
"GroupName": {groupName},
|
|
})
|
|
require.NoError(t, err)
|
|
defer resp.Body.Close()
|
|
assert.Equal(t, http.StatusOK, resp.StatusCode)
|
|
})
|
|
}
|
|
|
|
// TestIAMGroupInlinePolicy tests group inline policy operations:
|
|
// PutGroupPolicy, GetGroupPolicy, ListGroupPolicies, DeleteGroupPolicy
|
|
func TestIAMGroupInlinePolicy(t *testing.T) {
|
|
framework := NewS3IAMTestFramework(t)
|
|
defer framework.Cleanup()
|
|
|
|
iamClient, err := framework.CreateIAMClientWithJWT("admin-user", "TestAdminRole")
|
|
require.NoError(t, err)
|
|
|
|
// Skip if running against embedded IAM (which returns NotImplemented for group inline policies)
|
|
_, probeErr := iamClient.ListGroupPolicies(&iam.ListGroupPoliciesInput{GroupName: aws.String("probe-group-inline-support")})
|
|
if probeErr != nil {
|
|
if awsErr, ok := probeErr.(awserr.Error); ok && awsErr.Code() == "NotImplemented" {
|
|
t.Skip("Skipping: group inline policies not supported in embedded IAM mode")
|
|
}
|
|
}
|
|
require.NoError(t, err)
|
|
|
|
groupName := "test-group-inline-policy"
|
|
policyName := "TestInlinePolicy"
|
|
_, err = iamClient.CreateGroup(&iam.CreateGroupInput{
|
|
GroupName: aws.String(groupName),
|
|
})
|
|
require.NoError(t, err)
|
|
defer func() {
|
|
// Clean up inline policies before deleting the group
|
|
iamClient.DeleteGroupPolicy(&iam.DeleteGroupPolicyInput{
|
|
GroupName: aws.String(groupName),
|
|
PolicyName: aws.String(policyName),
|
|
})
|
|
iamClient.DeleteGroup(&iam.DeleteGroupInput{GroupName: aws.String(groupName)})
|
|
}()
|
|
|
|
t.Run("list_empty", func(t *testing.T) {
|
|
resp, err := iamClient.ListGroupPolicies(&iam.ListGroupPoliciesInput{
|
|
GroupName: aws.String(groupName),
|
|
})
|
|
require.NoError(t, err)
|
|
assert.Empty(t, resp.PolicyNames)
|
|
assert.False(t, *resp.IsTruncated)
|
|
})
|
|
|
|
policyDoc := `{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Action":"s3:GetObject","Resource":"arn:aws:s3:::test-bucket/*"}]}`
|
|
|
|
t.Run("put_group_policy", func(t *testing.T) {
|
|
_, err := iamClient.PutGroupPolicy(&iam.PutGroupPolicyInput{
|
|
GroupName: aws.String(groupName),
|
|
PolicyName: aws.String(policyName),
|
|
PolicyDocument: aws.String(policyDoc),
|
|
})
|
|
require.NoError(t, err)
|
|
})
|
|
|
|
t.Run("list_after_put", func(t *testing.T) {
|
|
resp, err := iamClient.ListGroupPolicies(&iam.ListGroupPoliciesInput{
|
|
GroupName: aws.String(groupName),
|
|
})
|
|
require.NoError(t, err)
|
|
assert.NotEmpty(t, resp.PolicyNames)
|
|
})
|
|
|
|
t.Run("get_group_policy", func(t *testing.T) {
|
|
resp, err := iamClient.GetGroupPolicy(&iam.GetGroupPolicyInput{
|
|
GroupName: aws.String(groupName),
|
|
PolicyName: aws.String(policyName),
|
|
})
|
|
require.NoError(t, err)
|
|
assert.Equal(t, groupName, *resp.GroupName)
|
|
assert.Equal(t, policyName, *resp.PolicyName)
|
|
assert.Contains(t, *resp.PolicyDocument, "s3:GetObject")
|
|
})
|
|
|
|
t.Run("delete_group_policy", func(t *testing.T) {
|
|
_, err := iamClient.DeleteGroupPolicy(&iam.DeleteGroupPolicyInput{
|
|
GroupName: aws.String(groupName),
|
|
PolicyName: aws.String(policyName),
|
|
})
|
|
require.NoError(t, err)
|
|
})
|
|
|
|
t.Run("list_after_delete", func(t *testing.T) {
|
|
resp, err := iamClient.ListGroupPolicies(&iam.ListGroupPoliciesInput{
|
|
GroupName: aws.String(groupName),
|
|
})
|
|
require.NoError(t, err)
|
|
assert.Empty(t, resp.PolicyNames)
|
|
})
|
|
|
|
t.Run("nonexistent_group", func(t *testing.T) {
|
|
_, err := iamClient.ListGroupPolicies(&iam.ListGroupPoliciesInput{
|
|
GroupName: aws.String("nonexistent-group-for-policies"),
|
|
})
|
|
require.Error(t, err)
|
|
awsErr, ok := err.(awserr.Error)
|
|
require.True(t, ok, "Expected AWS error type")
|
|
assert.Equal(t, iam.ErrCodeNoSuchEntityException, awsErr.Code())
|
|
})
|
|
}
|
|
|
|
// createS3Client creates an S3 client with static credentials
|
|
func createS3Client(t *testing.T, accessKey, secretKey string) *s3.S3 {
|
|
sess, err := session.NewSession(&aws.Config{
|
|
Region: aws.String("us-east-1"),
|
|
Endpoint: aws.String(TestS3Endpoint),
|
|
Credentials: credentials.NewStaticCredentials(accessKey, secretKey, ""),
|
|
DisableSSL: aws.Bool(true),
|
|
S3ForcePathStyle: aws.Bool(true),
|
|
})
|
|
require.NoError(t, err)
|
|
return s3.New(sess)
|
|
}
|