mirror of
https://github.com/seaweedfs/seaweedfs.git
synced 2026-06-13 23:36:45 +03:00
cce98fcecf
* fix(s3): strip client-supplied X-SeaweedFS-Principal/Session-Token in AuthSignatureOnly AuthSignatureOnly is the only auth gate in front of S3Tables routes (incl. CreateTableBucket) and UnifiedPostHandler, but unlike authenticateRequestInternal it did not clear the internal IAM trust headers before running signature verification. S3Tables authorizeIAMAction reads X-SeaweedFS-Principal directly from the request and prefers it over the authenticated identity's PrincipalArn, so a signed low-privilege caller could append that header after signing (unsigned header, SigV4 still verifies) and have IAM policy evaluated against a spoofed principal, bypassing authorization. Clear both X-SeaweedFS-Principal and X-SeaweedFS-Session-Token at the top of AuthSignatureOnly, mirroring the existing guard in authenticateRequestInternal. Add a regression test covering the header-injection path. * refactor(s3): route AuthSignatureOnly through authenticateRequestInternal Addresses review feedback: both entry points were independently maintaining the internal-IAM-header stripping and the auth-type dispatch switch. Collapse AuthSignatureOnly into a thin wrapper around authenticateRequestInternal so the security-critical header scrub and the signature-verify switch live in one place. Post-auth behavior unique to AuthSignatureOnly (AmzAccountId header) stays inline. No functional change beyond two harmless telemetry tweaks that now match authenticateRequestInternal: the per-branch glog verbosity shifts from V(3) to V(4), and the anonymous-found path now sets AmzAuthType. * refactor(s3): centralize X-SeaweedFS-Principal/Session-Token header names Introduce SeaweedFSPrincipalHeader and SeaweedFSSessionTokenHeader in weed/s3api/s3_constants so the trust-header literals are defined once and referenced consistently by the auth scrub, JWT auth path, bucket policy principal resolution, IAM authorization, and S3Tables IAM evaluation. Replace every remaining usage in weed/s3api and weed/s3api/s3tables. This removes the drift risk the reviewer called out: adding another call site with a typo can no longer silently bypass the scrub. Pure rename, no behavior change. No-op integration-test helper in test/s3/iam/s3_iam_framework.go left untouched (separate module, and the server now strips the client-supplied value regardless).
150 lines
5.9 KiB
Go
150 lines
5.9 KiB
Go
package s3api
|
|
|
|
import (
|
|
"net/http"
|
|
"net/url"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
|
|
"github.com/seaweedfs/seaweedfs/weed/iam/sts"
|
|
"github.com/seaweedfs/seaweedfs/weed/s3api/s3_constants"
|
|
)
|
|
|
|
// TestAuthorizeWithIAMSessionTokenExtraction tests that the authorizeWithIAM function
|
|
// correctly extracts session tokens from multiple sources and prioritizes them appropriately.
|
|
// This is a regression test for the bug where X-Amz-Security-Token was not being checked
|
|
// for V4 signature authentication with STS credentials.
|
|
func TestAuthorizeWithIAMSessionTokenExtraction(t *testing.T) {
|
|
t.Run("Extracts X-SeaweedFS-Session-Token from JWT auth", func(t *testing.T) {
|
|
req := &http.Request{
|
|
Header: http.Header{
|
|
"X-Seaweedfs-Session-Token": {"jwt-token-123"},
|
|
"X-Seaweedfs-Principal": {"arn:aws:iam::user/test"},
|
|
},
|
|
URL: &url.URL{},
|
|
}
|
|
|
|
// Extract tokens the same way authorizeWithIAM does
|
|
sessionToken := req.Header.Get(s3_constants.SeaweedFSSessionTokenHeader)
|
|
principal := req.Header.Get(s3_constants.SeaweedFSPrincipalHeader)
|
|
|
|
assert.Equal(t, "jwt-token-123", sessionToken, "Should extract JWT session token from header")
|
|
assert.Equal(t, "arn:aws:iam::user/test", principal, "Should extract principal from header")
|
|
})
|
|
|
|
t.Run("Extracts X-Amz-Security-Token from V4 STS auth header", func(t *testing.T) {
|
|
req := &http.Request{
|
|
Header: http.Header{
|
|
"X-Amz-Security-Token": {"sts-token-header-456"},
|
|
},
|
|
URL: &url.URL{},
|
|
}
|
|
|
|
// Extract tokens the same way authorizeWithIAM does
|
|
sessionToken := req.Header.Get(s3_constants.SeaweedFSSessionTokenHeader)
|
|
principal := req.Header.Get(s3_constants.SeaweedFSPrincipalHeader)
|
|
|
|
// If JWT token is empty, should fallback to X-Amz-Security-Token
|
|
if sessionToken == "" {
|
|
sessionToken = req.Header.Get("X-Amz-Security-Token")
|
|
}
|
|
|
|
assert.Equal(t, "sts-token-header-456", sessionToken, "Should fallback to X-Amz-Security-Token when JWT token is empty")
|
|
assert.Empty(t, principal, "JWT principal should be empty for V4 auth")
|
|
})
|
|
|
|
t.Run("Extracts X-Amz-Security-Token from query parameter (presigned URL)", func(t *testing.T) {
|
|
req := &http.Request{
|
|
Header: http.Header{},
|
|
URL: &url.URL{RawQuery: "X-Amz-Security-Token=sts-token-query-789"},
|
|
}
|
|
|
|
// Extract tokens the same way authorizeWithIAM does
|
|
sessionToken := req.Header.Get(s3_constants.SeaweedFSSessionTokenHeader)
|
|
if sessionToken == "" {
|
|
sessionToken = req.Header.Get("X-Amz-Security-Token")
|
|
if sessionToken == "" {
|
|
sessionToken = req.URL.Query().Get("X-Amz-Security-Token")
|
|
}
|
|
}
|
|
|
|
assert.Equal(t, "sts-token-query-789", sessionToken, "Should extract token from query parameter")
|
|
})
|
|
|
|
t.Run("JWT token takes precedence over X-Amz-Security-Token", func(t *testing.T) {
|
|
req := &http.Request{
|
|
Header: http.Header{
|
|
"X-Seaweedfs-Session-Token": {"jwt-preferred"},
|
|
"X-Seaweedfs-Principal": {"arn:aws:iam::user/jwt-user"},
|
|
"X-Amz-Security-Token": {"sts-fallback"},
|
|
},
|
|
URL: &url.URL{},
|
|
}
|
|
|
|
// Extract tokens the same way authorizeWithIAM does
|
|
sessionToken := req.Header.Get(s3_constants.SeaweedFSSessionTokenHeader)
|
|
if sessionToken == "" {
|
|
sessionToken = req.Header.Get("X-Amz-Security-Token")
|
|
}
|
|
|
|
assert.Equal(t, "jwt-preferred", sessionToken, "JWT token should take precedence")
|
|
})
|
|
}
|
|
|
|
// TestSTSSessionTokenIntoCredentials verifies that STS session tokens are properly
|
|
// preserved when converting to credentials for authorization.
|
|
func TestSTSSessionTokenIntoCredentials(t *testing.T) {
|
|
// Create a credential generator and session claims
|
|
credGen := sts.NewCredentialGenerator()
|
|
sessionId := "test-session-123"
|
|
expiresAt := time.Now().Add(time.Hour)
|
|
|
|
// Generate temporary credentials
|
|
creds, err := credGen.GenerateTemporaryCredentials(sessionId, expiresAt)
|
|
require.NoError(t, err, "Should generate credentials successfully")
|
|
require.NotNil(t, creds, "Credentials should not be nil")
|
|
|
|
// Verify all credential fields are present
|
|
assert.NotEmpty(t, creds.AccessKeyId, "AccessKeyId should be present")
|
|
assert.NotEmpty(t, creds.SecretAccessKey, "SecretAccessKey should be present")
|
|
assert.NotEmpty(t, creds.SessionToken, "SessionToken should be present for STS")
|
|
|
|
// Verify deterministic generation (same session ID produces same credentials)
|
|
creds2, err := credGen.GenerateTemporaryCredentials(sessionId, expiresAt)
|
|
require.NoError(t, err)
|
|
|
|
assert.Equal(t, creds.AccessKeyId, creds2.AccessKeyId, "AccessKeyId should be deterministic")
|
|
assert.Equal(t, creds.SecretAccessKey, creds2.SecretAccessKey, "SecretAccessKey should be deterministic")
|
|
assert.Equal(t, creds.SessionToken, creds2.SessionToken, "SessionToken should be deterministic for same sessionId")
|
|
|
|
// Verify different session produces different credentials
|
|
creds3, err := credGen.GenerateTemporaryCredentials("different-session", expiresAt)
|
|
require.NoError(t, err)
|
|
|
|
assert.NotEqual(t, creds.AccessKeyId, creds3.AccessKeyId, "Different sessions should produce different access key IDs")
|
|
assert.NotEqual(t, creds.SecretAccessKey, creds3.SecretAccessKey, "Different sessions should produce different secret keys")
|
|
assert.NotEqual(t, creds.SessionToken, creds3.SessionToken, "Different sessions should produce different session tokens")
|
|
}
|
|
|
|
// TestActionConstantsForV4Auth verifies that action constants are properly available
|
|
// for use in authorization checks with V4 signature authentication.
|
|
func TestActionConstantsForV4Auth(t *testing.T) {
|
|
// Verify that S3 action constants are available
|
|
actions := map[string]string{
|
|
"READ": s3_constants.ACTION_READ,
|
|
"WRITE": s3_constants.ACTION_WRITE,
|
|
"READ_ACP": s3_constants.ACTION_READ_ACP,
|
|
"WRITE_ACP": s3_constants.ACTION_WRITE_ACP,
|
|
"LIST": s3_constants.ACTION_LIST,
|
|
"TAGGING": s3_constants.ACTION_TAGGING,
|
|
"ADMIN": s3_constants.ACTION_ADMIN,
|
|
}
|
|
|
|
for name, action := range actions {
|
|
assert.NotEmpty(t, action, "Action %s should not be empty", name)
|
|
}
|
|
}
|