mirror of
https://github.com/seaweedfs/seaweedfs.git
synced 2026-09-19 13:00:45 +02:00
* s3api: fail closed when S3 Tables signature verification fails * s3api: avoid nil Account dereference in S3 Tables auth log * iceberg: return auth error instead of falling back to DefaultAllow * lance: return auth error instead of falling back to DefaultAllow * s3api: stop trusting client-supplied s3-account-id The header is set by the server after successful authentication; scrub inbound values alongside the other internal headers, and apply the same admin guard to the header fallback branch of getAccountID that the identity branch already has. * test: cover table-catalog auth wrappers and principal resolution * test: configure anonymous identity where catalog clients do not sign * s3api: scrub s3-account-id after signature verification
172 lines
5.9 KiB
Go
172 lines
5.9 KiB
Go
package s3api
|
|
|
|
import (
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/aws/aws-sdk-go/aws/credentials"
|
|
v4 "github.com/aws/aws-sdk-go/aws/signer/v4"
|
|
"github.com/seaweedfs/seaweedfs/weed/s3api/iceberg"
|
|
"github.com/seaweedfs/seaweedfs/weed/s3api/lance"
|
|
"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"
|
|
)
|
|
|
|
// setupDefaultAllowAuthServer builds the wiring NewS3ApiServer produces when
|
|
// S3 credentials are configured but no -iam.config file was given: auth is
|
|
// enforced while the IAM policy engine still defaults to allow.
|
|
func setupDefaultAllowAuthServer(t *testing.T) *S3ApiServer {
|
|
t.Helper()
|
|
s3a := setupRoutingTestServer(t)
|
|
manager, err := loadIAMManagerFromConfig("",
|
|
func() string { return "localhost:8888" },
|
|
func() string { return "test-signing-key" })
|
|
require.NoError(t, err)
|
|
require.True(t, manager.DefaultAllow())
|
|
s3a.iam.iamIntegration = NewS3IAMIntegration(manager, "")
|
|
return s3a
|
|
}
|
|
|
|
func forgedS3TablesRequest(t *testing.T, method, target string) *http.Request {
|
|
t.Helper()
|
|
req, err := http.NewRequest(method, "http://localhost"+target, nil)
|
|
require.NoError(t, err)
|
|
req.Header.Set("Authorization", "AWS4-HMAC-SHA256 Credential=AAAA/20260917/us-east-1/s3tables/aws4_request, SignedHeaders=host, Signature=00")
|
|
return req
|
|
}
|
|
|
|
func TestS3TablesAuthRejectsFailedSignature(t *testing.T) {
|
|
s3a := setupDefaultAllowAuthServer(t)
|
|
|
|
req := forgedS3TablesRequest(t, http.MethodGet, "/buckets")
|
|
req.Header.Set(s3_constants.AmzAccountId, "admin")
|
|
|
|
reached := false
|
|
rr := httptest.NewRecorder()
|
|
s3a.authenticateS3Tables(func(w http.ResponseWriter, r *http.Request) {
|
|
reached = true
|
|
w.WriteHeader(http.StatusOK)
|
|
})(rr, req)
|
|
|
|
assert.False(t, reached, "failed authentication must not reach the S3 Tables handler")
|
|
assert.GreaterOrEqual(t, rr.Code, http.StatusBadRequest)
|
|
assert.Empty(t, req.Header.Get(s3_constants.AmzAccountId), "client-supplied account header must not survive authentication")
|
|
}
|
|
|
|
func TestS3TablesAuthControlDataPlane(t *testing.T) {
|
|
s3a := setupDefaultAllowAuthServer(t)
|
|
|
|
req := forgedS3TablesRequest(t, http.MethodGet, "/buckets")
|
|
reached := false
|
|
rr := httptest.NewRecorder()
|
|
s3a.iam.Auth(func(w http.ResponseWriter, r *http.Request) {
|
|
reached = true
|
|
}, s3_constants.ACTION_READ)(rr, req)
|
|
|
|
assert.False(t, reached, "control: data plane must reject the same forged request")
|
|
assert.GreaterOrEqual(t, rr.Code, http.StatusBadRequest)
|
|
}
|
|
|
|
func TestS3TablesAuthOpenWhenAuthDisabled(t *testing.T) {
|
|
s3a := setupDefaultAllowAuthServer(t)
|
|
s3a.iam.isAuthEnabled = false
|
|
|
|
req := forgedS3TablesRequest(t, http.MethodGet, "/buckets")
|
|
reached := false
|
|
rr := httptest.NewRecorder()
|
|
s3a.authenticateS3Tables(func(w http.ResponseWriter, r *http.Request) {
|
|
reached = true
|
|
w.WriteHeader(http.StatusOK)
|
|
})(rr, req)
|
|
|
|
assert.True(t, reached, "zero-config gateway keeps serving unauthenticated requests")
|
|
assert.Equal(t, http.StatusOK, rr.Code)
|
|
}
|
|
|
|
func TestS3TablesAuthSignedRequestPassess(t *testing.T) {
|
|
s3a := setupDefaultAllowAuthServer(t)
|
|
|
|
req, err := http.NewRequest(http.MethodGet, "http://localhost/buckets", nil)
|
|
require.NoError(t, err)
|
|
creds := credentials.NewStaticCredentials(routingTestAccessKey, routingTestSecretKey, "")
|
|
_, err = v4.NewSigner(creds).Sign(req, strings.NewReader(""), "s3tables", "us-east-1", time.Now())
|
|
require.NoError(t, err)
|
|
|
|
reached := false
|
|
rr := httptest.NewRecorder()
|
|
s3a.authenticateS3Tables(func(w http.ResponseWriter, r *http.Request) {
|
|
reached = true
|
|
w.WriteHeader(http.StatusOK)
|
|
})(rr, req)
|
|
|
|
assert.True(t, reached, "properly signed request must still pass; got %d %s", rr.Code, rr.Body.String())
|
|
}
|
|
|
|
func TestSignedAccountHeaderDoesNotReachHandler(t *testing.T) {
|
|
s3a := setupDefaultAllowAuthServer(t)
|
|
|
|
req, err := http.NewRequest(http.MethodGet, "http://localhost/buckets", nil)
|
|
require.NoError(t, err)
|
|
req.Header.Set(s3_constants.AmzAccountId, "admin")
|
|
signRoutingTestRequest(t, req, "", "s3tables")
|
|
require.Contains(t, req.Header.Get("Authorization"), "s3-account-id", "the header must be covered by the signature")
|
|
|
|
reached := false
|
|
rr := httptest.NewRecorder()
|
|
s3a.authenticateS3Tables(func(w http.ResponseWriter, r *http.Request) {
|
|
reached = true
|
|
w.WriteHeader(http.StatusOK)
|
|
})(rr, req)
|
|
|
|
assert.True(t, reached, "a signature covering s3-account-id must still verify; got %d %s", rr.Code, rr.Body.String())
|
|
assert.Empty(t, req.Header.Get(s3_constants.AmzAccountId), "the signed-in header value must not survive authentication")
|
|
}
|
|
|
|
func TestIcebergAuthRejectsFailedSignature(t *testing.T) {
|
|
s3a := setupDefaultAllowAuthServer(t)
|
|
server := iceberg.NewServer(nil, s3a)
|
|
|
|
req := forgedS3TablesRequest(t, http.MethodGet, "/v1/namespaces")
|
|
reached := false
|
|
rr := httptest.NewRecorder()
|
|
server.Auth(func(w http.ResponseWriter, r *http.Request) {
|
|
reached = true
|
|
w.WriteHeader(http.StatusOK)
|
|
})(rr, req)
|
|
|
|
assert.False(t, reached, "failed authentication must not reach the Iceberg handler")
|
|
assert.NotEqual(t, http.StatusOK, rr.Code)
|
|
}
|
|
|
|
func TestLanceAuthRejectsFailedSignature(t *testing.T) {
|
|
s3a := setupDefaultAllowAuthServer(t)
|
|
server := lance.NewServer(nil, s3a)
|
|
|
|
req := forgedS3TablesRequest(t, http.MethodGet, "/v1/namespace/list")
|
|
reached := false
|
|
rr := httptest.NewRecorder()
|
|
server.Auth(func(w http.ResponseWriter, r *http.Request) {
|
|
reached = true
|
|
w.WriteHeader(http.StatusOK)
|
|
})(rr, req)
|
|
|
|
assert.False(t, reached, "failed authentication must not reach the Lance handler")
|
|
assert.NotEqual(t, http.StatusOK, rr.Code)
|
|
}
|
|
|
|
func TestAuthSignatureOnlyScrubsAccountHeader(t *testing.T) {
|
|
s3a := setupDefaultAllowAuthServer(t)
|
|
|
|
req := forgedS3TablesRequest(t, http.MethodGet, "/buckets")
|
|
req.Header.Set(s3_constants.AmzAccountId, "admin")
|
|
|
|
_, errCode := s3a.iam.AuthSignatureOnly(req)
|
|
assert.NotEqual(t, s3err.ErrNone, errCode)
|
|
assert.Empty(t, req.Header.Get(s3_constants.AmzAccountId))
|
|
}
|