diff --git a/test/s3tables/table-buckets/s3tables_integration_test.go b/test/s3tables/table-buckets/s3tables_integration_test.go index 3512dc4d5..753012283 100644 --- a/test/s3tables/table-buckets/s3tables_integration_test.go +++ b/test/s3tables/table-buckets/s3tables_integration_test.go @@ -70,6 +70,92 @@ func TestS3TablesIntegration(t *testing.T) { }) } +func TestS3TablesCreateBucketIAMPolicy(t *testing.T) { + if testing.Short() { + t.Skip("Skipping IAM integration test in short mode") + } + + t.Setenv("AWS_ACCESS_KEY_ID", "env-admin") + t.Setenv("AWS_SECRET_ACCESS_KEY", "env-secret") + + allowedBucket := "tables-allowed" + deniedBucket := "tables-denied" + iamConfigDir := t.TempDir() + iamConfigPath := filepath.Join(iamConfigDir, "iam_config.json") + iamConfig := fmt.Sprintf(`{ + "sts": { + "tokenDuration": "1h", + "maxSessionLength": "12h", + "issuer": "seaweedfs-sts", + "signingKey": "%s" + }, + "accounts": [ + { + "id": "%s", + "displayName": "tables-integration" + } + ], + "identities": [ + { + "name": "admin", + "credentials": [ + { + "accessKey": "%s", + "secretKey": "%s" + } + ], + "account": { + "id": "%s", + "displayName": "tables-integration" + }, + "policyNames": ["S3TablesBucketPolicy"] + } + ], + "policy": { + "defaultEffect": "Deny", + "storeType": "memory" + }, + "policies": [ + { + "name": "S3TablesBucketPolicy", + "document": { + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "Action": ["s3tables:CreateTableBucket"], + "Resource": [ + "arn:aws:s3tables:%s:%s:bucket/%s", + "arn:aws:s3:::%s" + ] + } + ] + } + } + ] +}`, testIAMSigningKey, testAccountID, testAccessKey, testSecretKey, testAccountID, testRegion, testAccountID, allowedBucket, allowedBucket) + require.NoError(t, os.WriteFile(iamConfigPath, []byte(iamConfig), 0644)) + + cluster, err := startMiniClusterWithExtraArgs(t, []string{ + "-s3.config=" + iamConfigPath, + "-s3.iam.config=" + iamConfigPath, + }) + require.NoError(t, err, "failed to start cluster with IAM config") + defer cluster.Stop() + + client := NewS3TablesClient(cluster.s3Endpoint, testRegion, testAccessKey, testSecretKey) + + _, err = client.CreateTableBucket(deniedBucket, nil) + require.Error(t, err, "denied bucket creation should fail") + assert.Contains(t, err.Error(), "AccessDenied") + + allowedResp, err := client.CreateTableBucket(allowedBucket, nil) + require.NoError(t, err, "allowed bucket creation should succeed") + defer func() { + _ = client.DeleteTableBucket(allowedResp.ARN) + }() +} + func testTableBucketLifecycle(t *testing.T, client *S3TablesClient) { bucketName := "test-bucket-" + randomString(8) @@ -509,7 +595,7 @@ func findAvailablePorts(n int) ([]int, error) { } // startMiniCluster starts a weed mini instance directly without exec -func startMiniCluster(t *testing.T) (*TestCluster, error) { +func startMiniClusterWithExtraArgs(t *testing.T, extraArgs []string) (*TestCluster, error) { // Find available ports // We need 8 unique ports: Master(2), Volume(2), Filer(2), S3(2) ports, err := findAvailablePorts(8) @@ -585,8 +671,7 @@ func startMiniCluster(t *testing.T) (*TestCluster, error) { os.Chdir(testDir) // Configure args for mini command - os.Args = []string{ - "weed", + baseArgs := []string{ "-dir=" + testDir, "-master.port=" + strconv.Itoa(masterPort), "-master.port.grpc=" + strconv.Itoa(masterGrpcPort), @@ -603,6 +688,10 @@ func startMiniCluster(t *testing.T) (*TestCluster, error) { "-master.peers=none", // Faster startup "-s3.iam.readOnly=false", // Enable IAM write operations for tests } + if len(extraArgs) > 0 { + baseArgs = append(baseArgs, extraArgs...) + } + os.Args = append([]string{"weed"}, baseArgs...) // Suppress most logging during tests glog.MaxSize = 1024 * 1024 @@ -633,6 +722,10 @@ func startMiniCluster(t *testing.T) (*TestCluster, error) { return cluster, nil } +func startMiniCluster(t *testing.T) (*TestCluster, error) { + return startMiniClusterWithExtraArgs(t, nil) +} + // Stop stops the test cluster func (c *TestCluster) Stop() { if c.cancel != nil { diff --git a/test/s3tables/table-buckets/setup.go b/test/s3tables/table-buckets/setup.go index 2d0c9a3c0..d4810fad5 100644 --- a/test/s3tables/table-buckets/setup.go +++ b/test/s3tables/table-buckets/setup.go @@ -46,8 +46,9 @@ func NewS3TablesClient(endpoint, region, accessKey, secretKey string) *S3TablesC // Test configuration constants const ( - testRegion = "us-west-2" - testAccessKey = "admin" - testSecretKey = "admin" - testAccountID = "111122223333" + testRegion = "us-west-2" + testAccessKey = "admin" + testSecretKey = "admin" + testAccountID = "111122223333" + testIAMSigningKey = "dGVzdC1zaWduaW5nLWtleS1mb3Itc3RzLWludGVncmF0aW9uLXRlc3Rz" ) diff --git a/weed/s3api/s3tables/handler_bucket_create.go b/weed/s3api/s3tables/handler_bucket_create.go index 0251d989a..b2791448a 100644 --- a/weed/s3api/s3tables/handler_bucket_create.go +++ b/weed/s3api/s3tables/handler_bucket_create.go @@ -28,11 +28,12 @@ func (h *S3TablesHandler) handleCreateTableBucket(w http.ResponseWriter, r *http principal := h.getAccountID(r) identityActions := getIdentityActions(r) - if h.shouldUseIAM(r, identityActions) && !h.defaultAllow { + identityPolicyNames := getIdentityPolicyNames(r) + if h.shouldUseIAM(r, identityActions, identityPolicyNames) && !h.defaultAllow { ownerAccountID := h.getAccountID(r) tableBucketARN := h.generateTableBucketARN(ownerAccountID, req.Name) s3BucketARN := fmt.Sprintf("arn:aws:s3:::%s", req.Name) - allowed, err := h.authorizeIAMAction(r, "s3tables:CreateTableBucket", tableBucketARN, s3BucketARN) + allowed, err := h.authorizeIAMAction(r, identityPolicyNames, "s3tables:CreateTableBucket", tableBucketARN, s3BucketARN) if err != nil || !allowed { h.writeError(w, http.StatusForbidden, ErrCodeAccessDenied, "not authorized to create table buckets") return NewAuthError("CreateTableBucket", principal, "not authorized to create table buckets") diff --git a/weed/s3api/s3tables/iam.go b/weed/s3api/s3tables/iam.go index 008a00859..9a0be69dd 100644 --- a/weed/s3api/s3tables/iam.go +++ b/weed/s3api/s3tables/iam.go @@ -22,14 +22,17 @@ func (h *S3TablesHandler) SetIAMAuthorizer(authorizer IAMAuthorizer) { h.iamAuthorizer = authorizer } -func (h *S3TablesHandler) shouldUseIAM(r *http.Request, identityActions []string) bool { +func (h *S3TablesHandler) shouldUseIAM(r *http.Request, identityActions, identityPolicyNames []string) bool { if h.iamAuthorizer == nil || r == nil { return false } if hasSessionToken(r) { return true } - return len(identityActions) == 0 + if len(identityActions) == 0 { + return true + } + return len(identityPolicyNames) > 0 } func hasSessionToken(r *http.Request) bool { @@ -42,7 +45,7 @@ func hasSessionToken(r *http.Request) bool { return r.URL.Query().Get("X-Amz-Security-Token") != "" } -func (h *S3TablesHandler) authorizeIAMAction(r *http.Request, action string, resources ...string) (bool, error) { +func (h *S3TablesHandler) authorizeIAMAction(r *http.Request, identityPolicyNames []string, action string, resources ...string) (bool, error) { if h.iamAuthorizer == nil { return false, nil } @@ -67,7 +70,10 @@ func (h *S3TablesHandler) authorizeIAMAction(r *http.Request, action string, res } requestContext := buildIAMRequestContext(r, getIdentityClaims(r)) - policyNames := getIdentityPolicyNames(r) + policyNames := identityPolicyNames + if len(policyNames) == 0 { + policyNames = getIdentityPolicyNames(r) + } var lastErr error for _, resource := range resources {