filer: require admin-signed JWT on the IAM gRPC service (#9442)

Every IAM RPC (CreateUser, PutPolicy, CreateAccessKey, ...) now requires
a Bearer token in the authorization metadata, signed with the filer
write-signing key. The service refuses to register on a filer that has
no jwt.filer_signing.key set, so the unauthenticated default is gone:
operators who use these RPCs must configure the key and attach a token
on every call.

Bearer scheme matching is case-insensitive (RFC 6750), every handler
nil-checks req before dereferencing it, and tests now cover the
expired-token path.
This commit is contained in:
Chris Lu
2026-05-12 10:11:08 -07:00
committed by GitHub
parent 05ed5c9ae8
commit 5e8f99f40a
5 changed files with 344 additions and 7 deletions
+15 -4
View File
@@ -431,11 +431,22 @@ func (fo *FilerOptions) startFiler() {
grpcS := pb.NewGrpcServer(security.LoadServerTLS(util.GetViper(), "grpc.filer"))
filer_pb.RegisterSeaweedFilerServer(grpcS, fs)
// Register IAM gRPC service if credential manager is available
// Register IAM gRPC service only when both a credential manager and an
// admin signing key are configured. The IAM RPCs can create users and
// mint access keys; mounting them on an unauthenticated listener would
// hand any caller that can reach the gRPC port S3-admin equivalent power.
// Operators who relied on the unauthenticated path must now set
// jwt.filer_signing.key in security.toml and attach a Bearer token signed
// with that key on every IAM call.
if credentialManager != nil {
iamGrpcServer := weed_server.NewIamGrpcServer(credentialManager)
iam_pb.RegisterSeaweedIdentityAccessManagementServer(grpcS, iamGrpcServer)
glog.V(0).Info("Registered IAM gRPC service on filer")
adminSigningKey := security.SigningKey(util.GetViper().GetString("jwt.filer_signing.key"))
if len(adminSigningKey) == 0 {
glog.Warningf("IAM gRPC service NOT registered on filer: jwt.filer_signing.key is empty in security.toml; configure it to enable IAM administration")
} else {
iamGrpcServer := weed_server.NewIamGrpcServer(credentialManager, adminSigningKey)
iam_pb.RegisterSeaweedIdentityAccessManagementServer(grpcS, iamGrpcServer)
glog.V(0).Info("Registered IAM gRPC service on filer (admin Bearer token required)")
}
}
reflection.Register(grpcS)
+5
View File
@@ -44,6 +44,11 @@ expires_after_seconds = 10 # seconds
# - f.e. the S3 API Shim generates the JWT
# - the Filer server validates the JWT on writing
# NOTE: This key is ALSO used as a fallback signing key for S3 STS if s3.iam.config does not specify a signingKey.
# NOTE: This key is ALSO required to mount the IAM gRPC service (CreateUser,
# PutPolicy, CreateAccessKey, ...) on the filer. The filer refuses to
# register that service when the key is empty, and every IAM RPC must
# carry a Bearer token signed with this key in its "authorization"
# gRPC metadata. Mint such a token with security.GenJwtForFilerAdmin.
# the jwt defaults to expire after 10 seconds.
[jwt.filer_signing]
key = ""
+35
View File
@@ -29,6 +29,41 @@ type SeaweedFilerClaims struct {
jwt.RegisteredClaims
}
// SeaweedFilerAdminClaims is presented by callers of the filer's IAM gRPC
// service to prove they are authorised to administer users, access keys, and
// policies. The token is signed with the filer write-signing key
// (jwt.filer_signing.key in security.toml).
//
// Validation is delegated to DecodeJwt below: it enforces HS256 via the
// SigningMethodHMAC type check, and jwt/v5 validates exp/nbf on the embedded
// RegisteredClaims. Extra JSON fields in the payload are silently ignored by
// encoding/json, which is the desired behaviour here (forward-compat).
type SeaweedFilerAdminClaims struct {
jwt.RegisteredClaims
}
// GenJwtForFilerAdmin mints a Bearer token for the filer IAM gRPC service.
// Returns an empty string if the signing key is not configured.
func GenJwtForFilerAdmin(signingKey SigningKey, expiresAfterSec int) EncodedJwt {
if len(signingKey) == 0 {
return ""
}
claims := SeaweedFilerAdminClaims{
RegisteredClaims: jwt.RegisteredClaims{},
}
if expiresAfterSec > 0 {
claims.ExpiresAt = jwt.NewNumericDate(time.Now().Add(time.Second * time.Duration(expiresAfterSec)))
}
t := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
encoded, e := t.SignedString([]byte(signingKey))
if e != nil {
glog.V(0).Infof("Failed to sign claims %+v: %v", t.Claims, e)
return ""
}
return EncodedJwt(encoded)
}
func GenJwtForVolumeServer(signingKey SigningKey, expiresAfterSec int, fileId string) EncodedJwt {
if len(signingKey) == 0 {
return ""
+145 -3
View File
@@ -3,32 +3,78 @@ package weed_server
import (
"context"
"encoding/json"
"strings"
"github.com/seaweedfs/seaweedfs/weed/credential"
"github.com/seaweedfs/seaweedfs/weed/glog"
"github.com/seaweedfs/seaweedfs/weed/pb/iam_pb"
"github.com/seaweedfs/seaweedfs/weed/s3api/policy_engine"
"github.com/seaweedfs/seaweedfs/weed/security"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/metadata"
"google.golang.org/grpc/status"
)
// IamGrpcServer implements the IAM gRPC service on the filer
// IamGrpcServer implements the IAM gRPC service on the filer.
// Every RPC requires a Bearer token in the "authorization" metadata, signed
// with the filer write-signing key (jwt.filer_signing.key in security.toml).
// If no signing key is configured the service refuses to register at all; the
// adminSigningKey check below is defensive in case that wiring is bypassed.
type IamGrpcServer struct {
iam_pb.UnimplementedSeaweedIdentityAccessManagementServer
credentialManager *credential.CredentialManager
adminSigningKey security.SigningKey
}
// NewIamGrpcServer creates a new IAM gRPC server
func NewIamGrpcServer(credentialManager *credential.CredentialManager) *IamGrpcServer {
// NewIamGrpcServer creates a new IAM gRPC server. adminSigningKey is required:
// callers without a Bearer token signed by this key are rejected.
func NewIamGrpcServer(credentialManager *credential.CredentialManager, adminSigningKey security.SigningKey) *IamGrpcServer {
return &IamGrpcServer{
credentialManager: credentialManager,
adminSigningKey: adminSigningKey,
}
}
// checkAdminAuth verifies the caller presented a Bearer token signed by the
// filer's write-signing key. It is invoked at the top of every IAM RPC.
func (s *IamGrpcServer) checkAdminAuth(ctx context.Context) error {
if len(s.adminSigningKey) == 0 {
// Service should not be registered without a key; fail closed.
return status.Error(codes.PermissionDenied, "iam admin auth not configured")
}
md, ok := metadata.FromIncomingContext(ctx)
if !ok {
return status.Error(codes.Unauthenticated, "missing metadata")
}
authHeaders := md.Get("authorization")
if len(authHeaders) == 0 {
return status.Error(codes.Unauthenticated, "missing authorization metadata")
}
// RFC 6750 §2.1: the "Bearer" auth-scheme name is case-insensitive.
// Tolerate surrounding whitespace and any spacing between scheme and token.
raw := strings.TrimSpace(authHeaders[0])
parts := strings.Fields(raw)
if len(parts) != 2 || !strings.EqualFold(parts[0], "Bearer") || parts[1] == "" {
return status.Error(codes.Unauthenticated, "authorization header must use Bearer scheme")
}
token := parts[1]
parsed, err := security.DecodeJwt(s.adminSigningKey, security.EncodedJwt(token), &security.SeaweedFilerAdminClaims{})
if err != nil || parsed == nil || !parsed.Valid {
return status.Error(codes.Unauthenticated, "invalid admin token")
}
return nil
}
//////////////////////////////////////////////////
// Configuration Management
func (s *IamGrpcServer) GetConfiguration(ctx context.Context, req *iam_pb.GetConfigurationRequest) (*iam_pb.GetConfigurationResponse, error) {
if err := s.checkAdminAuth(ctx); err != nil {
return nil, err
}
if req == nil {
return nil, status.Errorf(codes.InvalidArgument, "request is required")
}
glog.V(4).Infof("GetConfiguration")
if s.credentialManager == nil {
@@ -47,6 +93,12 @@ func (s *IamGrpcServer) GetConfiguration(ctx context.Context, req *iam_pb.GetCon
}
func (s *IamGrpcServer) PutConfiguration(ctx context.Context, req *iam_pb.PutConfigurationRequest) (*iam_pb.PutConfigurationResponse, error) {
if err := s.checkAdminAuth(ctx); err != nil {
return nil, err
}
if req == nil {
return nil, status.Errorf(codes.InvalidArgument, "request is required")
}
glog.V(4).Infof("PutConfiguration")
if s.credentialManager == nil {
@@ -70,6 +122,9 @@ func (s *IamGrpcServer) PutConfiguration(ctx context.Context, req *iam_pb.PutCon
// User Management
func (s *IamGrpcServer) CreateUser(ctx context.Context, req *iam_pb.CreateUserRequest) (*iam_pb.CreateUserResponse, error) {
if err := s.checkAdminAuth(ctx); err != nil {
return nil, err
}
if req == nil || req.Identity == nil {
return nil, status.Errorf(codes.InvalidArgument, "identity is required")
}
@@ -92,6 +147,12 @@ func (s *IamGrpcServer) CreateUser(ctx context.Context, req *iam_pb.CreateUserRe
}
func (s *IamGrpcServer) GetUser(ctx context.Context, req *iam_pb.GetUserRequest) (*iam_pb.GetUserResponse, error) {
if err := s.checkAdminAuth(ctx); err != nil {
return nil, err
}
if req == nil {
return nil, status.Errorf(codes.InvalidArgument, "request is required")
}
glog.V(4).Infof("GetUser: %s", req.Username)
if s.credentialManager == nil {
@@ -117,6 +178,9 @@ func (s *IamGrpcServer) GetUser(ctx context.Context, req *iam_pb.GetUserRequest)
}
func (s *IamGrpcServer) UpdateUser(ctx context.Context, req *iam_pb.UpdateUserRequest) (*iam_pb.UpdateUserResponse, error) {
if err := s.checkAdminAuth(ctx); err != nil {
return nil, err
}
if req == nil || req.Identity == nil {
return nil, status.Errorf(codes.InvalidArgument, "identity is required")
}
@@ -139,6 +203,12 @@ func (s *IamGrpcServer) UpdateUser(ctx context.Context, req *iam_pb.UpdateUserRe
}
func (s *IamGrpcServer) DeleteUser(ctx context.Context, req *iam_pb.DeleteUserRequest) (*iam_pb.DeleteUserResponse, error) {
if err := s.checkAdminAuth(ctx); err != nil {
return nil, err
}
if req == nil {
return nil, status.Errorf(codes.InvalidArgument, "request is required")
}
glog.V(4).Infof("IAM: Filer.DeleteUser %s", req.Username)
if s.credentialManager == nil {
@@ -158,6 +228,12 @@ func (s *IamGrpcServer) DeleteUser(ctx context.Context, req *iam_pb.DeleteUserRe
}
func (s *IamGrpcServer) ListUsers(ctx context.Context, req *iam_pb.ListUsersRequest) (*iam_pb.ListUsersResponse, error) {
if err := s.checkAdminAuth(ctx); err != nil {
return nil, err
}
if req == nil {
return nil, status.Errorf(codes.InvalidArgument, "request is required")
}
glog.V(4).Infof("ListUsers")
if s.credentialManager == nil {
@@ -193,6 +269,9 @@ func (s *IamGrpcServer) ListUsers(ctx context.Context, req *iam_pb.ListUsersRequ
// Access Key Management
func (s *IamGrpcServer) CreateAccessKey(ctx context.Context, req *iam_pb.CreateAccessKeyRequest) (*iam_pb.CreateAccessKeyResponse, error) {
if err := s.checkAdminAuth(ctx); err != nil {
return nil, err
}
if req == nil || req.Credential == nil {
return nil, status.Errorf(codes.InvalidArgument, "credential is required")
}
@@ -215,6 +294,12 @@ func (s *IamGrpcServer) CreateAccessKey(ctx context.Context, req *iam_pb.CreateA
}
func (s *IamGrpcServer) DeleteAccessKey(ctx context.Context, req *iam_pb.DeleteAccessKeyRequest) (*iam_pb.DeleteAccessKeyResponse, error) {
if err := s.checkAdminAuth(ctx); err != nil {
return nil, err
}
if req == nil {
return nil, status.Errorf(codes.InvalidArgument, "request is required")
}
glog.V(4).Infof("DeleteAccessKey: %s for user: %s", req.AccessKey, req.Username)
if s.credentialManager == nil {
@@ -237,6 +322,12 @@ func (s *IamGrpcServer) DeleteAccessKey(ctx context.Context, req *iam_pb.DeleteA
}
func (s *IamGrpcServer) GetUserByAccessKey(ctx context.Context, req *iam_pb.GetUserByAccessKeyRequest) (*iam_pb.GetUserByAccessKeyResponse, error) {
if err := s.checkAdminAuth(ctx); err != nil {
return nil, err
}
if req == nil {
return nil, status.Errorf(codes.InvalidArgument, "request is required")
}
glog.V(4).Infof("GetUserByAccessKey: %s", req.AccessKey)
if s.credentialManager == nil {
@@ -261,6 +352,12 @@ func (s *IamGrpcServer) GetUserByAccessKey(ctx context.Context, req *iam_pb.GetU
// Policy Management
func (s *IamGrpcServer) PutPolicy(ctx context.Context, req *iam_pb.PutPolicyRequest) (*iam_pb.PutPolicyResponse, error) {
if err := s.checkAdminAuth(ctx); err != nil {
return nil, err
}
if req == nil {
return nil, status.Errorf(codes.InvalidArgument, "request is required")
}
glog.V(4).Infof("IAM: Filer.PutPolicy %s", req.Name)
if s.credentialManager == nil {
@@ -293,6 +390,12 @@ func (s *IamGrpcServer) PutPolicy(ctx context.Context, req *iam_pb.PutPolicyRequ
}
func (s *IamGrpcServer) GetPolicy(ctx context.Context, req *iam_pb.GetPolicyRequest) (*iam_pb.GetPolicyResponse, error) {
if err := s.checkAdminAuth(ctx); err != nil {
return nil, err
}
if req == nil {
return nil, status.Errorf(codes.InvalidArgument, "request is required")
}
glog.V(4).Infof("GetPolicy: %s", req.Name)
if s.credentialManager == nil {
@@ -322,6 +425,12 @@ func (s *IamGrpcServer) GetPolicy(ctx context.Context, req *iam_pb.GetPolicyRequ
}
func (s *IamGrpcServer) ListPolicies(ctx context.Context, req *iam_pb.ListPoliciesRequest) (*iam_pb.ListPoliciesResponse, error) {
if err := s.checkAdminAuth(ctx); err != nil {
return nil, err
}
if req == nil {
return nil, status.Errorf(codes.InvalidArgument, "request is required")
}
glog.V(4).Infof("ListPolicies")
if s.credentialManager == nil {
@@ -352,6 +461,12 @@ func (s *IamGrpcServer) ListPolicies(ctx context.Context, req *iam_pb.ListPolici
}
func (s *IamGrpcServer) DeletePolicy(ctx context.Context, req *iam_pb.DeletePolicyRequest) (*iam_pb.DeletePolicyResponse, error) {
if err := s.checkAdminAuth(ctx); err != nil {
return nil, err
}
if req == nil {
return nil, status.Errorf(codes.InvalidArgument, "request is required")
}
glog.V(4).Infof("DeletePolicy: %s", req.Name)
if s.credentialManager == nil {
@@ -371,6 +486,9 @@ func (s *IamGrpcServer) DeletePolicy(ctx context.Context, req *iam_pb.DeletePoli
// Service Account Management
func (s *IamGrpcServer) CreateServiceAccount(ctx context.Context, req *iam_pb.CreateServiceAccountRequest) (*iam_pb.CreateServiceAccountResponse, error) {
if err := s.checkAdminAuth(ctx); err != nil {
return nil, err
}
if req == nil || req.ServiceAccount == nil {
return nil, status.Errorf(codes.InvalidArgument, "service account is required")
}
@@ -393,6 +511,9 @@ func (s *IamGrpcServer) CreateServiceAccount(ctx context.Context, req *iam_pb.Cr
}
func (s *IamGrpcServer) UpdateServiceAccount(ctx context.Context, req *iam_pb.UpdateServiceAccountRequest) (*iam_pb.UpdateServiceAccountResponse, error) {
if err := s.checkAdminAuth(ctx); err != nil {
return nil, err
}
if req == nil || req.ServiceAccount == nil {
return nil, status.Errorf(codes.InvalidArgument, "service account is required")
}
@@ -412,6 +533,12 @@ func (s *IamGrpcServer) UpdateServiceAccount(ctx context.Context, req *iam_pb.Up
}
func (s *IamGrpcServer) DeleteServiceAccount(ctx context.Context, req *iam_pb.DeleteServiceAccountRequest) (*iam_pb.DeleteServiceAccountResponse, error) {
if err := s.checkAdminAuth(ctx); err != nil {
return nil, err
}
if req == nil {
return nil, status.Errorf(codes.InvalidArgument, "request is required")
}
glog.V(4).Infof("DeleteServiceAccount: %s", req.Id)
if s.credentialManager == nil {
@@ -431,6 +558,12 @@ func (s *IamGrpcServer) DeleteServiceAccount(ctx context.Context, req *iam_pb.De
}
func (s *IamGrpcServer) GetServiceAccount(ctx context.Context, req *iam_pb.GetServiceAccountRequest) (*iam_pb.GetServiceAccountResponse, error) {
if err := s.checkAdminAuth(ctx); err != nil {
return nil, err
}
if req == nil {
return nil, status.Errorf(codes.InvalidArgument, "request is required")
}
glog.V(4).Infof("GetServiceAccount: %s", req.Id)
if s.credentialManager == nil {
@@ -453,6 +586,12 @@ func (s *IamGrpcServer) GetServiceAccount(ctx context.Context, req *iam_pb.GetSe
}
func (s *IamGrpcServer) ListServiceAccounts(ctx context.Context, req *iam_pb.ListServiceAccountsRequest) (*iam_pb.ListServiceAccountsResponse, error) {
if err := s.checkAdminAuth(ctx); err != nil {
return nil, err
}
if req == nil {
return nil, status.Errorf(codes.InvalidArgument, "request is required")
}
glog.V(4).Infof("ListServiceAccounts")
if s.credentialManager == nil {
@@ -471,6 +610,9 @@ func (s *IamGrpcServer) ListServiceAccounts(ctx context.Context, req *iam_pb.Lis
}
func (s *IamGrpcServer) GetServiceAccountByAccessKey(ctx context.Context, req *iam_pb.GetServiceAccountByAccessKeyRequest) (*iam_pb.GetServiceAccountByAccessKeyResponse, error) {
if err := s.checkAdminAuth(ctx); err != nil {
return nil, err
}
if req == nil {
return nil, status.Errorf(codes.InvalidArgument, "request is required")
}
@@ -0,0 +1,144 @@
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_NoSigningKey_PermissionDenied(t *testing.T) {
// Defensive path: even if the service is somehow registered without a
// key, every RPC must refuse.
cm, err := credential.NewCredentialManager(credential.StoreTypeMemory, nil, "")
if err != nil {
t.Fatalf("NewCredentialManager: %v", err)
}
s := NewIamGrpcServer(cm, nil)
good := security.GenJwtForFilerAdmin(security.SigningKey(testIamSigningKey), 60)
_, err = s.ListUsers(ctxWithBearer(string(good)), &iam_pb.ListUsersRequest{})
if got, want := status.Code(err), codes.PermissionDenied; got != want {
t.Fatalf("ListUsers with no signing key: got code %v, want %v (err=%v)", got, want, 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)
}
}