mirror of
https://github.com/seaweedfs/seaweedfs.git
synced 2026-11-07 20:47:46 +01:00
* fix(s3): accept fresh admin tokens when context carries stale authorization (#12390) Signed-off-by: Mohd Quamar Tyagi <104281681+Tyagiquamar@users.noreply.github.com> * security: share the admin Bearer check between S3 and admin-plane peers Move the authorization scan into security.CheckGrpcAdminToken, built on IsFilerAdminToken, and call it from CheckGrpcAdminPeer and the S3 IAM cache check. Both now accept a valid admin token in any authorization value instead of S3 re-implementing the decode and claim checks. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * filer: accept a fresh admin token after a stale one on IAM gRPC The S3 IAM cache check is meant to mirror the filer's, so route the filer's checkAdminAuth through security.CheckGrpcAdminToken too. IamGrpcStore appends its token to the caller's context, so a forwarded older token would otherwise still shadow it here. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * security: parse at most four authorization values per admin call Scanning every value let one request repeat a junk Bearer token (cheap over HPACK) tens of thousands of times and burn about half a second of JWT parsing on an unauthenticated port. Real callers send their own token plus at most one forwarded from their context. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * credential: match the other WithS3InternalAdminAuth replace fix The plugin worker fix makes the same set-instead-of-append change to WithS3InternalAdminAuth with different code and a different test at the same spot, so whichever lands second would conflict. Use the identical code and test so the two merge cleanly in either order. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * security: test an expired admin token ahead of a fresh one Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * security: test that a same-key session token is not an admin token Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * filer: test IAM gRPC with only invalid authorization values Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Signed-off-by: Mohd Quamar Tyagi <104281681+Tyagiquamar@users.noreply.github.com> Co-authored-by: Chris Lu <chrislusf@users.noreply.github.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
183 lines
7.2 KiB
Go
183 lines
7.2 KiB
Go
package weed_server
|
|
|
|
import (
|
|
"context"
|
|
"testing"
|
|
"time"
|
|
|
|
jwt "github.com/golang-jwt/jwt/v5"
|
|
"github.com/seaweedfs/seaweedfs/weed/credential"
|
|
_ "github.com/seaweedfs/seaweedfs/weed/credential/memory"
|
|
"github.com/seaweedfs/seaweedfs/weed/pb/iam_pb"
|
|
"github.com/seaweedfs/seaweedfs/weed/security"
|
|
"google.golang.org/grpc/codes"
|
|
"google.golang.org/grpc/metadata"
|
|
"google.golang.org/grpc/status"
|
|
)
|
|
|
|
const testIamSigningKey = "iam-admin-test-key-do-not-use-in-prod"
|
|
|
|
func newTestIamGrpcServer(t *testing.T) *IamGrpcServer {
|
|
t.Helper()
|
|
cm, err := credential.NewCredentialManager(credential.StoreTypeMemory, nil, "")
|
|
if err != nil {
|
|
t.Fatalf("NewCredentialManager: %v", err)
|
|
}
|
|
return NewIamGrpcServer(cm, security.SigningKey(testIamSigningKey))
|
|
}
|
|
|
|
func ctxWithBearer(token string) context.Context {
|
|
md := metadata.New(map[string]string{"authorization": "Bearer " + token})
|
|
return metadata.NewIncomingContext(context.Background(), md)
|
|
}
|
|
|
|
func TestIamGrpc_NoMetadata_Unauthenticated(t *testing.T) {
|
|
s := newTestIamGrpcServer(t)
|
|
_, err := s.ListUsers(context.Background(), &iam_pb.ListUsersRequest{})
|
|
if got, want := status.Code(err), codes.Unauthenticated; got != want {
|
|
t.Fatalf("ListUsers without metadata: got code %v, want %v (err=%v)", got, want, err)
|
|
}
|
|
}
|
|
|
|
func TestIamGrpc_MissingAuthorizationHeader_Unauthenticated(t *testing.T) {
|
|
s := newTestIamGrpcServer(t)
|
|
ctx := metadata.NewIncomingContext(context.Background(), metadata.New(map[string]string{"other": "value"}))
|
|
_, err := s.ListUsers(ctx, &iam_pb.ListUsersRequest{})
|
|
if got, want := status.Code(err), codes.Unauthenticated; got != want {
|
|
t.Fatalf("ListUsers with no authorization header: got code %v, want %v (err=%v)", got, want, err)
|
|
}
|
|
}
|
|
|
|
func TestIamGrpc_NonBearerAuthorization_Unauthenticated(t *testing.T) {
|
|
s := newTestIamGrpcServer(t)
|
|
md := metadata.New(map[string]string{"authorization": "Basic dXNlcjpwYXNz"})
|
|
ctx := metadata.NewIncomingContext(context.Background(), md)
|
|
_, err := s.ListUsers(ctx, &iam_pb.ListUsersRequest{})
|
|
if got, want := status.Code(err), codes.Unauthenticated; got != want {
|
|
t.Fatalf("ListUsers with non-Bearer scheme: got code %v, want %v (err=%v)", got, want, err)
|
|
}
|
|
}
|
|
|
|
func TestIamGrpc_InvalidToken_Unauthenticated(t *testing.T) {
|
|
s := newTestIamGrpcServer(t)
|
|
// Token signed with the wrong key.
|
|
bad := security.GenJwtForFilerAdmin(security.SigningKey("a-different-key"), 60)
|
|
if bad == "" {
|
|
t.Fatal("GenJwtForFilerAdmin returned empty")
|
|
}
|
|
_, err := s.ListUsers(ctxWithBearer(string(bad)), &iam_pb.ListUsersRequest{})
|
|
if got, want := status.Code(err), codes.Unauthenticated; got != want {
|
|
t.Fatalf("ListUsers with mis-signed token: got code %v, want %v (err=%v)", got, want, err)
|
|
}
|
|
}
|
|
|
|
func TestIamGrpc_GarbageToken_Unauthenticated(t *testing.T) {
|
|
s := newTestIamGrpcServer(t)
|
|
_, err := s.ListUsers(ctxWithBearer("not.a.jwt"), &iam_pb.ListUsersRequest{})
|
|
if got, want := status.Code(err), codes.Unauthenticated; got != want {
|
|
t.Fatalf("ListUsers with garbage token: got code %v, want %v (err=%v)", got, want, err)
|
|
}
|
|
}
|
|
|
|
func TestIamGrpc_ExpiredToken_Unauthenticated(t *testing.T) {
|
|
s := newTestIamGrpcServer(t)
|
|
// Mint a token that's already expired (exp in the past).
|
|
claims := security.SeaweedFilerAdminClaims{
|
|
RegisteredClaims: jwt.RegisteredClaims{
|
|
ExpiresAt: jwt.NewNumericDate(time.Now().Add(-time.Hour)),
|
|
},
|
|
}
|
|
tok := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
|
|
encoded, err := tok.SignedString([]byte(testIamSigningKey))
|
|
if err != nil {
|
|
t.Fatalf("SignedString: %v", err)
|
|
}
|
|
_, err = s.ListUsers(ctxWithBearer(encoded), &iam_pb.ListUsersRequest{})
|
|
if got, want := status.Code(err), codes.Unauthenticated; got != want {
|
|
t.Fatalf("ListUsers with expired token: got code %v, want %v (err=%v)", got, want, err)
|
|
}
|
|
}
|
|
|
|
func TestIamGrpc_ValidToken_ReachesHandler(t *testing.T) {
|
|
s := newTestIamGrpcServer(t)
|
|
good := security.GenJwtForFilerAdmin(security.SigningKey(testIamSigningKey), 60)
|
|
if good == "" {
|
|
t.Fatal("GenJwtForFilerAdmin returned empty")
|
|
}
|
|
resp, err := s.ListUsers(ctxWithBearer(string(good)), &iam_pb.ListUsersRequest{})
|
|
if err != nil {
|
|
t.Fatalf("ListUsers with valid token: unexpected error %v", err)
|
|
}
|
|
if resp == nil {
|
|
t.Fatal("ListUsers with valid token: nil response")
|
|
}
|
|
// Memory store starts empty; the handler ran past the auth gate.
|
|
if len(resp.Usernames) != 0 {
|
|
t.Fatalf("ListUsers: expected empty user list from fresh memory store, got %v", resp.Usernames)
|
|
}
|
|
}
|
|
|
|
func TestIamGrpc_ValidTokenAfterStaleToken_ReachesHandler(t *testing.T) {
|
|
s := newTestIamGrpcServer(t)
|
|
stale := security.GenJwtForFilerAdmin(security.SigningKey("a-different-key"), 60)
|
|
good := security.GenJwtForFilerAdmin(security.SigningKey(testIamSigningKey), 60)
|
|
ctx := metadata.NewIncomingContext(context.Background(), metadata.MD{
|
|
"authorization": []string{"Bearer " + string(stale), "Bearer " + string(good)},
|
|
})
|
|
if _, err := s.ListUsers(ctx, &iam_pb.ListUsersRequest{}); err != nil {
|
|
t.Fatalf("ListUsers with a valid token after a stale one: unexpected error %v", err)
|
|
}
|
|
}
|
|
|
|
func TestIamGrpc_AllInvalidTokens_Unauthenticated(t *testing.T) {
|
|
s := newTestIamGrpcServer(t)
|
|
stale := security.GenJwtForFilerAdmin(security.SigningKey("a-different-key"), 60)
|
|
other := security.GenJwtForFilerAdmin(security.SigningKey("another-different-key"), 60)
|
|
ctx := metadata.NewIncomingContext(context.Background(), metadata.MD{
|
|
"authorization": []string{"Bearer " + string(stale), "Bearer " + string(other)},
|
|
})
|
|
_, err := s.ListUsers(ctx, &iam_pb.ListUsersRequest{})
|
|
if got, want := status.Code(err), codes.Unauthenticated; got != want {
|
|
t.Fatalf("ListUsers with two invalid tokens: got code %v, want %v (err=%v)", got, want, err)
|
|
}
|
|
}
|
|
|
|
func TestIamGrpc_NoSigningKey_Unauthenticated_Allowed(t *testing.T) {
|
|
// Auth is opt-in: when the server is built without a signing key, every
|
|
// RPC is accepted regardless of (or in the absence of) metadata so the
|
|
// admin UI works against a filer that has no jwt.filer_signing.key set.
|
|
cm, err := credential.NewCredentialManager(credential.StoreTypeMemory, nil, "")
|
|
if err != nil {
|
|
t.Fatalf("NewCredentialManager: %v", err)
|
|
}
|
|
s := NewIamGrpcServer(cm, nil)
|
|
resp, err := s.ListUsers(context.Background(), &iam_pb.ListUsersRequest{})
|
|
if err != nil {
|
|
t.Fatalf("ListUsers without key: unexpected error %v", err)
|
|
}
|
|
if resp == nil {
|
|
t.Fatal("ListUsers without key: nil response")
|
|
}
|
|
if len(resp.Usernames) != 0 {
|
|
t.Fatalf("ListUsers: expected empty user list from fresh memory store, got %v", resp.Usernames)
|
|
}
|
|
|
|
// A token sent by a client that does configure a key is also accepted —
|
|
// the server just ignores it rather than rejecting on signature mismatch.
|
|
good := security.GenJwtForFilerAdmin(security.SigningKey(testIamSigningKey), 60)
|
|
if _, err := s.ListUsers(ctxWithBearer(string(good)), &iam_pb.ListUsersRequest{}); err != nil {
|
|
t.Fatalf("ListUsers with stray token but no server key: unexpected error %v", err)
|
|
}
|
|
}
|
|
|
|
func TestIamGrpc_CreateUser_RequiresAuth(t *testing.T) {
|
|
// Spot-check a write RPC too — auth must run before any work.
|
|
s := newTestIamGrpcServer(t)
|
|
_, err := s.CreateUser(context.Background(), &iam_pb.CreateUserRequest{
|
|
Identity: &iam_pb.Identity{Name: "admin"},
|
|
})
|
|
if got, want := status.Code(err), codes.Unauthenticated; got != want {
|
|
t.Fatalf("CreateUser without token: got code %v, want %v (err=%v)", got, want, err)
|
|
}
|
|
}
|