diff --git a/weed/s3api/policy_engine/types.go b/weed/s3api/policy_engine/types.go index 0af425de4..006f264bf 100644 --- a/weed/s3api/policy_engine/types.go +++ b/weed/s3api/policy_engine/types.go @@ -30,11 +30,29 @@ import ( const ( // PolicyVersion2012_10_17 is the standard AWS policy version PolicyVersion2012_10_17 = "2012-10-17" + + // S3 multipart upload actions that are implicitly granted with s3:PutObject + // Multipart upload is an implementation detail of putting objects, not a separate permission + s3CreateMultipartUpload = "s3:CreateMultipartUpload" + s3UploadPart = "s3:UploadPart" + s3CompleteMultipartUpload = "s3:CompleteMultipartUpload" + s3AbortMultipartUpload = "s3:AbortMultipartUpload" + s3ListParts = "s3:ListParts" ) var ( // PolicyVariableRegex detects AWS IAM policy variables like ${aws:username} PolicyVariableRegex = regexp.MustCompile(`\$\{([^}]+)\}`) + + // multipartActionSet contains all S3 multipart upload actions + // These are treated as equivalent to s3:PutObject for authorization purposes + multipartActionSet = map[string]bool{ + s3CreateMultipartUpload: true, + s3UploadPart: true, + s3CompleteMultipartUpload: true, + s3AbortMultipartUpload: true, + s3ListParts: true, + } ) // StringOrStringSlice represents a value that can be either a string or []string @@ -497,13 +515,27 @@ var S3Actions = map[string]string{ "BypassGovernanceRetention": "s3:BypassGovernanceRetention", } -// MatchesAction checks if an action matches any of the compiled action matchers +// MatchesAction checks if an action matches any of the compiled action matchers. +// It also implicitly grants multipart upload actions if s3:PutObject is allowed, +// since multipart upload is an implementation detail of putting objects. func (cs *CompiledStatement) MatchesAction(action string) bool { for _, matcher := range cs.ActionMatchers { if matcher.Match(action) { return true } } + + // Multipart upload operations are part of s3:PutObject permission + // If s3:PutObject is allowed, implicitly allow multipart operations + if multipartActionSet[action] { + // Check if s3:PutObject is explicitly allowed + for _, matcher := range cs.ActionMatchers { + if matcher.Match("s3:PutObject") { + return true + } + } + } + return false } diff --git a/weed/s3api/s3api_embedded_iam.go b/weed/s3api/s3api_embedded_iam.go index f18114ad1..9085b2b2e 100644 --- a/weed/s3api/s3api_embedded_iam.go +++ b/weed/s3api/s3api_embedded_iam.go @@ -51,6 +51,19 @@ func NewEmbeddedIamApi(credentialManager *credential.CredentialManager, iam *Ide } } +func (e *EmbeddedIamApi) refreshIAMConfiguration() error { + if e.reloadConfigurationFunc != nil { + return e.reloadConfigurationFunc() + } + if e.iam == nil { + return nil + } + if err := e.iam.LoadS3ApiConfigurationFromCredentialManager(); err != nil { + return fmt.Errorf("failed to refresh IAM configuration: %w", err) + } + return nil +} + // Constants for service account identifiers const ( ServiceAccountIDLength = 12 // Length of the service account ID @@ -903,6 +916,11 @@ func (e *EmbeddedIamApi) AttachUserPolicy(ctx context.Context, values url.Values return resp, &iamError{Code: iam.ErrCodeServiceFailureException, Error: err} } + // Best-effort refresh: log any failures but don't fail the API call since the mutation succeeded + if err := e.refreshIAMConfiguration(); err != nil { + glog.Warningf("Failed to refresh IAM configuration after attaching policy %s to user %s: %v", policyName, userName, err) + } + return resp, nil } @@ -943,6 +961,11 @@ func (e *EmbeddedIamApi) DetachUserPolicy(ctx context.Context, values url.Values return resp, &iamError{Code: iam.ErrCodeServiceFailureException, Error: err} } + // Best-effort refresh: log any failures but don't fail the API call since the mutation succeeded + if err := e.refreshIAMConfiguration(); err != nil { + glog.Warningf("Failed to refresh IAM configuration after detaching policy %s from user %s: %v", policyName, userName, err) + } + return resp, nil } diff --git a/weed/s3api/s3api_embedded_iam_test.go b/weed/s3api/s3api_embedded_iam_test.go index 906895f6d..1c0d351f8 100644 --- a/weed/s3api/s3api_embedded_iam_test.go +++ b/weed/s3api/s3api_embedded_iam_test.go @@ -20,9 +20,11 @@ import ( "github.com/seaweedfs/seaweedfs/weed/credential" "github.com/seaweedfs/seaweedfs/weed/credential/memory" "github.com/seaweedfs/seaweedfs/weed/pb/iam_pb" + "github.com/seaweedfs/seaweedfs/weed/s3api/policy_engine" . "github.com/seaweedfs/seaweedfs/weed/s3api/s3_constants" "github.com/seaweedfs/seaweedfs/weed/s3api/s3err" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" "google.golang.org/protobuf/proto" ) @@ -468,6 +470,46 @@ func TestEmbeddedIamAttachUserPolicy(t *testing.T) { assert.Equal(t, []string{"TestManagedPolicy"}, api.mockConfig.Identities[0].PolicyNames) } +func TestEmbeddedIamAttachUserPolicyRefreshesIAM(t *testing.T) { + api := NewEmbeddedIamApiForTest() + ctx := context.Background() + cm := api.credentialManager + user := &iam_pb.Identity{ + Name: "policyRefreshUser", + Credentials: []*iam_pb.Credential{ + {AccessKey: "REFRESHACCESS", SecretKey: "REFRESHSECRET"}, + }, + } + require.NoError(t, cm.CreateUser(ctx, user)) + policy := policy_engine.PolicyDocument{ + Version: policy_engine.PolicyVersion2012_10_17, + Statement: []policy_engine.PolicyStatement{ + { + Effect: policy_engine.PolicyEffectAllow, + Action: policy_engine.NewStringOrStringSlice("s3:GetObject"), + Resource: policy_engine.NewStringOrStringSlice("arn:aws:s3:::bucket/*"), + }, + }, + } + require.NoError(t, cm.PutPolicy(ctx, "RefreshPolicy", policy)) + require.NoError(t, api.iam.LoadS3ApiConfigurationFromCredentialManager()) + + identity := api.iam.lookupByIdentityName("policyRefreshUser") + require.NotNil(t, identity) + assert.Empty(t, identity.PolicyNames) + + values := url.Values{} + values.Set("UserName", "policyRefreshUser") + values.Set("PolicyArn", "arn:aws:iam:::policy/RefreshPolicy") + + _, iamErr := api.AttachUserPolicy(ctx, values) + require.Nil(t, iamErr) + + identity = api.iam.lookupByIdentityName("policyRefreshUser") + require.NotNil(t, identity) + assert.Equal(t, []string{"RefreshPolicy"}, identity.PolicyNames) +} + // TestEmbeddedIamAttachUserPolicyNoSuchPolicy tests attach failure when managed policy does not exist. func TestEmbeddedIamAttachUserPolicyNoSuchPolicy(t *testing.T) { api := NewEmbeddedIamApiForTest()