mirror of
https://github.com/seaweedfs/seaweedfs.git
synced 2026-09-15 19:10:48 +02:00
s3: gate internal LifecycleDelete gRPC behind admin Bearer auth (#11291)
* s3/lifecycle: attach admin Bearer token on internal LifecycleDelete clients Export credential.WithS3InternalAdminAuth (renamed from withIamCacheAdminAuth) and use it in the worker and shell lifecycle RPC adapters so lifecycle calls carry the same admin token the IAM-cache propagation already attaches. No-op when jwt.filer_signing.key is unset, matching the server-side checkAdminAuth. Prepares the internal clients for the server-side auth gate that follows. * s3/lifecycle: gate LifecycleDelete behind admin Bearer auth Add checkAdminAuth to LifecycleDelete, matching the SeaweedS3IamCache handlers on the same internal gRPC listener (PR #11190). No-op when jwt.filer_signing.key is unset; rejects unauthenticated callers when it is. The internal worker/shell clients already attach the token in the previous commit.
This commit is contained in:
@@ -51,12 +51,12 @@ func (s *PropagatingCredentialStore) SetFilerAddressFunc(getFiler func() pb.Serv
|
||||
}
|
||||
}
|
||||
|
||||
// withIamCacheAdminAuth attaches a Bearer token signed with jwt.filer_signing.key
|
||||
// to the outgoing context so the S3 gateway's IAM-cache gRPC handlers accept the
|
||||
// propagation. With no key configured it is a no-op, matching the S3 handler's
|
||||
// checkAdminAuth. Returns the token's lifetime (0 = no expiry) so callers can
|
||||
// cap any downstream timeout below it.
|
||||
func withIamCacheAdminAuth(ctx context.Context) (context.Context, time.Duration) {
|
||||
// WithS3InternalAdminAuth attaches a Bearer token signed with
|
||||
// jwt.filer_signing.key to the outgoing context so the S3 gateway's internal
|
||||
// gRPC handlers (IAM cache, lifecycle) accept the call. With no key configured
|
||||
// it is a no-op, matching the S3 handler's checkAdminAuth. Returns the token's
|
||||
// lifetime (0 = no expiry) so callers can cap any downstream timeout below it.
|
||||
func WithS3InternalAdminAuth(ctx context.Context) (context.Context, time.Duration) {
|
||||
signingKey := util.GetViper().GetString("jwt.filer_signing.key")
|
||||
if signingKey == "" {
|
||||
return ctx, 0
|
||||
@@ -102,7 +102,7 @@ func (s *PropagatingCredentialStore) propagateChange(ctx context.Context, fn fun
|
||||
// through the token lifetime before the peer fan-out begins. Cap the
|
||||
// propagation deadline below the token's expiry so slower peers don't see
|
||||
// an expired token.
|
||||
authedCtx, tokenTTL := withIamCacheAdminAuth(ctx)
|
||||
authedCtx, tokenTTL := WithS3InternalAdminAuth(ctx)
|
||||
propagateTimeout := 10 * time.Second
|
||||
if tokenTTL > 0 && tokenTTL < propagateTimeout {
|
||||
propagateTimeout = tokenTTL
|
||||
|
||||
@@ -11,9 +11,9 @@ import (
|
||||
"google.golang.org/grpc/metadata"
|
||||
)
|
||||
|
||||
func TestWithIamCacheAdminAuth_NoKey_NoOp(t *testing.T) {
|
||||
func TestWithS3InternalAdminAuth_NoKey_NoOp(t *testing.T) {
|
||||
util.GetViper().Set("jwt.filer_signing.key", "")
|
||||
ctx, ttl := withIamCacheAdminAuth(context.Background())
|
||||
ctx, ttl := WithS3InternalAdminAuth(context.Background())
|
||||
if ttl != 0 {
|
||||
t.Fatalf("expected zero TTL without key, got %v", ttl)
|
||||
}
|
||||
@@ -23,13 +23,13 @@ func TestWithIamCacheAdminAuth_NoKey_NoOp(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestWithIamCacheAdminAuth_WithKey_AttachesBearer(t *testing.T) {
|
||||
func TestWithS3InternalAdminAuth_WithKey_AttachesBearer(t *testing.T) {
|
||||
const k = "propagation-test-signing-key"
|
||||
util.GetViper().Set("jwt.filer_signing.key", k)
|
||||
defer util.GetViper().Set("jwt.filer_signing.key", "")
|
||||
util.GetViper().Set("jwt.filer_signing.expires_after_seconds", 60)
|
||||
|
||||
ctx, ttl := withIamCacheAdminAuth(context.Background())
|
||||
ctx, ttl := WithS3InternalAdminAuth(context.Background())
|
||||
if ttl != 60*time.Second {
|
||||
t.Fatalf("expected 60s TTL, got %v", ttl)
|
||||
}
|
||||
@@ -51,13 +51,13 @@ func TestWithIamCacheAdminAuth_WithKey_AttachesBearer(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestWithIamCacheAdminAuth_ZeroExpiry_ZeroTTL(t *testing.T) {
|
||||
func TestWithS3InternalAdminAuth_ZeroExpiry_ZeroTTL(t *testing.T) {
|
||||
const k = "propagation-test-signing-key"
|
||||
util.GetViper().Set("jwt.filer_signing.key", k)
|
||||
defer util.GetViper().Set("jwt.filer_signing.key", "")
|
||||
util.GetViper().Set("jwt.filer_signing.expires_after_seconds", 0)
|
||||
|
||||
_, ttl := withIamCacheAdminAuth(context.Background())
|
||||
_, ttl := WithS3InternalAdminAuth(context.Background())
|
||||
if ttl != 0 {
|
||||
t.Fatalf("expected zero TTL for no-expiry token, got %v", ttl)
|
||||
}
|
||||
|
||||
@@ -20,6 +20,9 @@ import (
|
||||
// CAS, object-lock check, dispatch by kind. Errors surface as outcomes;
|
||||
// reader cursors and pending state are the worker's concern.
|
||||
func (s3a *S3ApiServer) LifecycleDelete(ctx context.Context, req *s3_lifecycle_pb.LifecycleDeleteRequest) (*s3_lifecycle_pb.LifecycleDeleteResponse, error) {
|
||||
if err := s3a.checkAdminAuth(ctx); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if req == nil || req.Bucket == "" || req.ObjectPath == "" {
|
||||
return blocked("FATAL_EVENT_ERROR: empty bucket or object_path"), nil
|
||||
}
|
||||
|
||||
@@ -2,13 +2,17 @@ package s3api
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/prometheus/client_golang/prometheus/testutil"
|
||||
"github.com/seaweedfs/seaweedfs/weed/pb/filer_pb"
|
||||
"github.com/seaweedfs/seaweedfs/weed/pb/s3_lifecycle_pb"
|
||||
"github.com/seaweedfs/seaweedfs/weed/s3api/s3lifecycle"
|
||||
"github.com/seaweedfs/seaweedfs/weed/security"
|
||||
stats_collect "github.com/seaweedfs/seaweedfs/weed/stats"
|
||||
"google.golang.org/grpc/codes"
|
||||
"google.golang.org/grpc/status"
|
||||
)
|
||||
|
||||
func TestComputeEntryIdentity_BasicFields(t *testing.T) {
|
||||
@@ -329,3 +333,33 @@ func TestRecordMetadataOnlyIf_EmptyRuleHashCollapsesToEmptyLabel(t *testing.T) {
|
||||
t.Fatalf("nil rule_hash should produce empty-label series; before=%v after=%v", before, got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLifecycleDelete_RequiresAdminAuth(t *testing.T) {
|
||||
s := newTestS3IamCacheServer(t, testS3IamCacheSigningKey)
|
||||
_, err := s.LifecycleDelete(context.Background(), &s3_lifecycle_pb.LifecycleDeleteRequest{
|
||||
Bucket: "victim", ObjectPath: "important.dat",
|
||||
ActionKind: s3_lifecycle_pb.ActionKind_EXPIRATION_DAYS,
|
||||
})
|
||||
if got, want := status.Code(err), codes.Unauthenticated; got != want {
|
||||
t.Fatalf("LifecycleDelete without token: got code %v, want %v (err=%v)", got, want, err)
|
||||
}
|
||||
bad := security.GenJwtForFilerAdmin(security.SigningKey("a-different-key"), 60)
|
||||
_, err = s.LifecycleDelete(s3IamCacheBearerCtx(string(bad)), &s3_lifecycle_pb.LifecycleDeleteRequest{
|
||||
Bucket: "victim", ObjectPath: "important.dat",
|
||||
ActionKind: s3_lifecycle_pb.ActionKind_EXPIRATION_DAYS,
|
||||
})
|
||||
if got, want := status.Code(err), codes.Unauthenticated; got != want {
|
||||
t.Fatalf("LifecycleDelete with mis-signed token: got code %v, want %v (err=%v)", got, want, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLifecycleDelete_NoSigningKey_Allowed(t *testing.T) {
|
||||
s := newTestS3IamCacheServer(t, "")
|
||||
resp, err := s.LifecycleDelete(context.Background(), &s3_lifecycle_pb.LifecycleDeleteRequest{})
|
||||
if err != nil {
|
||||
t.Fatalf("LifecycleDelete without signing key: unexpected error %v", err)
|
||||
}
|
||||
if resp == nil || resp.Outcome != s3_lifecycle_pb.LifecycleDeleteOutcome_BLOCKED {
|
||||
t.Fatalf("expected BLOCKED for empty request without signing key, got %v", resp)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/seaweedfs/seaweedfs/weed/credential"
|
||||
"github.com/seaweedfs/seaweedfs/weed/pb"
|
||||
"github.com/seaweedfs/seaweedfs/weed/pb/filer_pb"
|
||||
"github.com/seaweedfs/seaweedfs/weed/pb/s3_lifecycle_pb"
|
||||
@@ -314,6 +315,7 @@ type lifecycleClientCallable struct {
|
||||
}
|
||||
|
||||
func (l *lifecycleClientCallable) LifecycleDelete(ctx context.Context, req *s3_lifecycle_pb.LifecycleDeleteRequest) (*s3_lifecycle_pb.LifecycleDeleteResponse, error) {
|
||||
ctx, _ = credential.WithS3InternalAdminAuth(ctx)
|
||||
return l.c.LifecycleDelete(ctx, req)
|
||||
}
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/seaweedfs/seaweedfs/weed/credential"
|
||||
"github.com/seaweedfs/seaweedfs/weed/glog"
|
||||
"github.com/seaweedfs/seaweedfs/weed/pb"
|
||||
"github.com/seaweedfs/seaweedfs/weed/pb/filer_pb"
|
||||
@@ -423,6 +424,7 @@ type lifecycleRPCAdapter struct {
|
||||
}
|
||||
|
||||
func (a lifecycleRPCAdapter) LifecycleDelete(ctx context.Context, req *s3_lifecycle_pb.LifecycleDeleteRequest) (*s3_lifecycle_pb.LifecycleDeleteResponse, error) {
|
||||
ctx, _ = credential.WithS3InternalAdminAuth(ctx)
|
||||
return a.c.LifecycleDelete(ctx, req)
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user