mirror of
https://github.com/seaweedfs/seaweedfs.git
synced 2026-08-18 21:26:56 +00:00
* s3tables: resolve account-less identities to a distinct principal Static identities with no account block default to the shared admin account, so getAccountID returned "admin" for every such user and the permission checks treated them all as the admin principal. Only keep the admin account when the identity actually carries an admin action; otherwise fall back to the unique identity name. * s3tables: limit the open-by-default fallback to anonymous access The legacy permission path allowed any request that no policy explicitly denied whenever default-allow was on, which is the zero-config default. That let an authenticated identity without table permissions reach table resources owned by others. Restrict the fallback to requests with no identity or the anonymous identity; authenticated callers must pass an explicit action or policy check. Zero-config and anonymous access are unchanged. * s3tables: drop the no-op ListTableBuckets account gate The top-level check passed the principal as its own owner, so it always allowed. Per-bucket filtering in the loop is the real authority; remove the dead gate and the now-unused locals. * s3tables: derive the Iceberg catalog's default-allow from auth state The Iceberg catalog reuses the S3 Tables Manager, which hardcoded default-allow on. Authenticated callers were enforced only because the identity struct happens to propagate into the handler; if it were ever dropped, a secured catalog would fall open. Mirror the S3 port and set the Manager's default-allow from the authenticator, so an authenticated caller is enforced regardless. Shell and admin keep their own trusted Manager. Regression test covers the struct, name-only, and admin paths. * s3tables: drop redundant ACTION_ADMIN string conversion ACTION_ADMIN is an untyped string constant, so the conversion is a no-op. * s3tables: enforce name-only authenticated callers, add trusted bypass defaultAllowFor treated a request with no identity object as anonymous, but the Manager path forwards only the identity name (not the struct). A name-only authenticated caller could therefore be misclassified as anonymous and allowed under the open default. Treat a server-set identity name as authenticated too, and add an explicit trusted flag for the local shell/admin tooling that legitimately bypasses authorization. * s3tables: trim verbose comments
174 lines
6.6 KiB
Go
174 lines
6.6 KiB
Go
package s3tables
|
|
|
|
import (
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"testing"
|
|
|
|
"github.com/seaweedfs/seaweedfs/weed/s3api/s3_constants"
|
|
"github.com/stretchr/testify/assert"
|
|
)
|
|
|
|
// testIdentity/testIdentityAccount mirror the production identity shape used via reflection.
|
|
// Keep these field names in sync with getAccountID to avoid silent breaks.
|
|
type testIdentityAccount struct {
|
|
Id string
|
|
}
|
|
|
|
type testIdentity struct {
|
|
Name string
|
|
Account *testIdentityAccount
|
|
Actions []string
|
|
Claims map[string]interface{}
|
|
}
|
|
|
|
func TestGetAccountIDPrefersClaimsOverAccountID(t *testing.T) {
|
|
h := NewS3TablesHandler()
|
|
id := &testIdentity{
|
|
Account: &testIdentityAccount{Id: s3_constants.AccountAdminId},
|
|
Claims: map[string]interface{}{
|
|
"preferred_username": "alice",
|
|
"sub": "alice-sub",
|
|
},
|
|
}
|
|
|
|
req := httptest.NewRequest(http.MethodGet, "/", nil)
|
|
req = req.WithContext(s3_constants.SetIdentityInContext(req.Context(), id))
|
|
|
|
got := h.getAccountID(req)
|
|
assert.Equal(t, "alice-sub", got, "expected sub claim to be used before preferred_username")
|
|
assert.NotEqual(t, DefaultAccountID, got, "claims should override default handler account")
|
|
}
|
|
|
|
func TestGetAccountIDUsesSubWhenPreferredUsernameMissing(t *testing.T) {
|
|
h := NewS3TablesHandler()
|
|
id := &testIdentity{
|
|
Account: &testIdentityAccount{Id: s3_constants.AccountAdminId},
|
|
Claims: map[string]interface{}{
|
|
"sub": "user-123",
|
|
},
|
|
}
|
|
|
|
req := httptest.NewRequest(http.MethodGet, "/", nil)
|
|
req = req.WithContext(s3_constants.SetIdentityInContext(req.Context(), id))
|
|
|
|
got := h.getAccountID(req)
|
|
assert.Equal(t, "user-123", got, "expected sub claim to be used when preferred_username missing")
|
|
assert.NotEqual(t, DefaultAccountID, got, "claims should override default handler account")
|
|
}
|
|
|
|
func TestGetAccountIDFallsBackToHandlerDefaultAccount(t *testing.T) {
|
|
h := NewS3TablesHandler()
|
|
req := httptest.NewRequest(http.MethodGet, "/", nil)
|
|
|
|
assert.Equal(t, DefaultAccountID, h.getAccountID(req), "expected handler default account to be returned when no identity is set")
|
|
}
|
|
|
|
func TestGetAccountIDIgnoresEmptyClaimValues(t *testing.T) {
|
|
h := NewS3TablesHandler()
|
|
id := &testIdentity{
|
|
Account: &testIdentityAccount{Id: s3_constants.AccountAdminId},
|
|
Claims: map[string]interface{}{
|
|
"preferred_username": " ",
|
|
"sub": "user-123",
|
|
},
|
|
}
|
|
req := httptest.NewRequest(http.MethodGet, "/", nil)
|
|
req = req.WithContext(s3_constants.SetIdentityInContext(req.Context(), id))
|
|
|
|
assert.Equal(t, "user-123", h.getAccountID(req), "expected whitespace preferred_username to be ignored")
|
|
}
|
|
|
|
func TestGetAccountIDFallsBackToIdentityName(t *testing.T) {
|
|
h := NewS3TablesHandler()
|
|
req := httptest.NewRequest(http.MethodGet, "/", nil)
|
|
req = req.WithContext(s3_constants.SetIdentityNameInContext(req.Context(), "arn:aws:sts::123456789012:assumed-role/S3UserRole/alice-session"))
|
|
|
|
assert.Equal(t, "alice-session", h.getAccountID(req), "expected ARN session suffix to be extracted")
|
|
}
|
|
|
|
func TestGetAccountIDFallsBackToARNColonSegment(t *testing.T) {
|
|
h := NewS3TablesHandler()
|
|
req := httptest.NewRequest(http.MethodGet, "/", nil)
|
|
req = req.WithContext(s3_constants.SetIdentityNameInContext(req.Context(), "arn:aws:iam::123456789012:root"))
|
|
|
|
assert.Equal(t, "root", h.getAccountID(req), "expected ARN colon segment to be returned as principal")
|
|
}
|
|
|
|
func TestGetAccountIDFallsBackToAmzAccountIdHeader(t *testing.T) {
|
|
h := NewS3TablesHandler()
|
|
req := httptest.NewRequest(http.MethodGet, "/", nil)
|
|
req.Header.Set(s3_constants.AmzAccountId, "header-account")
|
|
|
|
assert.Equal(t, "header-account", h.getAccountID(req), "expected header value to be used when no identity is present")
|
|
}
|
|
|
|
func TestGetAccountIDFallsBackToAccountID(t *testing.T) {
|
|
h := NewS3TablesHandler()
|
|
id := &testIdentity{
|
|
Account: &testIdentityAccount{Id: "my-account-id"},
|
|
}
|
|
req := httptest.NewRequest(http.MethodGet, "/", nil)
|
|
req = req.WithContext(s3_constants.SetIdentityInContext(req.Context(), id))
|
|
|
|
assert.Equal(t, "my-account-id", h.getAccountID(req), "expected Account.Id to be returned when claims are missing")
|
|
}
|
|
|
|
func TestGetAccountIDNonAdminDoesNotInheritAdminAccount(t *testing.T) {
|
|
h := NewS3TablesHandler()
|
|
id := &testIdentity{
|
|
Account: &testIdentityAccount{Id: s3_constants.AccountAdminId},
|
|
Actions: []string{"Read", "List"},
|
|
}
|
|
req := httptest.NewRequest(http.MethodGet, "/", nil)
|
|
req = req.WithContext(s3_constants.SetIdentityInContext(req.Context(), id))
|
|
req = req.WithContext(s3_constants.SetIdentityNameInContext(req.Context(), "readonly"))
|
|
|
|
assert.Equal(t, "readonly", h.getAccountID(req), "a non-admin identity must not inherit the shared admin account")
|
|
}
|
|
|
|
func TestGetAccountIDAdminActionKeepsAdminAccount(t *testing.T) {
|
|
h := NewS3TablesHandler()
|
|
id := &testIdentity{
|
|
Account: &testIdentityAccount{Id: s3_constants.AccountAdminId},
|
|
Actions: []string{s3_constants.ACTION_ADMIN},
|
|
}
|
|
req := httptest.NewRequest(http.MethodGet, "/", nil)
|
|
req = req.WithContext(s3_constants.SetIdentityInContext(req.Context(), id))
|
|
req = req.WithContext(s3_constants.SetIdentityNameInContext(req.Context(), "root"))
|
|
|
|
assert.Equal(t, s3_constants.AccountAdminId, h.getAccountID(req), "an admin identity keeps the admin account as principal")
|
|
}
|
|
|
|
func TestDefaultAllowForOnlyAppliesToUnauthenticatedOrAnonymous(t *testing.T) {
|
|
h := NewS3TablesHandler()
|
|
h.SetDefaultAllow(true)
|
|
|
|
noIdentity := httptest.NewRequest(http.MethodGet, "/", nil)
|
|
assert.True(t, h.defaultAllowFor(noIdentity), "zero-config requests with no identity keep the open default")
|
|
|
|
anon := httptest.NewRequest(http.MethodGet, "/", nil)
|
|
anon = anon.WithContext(s3_constants.SetIdentityInContext(anon.Context(),
|
|
&testIdentity{Name: s3_constants.AccountAnonymousId}))
|
|
assert.True(t, h.defaultAllowFor(anon), "anonymous requests keep the open default")
|
|
|
|
authed := httptest.NewRequest(http.MethodGet, "/", nil)
|
|
authed = authed.WithContext(s3_constants.SetIdentityInContext(authed.Context(),
|
|
&testIdentity{Name: "readonly", Account: &testIdentityAccount{Id: s3_constants.AccountAdminId}, Actions: []string{"Read"}}))
|
|
assert.False(t, h.defaultAllowFor(authed), "an authenticated identity must not benefit from the open default")
|
|
|
|
h.SetDefaultAllow(false)
|
|
assert.False(t, h.defaultAllowFor(noIdentity), "default-allow disabled is never open")
|
|
}
|
|
|
|
func TestGetAccountIDNormalizesAccountIDARN(t *testing.T) {
|
|
h := NewS3TablesHandler()
|
|
id := &testIdentity{
|
|
Account: &testIdentityAccount{Id: "arn:aws:iam::123456789012:user/bob"},
|
|
}
|
|
req := httptest.NewRequest(http.MethodGet, "/", nil)
|
|
req = req.WithContext(s3_constants.SetIdentityInContext(req.Context(), id))
|
|
|
|
assert.Equal(t, "bob", h.getAccountID(req), "expected ARN account ID to be normalized to the suffix")
|
|
}
|