mirror of
https://github.com/seaweedfs/seaweedfs.git
synced 2026-11-05 03:27:42 +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>
175 lines
6.1 KiB
Go
175 lines
6.1 KiB
Go
package s3api
|
|
|
|
import (
|
|
"context"
|
|
|
|
"github.com/seaweedfs/seaweedfs/weed/glog"
|
|
"github.com/seaweedfs/seaweedfs/weed/pb/iam_pb"
|
|
"github.com/seaweedfs/seaweedfs/weed/security"
|
|
"google.golang.org/grpc/codes"
|
|
"google.golang.org/grpc/status"
|
|
)
|
|
|
|
// SeaweedS3IamCacheServer Implementation
|
|
// This interface is dedicated to UNIDIRECTIONAL updates from Filer to S3 Server.
|
|
// S3 Server acts purely as a cache.
|
|
|
|
// checkAdminAuth verifies the caller presented a Bearer token signed by the
|
|
// filer write-signing key (jwt.filer_signing.key). It mirrors the filer's
|
|
// IamGrpcServer.checkAdminAuth so the same operator knob that locks down the
|
|
// filer IAM gRPC service also locks down this cache. With no key configured
|
|
// remote callers cannot be told apart, so only local clients (unix socket,
|
|
// loopback, or the server's own addresses) are allowed.
|
|
func (s3a *S3ApiServer) checkAdminAuth(ctx context.Context) error {
|
|
var signingKey security.SigningKey
|
|
if s3a.filerGuard != nil {
|
|
signingKey = s3a.filerGuard.SigningKey()
|
|
}
|
|
if len(signingKey) == 0 {
|
|
return checkLocalPeer(ctx)
|
|
}
|
|
return security.CheckGrpcAdminToken(ctx, signingKey)
|
|
}
|
|
|
|
func checkLocalPeer(ctx context.Context) error {
|
|
return security.CheckLocalGrpcPeer(ctx)
|
|
}
|
|
|
|
func (s3a *S3ApiServer) PutIdentity(ctx context.Context, req *iam_pb.PutIdentityRequest) (*iam_pb.PutIdentityResponse, error) {
|
|
if err := s3a.checkAdminAuth(ctx); err != nil {
|
|
return nil, err
|
|
}
|
|
if req.Identity == nil {
|
|
return nil, status.Errorf(codes.InvalidArgument, "identity is required")
|
|
}
|
|
glog.V(1).Infof("IAM: received identity update for %s", req.Identity.Name)
|
|
if err := s3a.iam.UpsertIdentity(req.Identity); err != nil {
|
|
glog.Errorf("failed to update identity cache for %s: %v", req.Identity.Name, err)
|
|
return nil, status.Errorf(codes.Internal, "failed to update identity cache: %v", err)
|
|
}
|
|
return &iam_pb.PutIdentityResponse{}, nil
|
|
}
|
|
|
|
func (s3a *S3ApiServer) RemoveIdentity(ctx context.Context, req *iam_pb.RemoveIdentityRequest) (*iam_pb.RemoveIdentityResponse, error) {
|
|
if err := s3a.checkAdminAuth(ctx); err != nil {
|
|
return nil, err
|
|
}
|
|
if req.Username == "" {
|
|
return nil, status.Errorf(codes.InvalidArgument, "username is required")
|
|
}
|
|
glog.V(1).Infof("IAM: received identity removal for %s", req.Username)
|
|
s3a.iam.RemoveIdentity(req.Username)
|
|
return &iam_pb.RemoveIdentityResponse{}, nil
|
|
}
|
|
|
|
func (s3a *S3ApiServer) PutPolicy(ctx context.Context, req *iam_pb.PutPolicyRequest) (*iam_pb.PutPolicyResponse, error) {
|
|
if err := s3a.checkAdminAuth(ctx); err != nil {
|
|
return nil, err
|
|
}
|
|
if req.Name == "" {
|
|
return nil, status.Errorf(codes.InvalidArgument, "policy name is required")
|
|
}
|
|
if req.Content == "" {
|
|
return nil, status.Errorf(codes.InvalidArgument, "policy content is required")
|
|
}
|
|
|
|
glog.V(1).Infof("IAM: received policy update for %s", req.Name)
|
|
if s3a.iam == nil {
|
|
return nil, status.Errorf(codes.Internal, "IAM not initialized")
|
|
}
|
|
|
|
if err := s3a.iam.PutPolicy(req.Name, req.Content); err != nil {
|
|
glog.Errorf("failed to update policy cache for %s: %v", req.Name, err)
|
|
return nil, status.Errorf(codes.Internal, "failed to update policy cache: %v", err)
|
|
}
|
|
return &iam_pb.PutPolicyResponse{}, nil
|
|
}
|
|
|
|
func (s3a *S3ApiServer) DeletePolicy(ctx context.Context, req *iam_pb.DeletePolicyRequest) (*iam_pb.DeletePolicyResponse, error) {
|
|
if err := s3a.checkAdminAuth(ctx); err != nil {
|
|
return nil, err
|
|
}
|
|
if req.Name == "" {
|
|
return nil, status.Errorf(codes.InvalidArgument, "policy name is required")
|
|
}
|
|
|
|
glog.V(1).Infof("IAM: received policy removal for %s", req.Name)
|
|
if s3a.iam == nil {
|
|
return nil, status.Errorf(codes.Internal, "IAM not initialized")
|
|
}
|
|
|
|
if err := s3a.iam.DeletePolicy(req.Name); err != nil {
|
|
glog.Errorf("failed to delete policy cache for %s: %v", req.Name, err)
|
|
return nil, status.Errorf(codes.Internal, "failed to delete policy cache: %v", err)
|
|
}
|
|
return &iam_pb.DeletePolicyResponse{}, nil
|
|
}
|
|
|
|
func (s3a *S3ApiServer) GetPolicy(ctx context.Context, req *iam_pb.GetPolicyRequest) (*iam_pb.GetPolicyResponse, error) {
|
|
if err := s3a.checkAdminAuth(ctx); err != nil {
|
|
return nil, err
|
|
}
|
|
if req.Name == "" {
|
|
return nil, status.Errorf(codes.InvalidArgument, "policy name is required")
|
|
}
|
|
if s3a.iam == nil {
|
|
return nil, status.Errorf(codes.Internal, "IAM not initialized")
|
|
}
|
|
policy, err := s3a.iam.GetPolicy(req.Name)
|
|
if err != nil {
|
|
return &iam_pb.GetPolicyResponse{}, nil // Not found is fine for cache
|
|
}
|
|
return &iam_pb.GetPolicyResponse{
|
|
Name: policy.Name,
|
|
Content: policy.Content,
|
|
}, nil
|
|
}
|
|
|
|
func (s3a *S3ApiServer) PutGroup(ctx context.Context, req *iam_pb.PutGroupRequest) (*iam_pb.PutGroupResponse, error) {
|
|
if err := s3a.checkAdminAuth(ctx); err != nil {
|
|
return nil, err
|
|
}
|
|
if req.Group == nil {
|
|
return nil, status.Errorf(codes.InvalidArgument, "group is required")
|
|
}
|
|
glog.V(1).Infof("IAM: received group update for %s", req.Group.Name)
|
|
if s3a.iam == nil {
|
|
return nil, status.Errorf(codes.Internal, "IAM not initialized")
|
|
}
|
|
if err := s3a.iam.PutGroup(req.Group); err != nil {
|
|
glog.Errorf("failed to update group cache for %s: %v", req.Group.Name, err)
|
|
return nil, status.Errorf(codes.Internal, "failed to update group cache: %v", err)
|
|
}
|
|
return &iam_pb.PutGroupResponse{}, nil
|
|
}
|
|
|
|
func (s3a *S3ApiServer) RemoveGroup(ctx context.Context, req *iam_pb.RemoveGroupRequest) (*iam_pb.RemoveGroupResponse, error) {
|
|
if err := s3a.checkAdminAuth(ctx); err != nil {
|
|
return nil, err
|
|
}
|
|
if req.GroupName == "" {
|
|
return nil, status.Errorf(codes.InvalidArgument, "group name is required")
|
|
}
|
|
glog.V(1).Infof("IAM: received group removal for %s", req.GroupName)
|
|
if s3a.iam == nil {
|
|
return nil, status.Errorf(codes.Internal, "IAM not initialized")
|
|
}
|
|
s3a.iam.RemoveGroup(req.GroupName)
|
|
return &iam_pb.RemoveGroupResponse{}, nil
|
|
}
|
|
|
|
func (s3a *S3ApiServer) ListPolicies(ctx context.Context, req *iam_pb.ListPoliciesRequest) (*iam_pb.ListPoliciesResponse, error) {
|
|
if err := s3a.checkAdminAuth(ctx); err != nil {
|
|
return nil, err
|
|
}
|
|
resp := &iam_pb.ListPoliciesResponse{}
|
|
if s3a.iam == nil {
|
|
return nil, status.Errorf(codes.Internal, "IAM not initialized")
|
|
}
|
|
policies := s3a.iam.ListPolicies()
|
|
for _, policy := range policies {
|
|
resp.Policies = append(resp.Policies, policy)
|
|
}
|
|
return resp, nil
|
|
}
|