diff --git a/core/cli/configure.go b/core/cli/configure.go index a533aba..d99f6ed 100644 --- a/core/cli/configure.go +++ b/core/cli/configure.go @@ -102,12 +102,33 @@ func runE(cmd *cobra.Command, _ []string) error { return err } + token, err := tui.StringPrompt("enter bearer token (optional)", "", config.CliConfig.Token) + if err != nil { + return err + } + certPath, err := tui.StringPrompt("enter TLS certificate path (optional)", "", config.CliConfig.CertPath) + if err != nil { + return err + } + certKey, err := tui.StringPrompt("enter TLS certificate key path (optional)", "", config.CliConfig.CertKey) + if err != nil { + return err + } + + config.CliConfig.PermifyURL = url + config.CliConfig.Token = token + config.CliConfig.CertPath = certPath + config.CliConfig.CertKey = certKey + resp, err := client.New(url) + if err != nil { + return err + } // Todo: Implement pagination tenants, err := resp.Tenancy.List(context.Background(), &v1.TenantListRequest{}) if err != nil { - logger.Log.Fatal(err) + return err } tenantNames := []string{} @@ -117,16 +138,15 @@ func runE(cmd *cobra.Command, _ []string) error { tenantNames = append(tenantNames, nameID) tenantIds[nameID] = tenant.Id } - + tenant, err := tui.Choice("Select a tenant: ", tenantNames) if err != nil { logger.Log.Error(err) } - config.CliConfig.PermifyURL = url config.CliConfig.Tenant = tenantIds[tenant] err = config.Write() if err != nil { - logger.Log.Error(err) + return err } logger.Log.Info("successfully configured ", "config file", configFile) return nil diff --git a/core/client/grpc.go b/core/client/grpc.go index 11835df..55daa1f 100644 --- a/core/client/grpc.go +++ b/core/client/grpc.go @@ -2,19 +2,95 @@ package client import ( + "crypto/tls" + "fmt" + "strings" + + "github.com/Permify/permify-cli/core/config" permify "github.com/Permify/permify-go/v1" "google.golang.org/grpc" + "google.golang.org/grpc/credentials" "google.golang.org/grpc/credentials/insecure" ) // New initializes a new permify client func New(endpoint string) (*permify.Client, error) { + cliConfig := config.CliConfig + if endpoint != "" { + cliConfig.PermifyURL = endpoint + } + if cliConfig.PermifyURL == "" { + return nil, fmt.Errorf("permify endpoint is empty") + } + + dialOptions, err := newDialOptions(cliConfig) + if err != nil { + return nil, err + } + client, err := permify.NewClient( permify.Config{ - Endpoint: endpoint, + Endpoint: cliConfig.PermifyURL, }, - // Todo: Implement secure call with tls certificate - grpc.WithTransportCredentials(insecure.NewCredentials()), + dialOptions..., ) return client, err } + +func newDialOptions(cliConfig config.CoreConfig) ([]grpc.DialOption, error) { + transportCredentials, secureTransport, err := newTransportCredentials(cliConfig.CertPath, cliConfig.CertKey) + if err != nil { + return nil, err + } + + dialOptions := []grpc.DialOption{ + grpc.WithTransportCredentials(transportCredentials), + } + + token := strings.TrimSpace(cliConfig.Token) + if token == "" { + return dialOptions, nil + } + + requestMetadata := map[string]string{ + "authorization": bearerToken(token), + } + if secureTransport { + dialOptions = append(dialOptions, grpc.WithPerRPCCredentials(secureTokenCredentials(requestMetadata))) + } else { + dialOptions = append(dialOptions, grpc.WithPerRPCCredentials(nonSecureTokenCredentials(requestMetadata))) + } + return dialOptions, nil +} + +func newTransportCredentials(certPath, certKey string) (credentials.TransportCredentials, bool, error) { + certPath = strings.TrimSpace(certPath) + certKey = strings.TrimSpace(certKey) + if certPath == "" { + return insecure.NewCredentials(), false, nil + } + + if certKey == "" { + transportCredentials, err := credentials.NewClientTLSFromFile(certPath, "") + if err != nil { + return nil, false, fmt.Errorf("load TLS certificate %q: %w", certPath, err) + } + return transportCredentials, true, nil + } + + cert, err := tls.LoadX509KeyPair(certPath, certKey) + if err != nil { + return nil, false, fmt.Errorf("load TLS certificate pair %q/%q: %w", certPath, certKey, err) + } + return credentials.NewTLS(&tls.Config{ + Certificates: []tls.Certificate{cert}, + MinVersion: tls.VersionTLS12, + }), true, nil +} + +func bearerToken(token string) string { + if strings.HasPrefix(strings.ToLower(token), "bearer ") { + return token + } + return "Bearer " + token +} diff --git a/core/client/grpc_test.go b/core/client/grpc_test.go new file mode 100644 index 0000000..c24df09 --- /dev/null +++ b/core/client/grpc_test.go @@ -0,0 +1,40 @@ +package client + +import ( + "strings" + "testing" +) + +func TestBearerTokenAddsBearerPrefix(t *testing.T) { + got := bearerToken("test-token") + if got != "Bearer test-token" { + t.Fatalf("expected bearer prefix, got %q", got) + } +} + +func TestBearerTokenPreservesExistingBearerPrefix(t *testing.T) { + got := bearerToken("Bearer test-token") + if got != "Bearer test-token" { + t.Fatalf("expected existing bearer token to be preserved, got %q", got) + } +} + +func TestNewTransportCredentialsFallsBackToInsecureWithoutCert(t *testing.T) { + _, secureTransport, err := newTransportCredentials("", "") + if err != nil { + t.Fatalf("expected no error without certificate paths, got %v", err) + } + if secureTransport { + t.Fatal("expected insecure transport when no certificate path is configured") + } +} + +func TestNewTransportCredentialsReturnsCertificateLoadErrors(t *testing.T) { + _, _, err := newTransportCredentials("missing-cert.pem", "") + if err == nil { + t.Fatal("expected an error for a missing certificate") + } + if !strings.Contains(err.Error(), "missing-cert.pem") { + t.Fatalf("expected error to include certificate path, got %v", err) + } +} diff --git a/core/config/config.go b/core/config/config.go index c9bdebb..19cc018 100644 --- a/core/config/config.go +++ b/core/config/config.go @@ -25,9 +25,12 @@ type ProfileConfigs struct { // CoreConfig is the config struct type CoreConfig struct { - PermifyURL string `yaml:"permify_url"` - Tenant string `yaml:"tenant"` - SslEnabled bool `yaml:"-"` + PermifyURL string `yaml:"permify_url"` + Tenant string `yaml:"tenant"` + Token string `yaml:"token,omitempty"` + CertPath string `yaml:"cert_path,omitempty"` + CertKey string `yaml:"cert_key,omitempty"` + SslEnabled bool `yaml:"-"` } // IsConfigured checks if permctl cli has been configured @@ -70,7 +73,7 @@ func Load(file string, profile string) error { profileConfigs.File = file profileConfigs.Profile = profile CliConfig = profileConfigs.Configs[profile] - CliConfig.SslEnabled = strings.HasPrefix(CliConfig.PermifyURL, "https") + CliConfig.SslEnabled = strings.HasPrefix(CliConfig.PermifyURL, "https") || CliConfig.CertPath != "" return err } @@ -84,7 +87,7 @@ func New(file string, profile string) error { if err != nil { return err } - err = os.WriteFile(file, newConfigDataByte, fs.FileMode(0644)) + err = os.WriteFile(file, newConfigDataByte, fs.FileMode(0600)) return err } @@ -100,6 +103,6 @@ func Write() error { if err != nil { return err } - err = os.WriteFile(profileConfigs.File, newConfigDataByte, fs.FileMode(0644)) + err = os.WriteFile(profileConfigs.File, newConfigDataByte, fs.FileMode(0600)) return err }