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:
Chris Lu authored and GitHub committed 2026-09-13 13:07:10 -07:00
1 parent c462fffce6
commit 10c0857476
6 files changed
+54 -13

No files matched your search

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