Files
seaweedfs/weed/s3api/s3api_tables_auth_test.go
T
Chris Lu 0ca1c19821 s3api: unify auth error handling across s3tables, iceberg and lance (#11381)
* 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
2026-09-18 01:01:04 -07:00

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))
}