From 1f3bcd6b691987edc43780b1c707da3f8fbb6384 Mon Sep 17 00:00:00 2001 From: Kyle Isom Date: Wed, 1 Apr 2026 19:39:00 -0700 Subject: [PATCH] Fix gRPC auth: inject bearer token via PerRPCCredentials The gRPC client was not sending the authorization header, causing "missing authorization header" errors even when a token was configured. Also fix config test to isolate from real ~/.config/mcrctl.toml. Co-Authored-By: Claude Opus 4.6 (1M context) --- cmd/mcrctl/client.go | 18 ++++++++++++++++-- cmd/mcrctl/config_test.go | 4 +++- 2 files changed, 19 insertions(+), 3 deletions(-) diff --git a/cmd/mcrctl/client.go b/cmd/mcrctl/client.go index b3e966d..9596ad8 100644 --- a/cmd/mcrctl/client.go +++ b/cmd/mcrctl/client.go @@ -62,10 +62,14 @@ func newClient(serverURL, grpcAddr, token, caCertFile string) (*apiClient, error if grpcAddr != "" { creds := credentials.NewTLS(tlsCfg) - cc, err := grpc.NewClient(grpcAddr, + dialOpts := []grpc.DialOption{ grpc.WithTransportCredentials(creds), grpc.WithDefaultCallOptions(grpc.ForceCodecV2(mcrv1.JSONCodec{})), - ) + } + if token != "" { + dialOpts = append(dialOpts, grpc.WithPerRPCCredentials(bearerToken(token))) + } + cc, err := grpc.NewClient(grpcAddr, dialOpts...) if err != nil { return nil, fmt.Errorf("grpc dial: %w", err) } @@ -91,6 +95,16 @@ func (c *apiClient) useGRPC() bool { return c.grpcConn != nil } +// bearerToken implements grpc.PerRPCCredentials, injecting the +// Authorization header into every gRPC call. +type bearerToken string + +func (t bearerToken) GetRequestMetadata(_ context.Context, _ ...string) (map[string]string, error) { + return map[string]string{"authorization": "Bearer " + string(t)}, nil +} + +func (t bearerToken) RequireTransportSecurity() bool { return true } + // apiError is the JSON error envelope returned by the REST API. type apiError struct { Error string `json:"error"` diff --git a/cmd/mcrctl/config_test.go b/cmd/mcrctl/config_test.go index 4eb9f8f..97cdeb3 100644 --- a/cmd/mcrctl/config_test.go +++ b/cmd/mcrctl/config_test.go @@ -38,7 +38,9 @@ ca_cert = "/path/to/ca.pem" } func TestLoadConfigMissingDefaultIsOK(t *testing.T) { - // Empty path triggers default search; missing file is not an error. + // Point XDG_CONFIG_HOME at an empty dir so we don't find the real config. + t.Setenv("XDG_CONFIG_HOME", t.TempDir()) + cfg, err := loadConfig("") if err != nil { t.Fatal(err)