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)