diff --git a/core/cli/configure.go b/core/cli/configure.go index a533aba..6a9c2f1 100644 --- a/core/cli/configure.go +++ b/core/cli/configure.go @@ -4,6 +4,7 @@ import ( "context" "fmt" "os" + "strings" "github.com/Permify/permify-cli/core/client" "github.com/Permify/permify-cli/core/config" @@ -101,8 +102,29 @@ func runE(cmd *cobra.Command, _ []string) error { if err != nil { return err } + token, err := tui.StringPrompt("enter token (optional)", "", config.CliConfig.Token) + if err != nil { + return err + } + certificatePath, err := tui.StringPrompt("enter certificate path (optional)", "", config.CliConfig.CertificatePath) + if err != nil { + return err + } + certificateKeyPath, err := tui.StringPrompt("enter certificate key path (optional)", "", config.CliConfig.CertificateKeyPath) + if err != nil { + return err + } + + config.CliConfig.PermifyURL = url + config.CliConfig.Token = token + config.CliConfig.CertificatePath = certificatePath + config.CliConfig.CertificateKeyPath = certificateKeyPath + config.CliConfig.SslEnabled = strings.HasPrefix(config.CliConfig.PermifyURL, "https") resp, err := client.New(url) + if err != nil { + logger.Log.Fatal(err) + } // Todo: Implement pagination tenants, err := resp.Tenancy.List(context.Background(), &v1.TenantListRequest{}) @@ -117,12 +139,11 @@ 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 { diff --git a/core/client/grpc.go b/core/client/grpc.go index 11835df..a03b9a4 100644 --- a/core/client/grpc.go +++ b/core/client/grpc.go @@ -2,19 +2,66 @@ 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) { + clientConfig := config.CliConfig + if endpoint != "" { + clientConfig.PermifyURL = endpoint + clientConfig.SslEnabled = strings.HasPrefix(endpoint, "https") + } + + options := []grpc.DialOption{} + if clientConfig.SslEnabled { + transportCredentials, err := tlsCredentials(clientConfig) + if err != nil { + return nil, err + } + options = append(options, grpc.WithTransportCredentials(transportCredentials)) + } else { + options = append(options, grpc.WithTransportCredentials(insecure.NewCredentials())) + } + + if clientConfig.Token != "" { + token := map[string]string{"authorization": fmt.Sprintf("Bearer %s", clientConfig.Token)} + if clientConfig.SslEnabled { + options = append(options, grpc.WithPerRPCCredentials(secureTokenCredentials(token))) + } else { + options = append(options, grpc.WithPerRPCCredentials(nonSecureTokenCredentials(token))) + } + } + client, err := permify.NewClient( permify.Config{ - Endpoint: endpoint, + Endpoint: clientConfig.PermifyURL, }, - // Todo: Implement secure call with tls certificate - grpc.WithTransportCredentials(insecure.NewCredentials()), + options..., ) return client, err } + +func tlsCredentials(clientConfig config.CoreConfig) (credentials.TransportCredentials, error) { + tlsConfig := &tls.Config{} + if clientConfig.CertificatePath == "" && clientConfig.CertificateKeyPath == "" { + return credentials.NewTLS(tlsConfig), nil + } + if clientConfig.CertificatePath == "" || clientConfig.CertificateKeyPath == "" { + return nil, fmt.Errorf("both certificate_path and certificate_key_path are required when configuring client certificates") + } + cert, err := tls.LoadX509KeyPair(clientConfig.CertificatePath, clientConfig.CertificateKeyPath) + if err != nil { + return nil, err + } + tlsConfig.Certificates = []tls.Certificate{cert} + return credentials.NewTLS(tlsConfig), nil +} diff --git a/core/config/config.go b/core/config/config.go index c9bdebb..3dfc106 100644 --- a/core/config/config.go +++ b/core/config/config.go @@ -5,12 +5,16 @@ import ( "fmt" "io/fs" "os" + "path/filepath" "strings" "github.com/Permify/permify-cli/core/logger" "gopkg.in/yaml.v3" ) +const credentialsDir = ".permify" +const credentialsFileName = "credentials" + // CliConfig is the global config variable var CliConfig = CoreConfig{} @@ -25,9 +29,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"` + CertificatePath string `yaml:"certificate_path,omitempty"` + CertificateKeyPath string `yaml:"certificate_key_path,omitempty"` + SslEnabled bool `yaml:"-"` } // IsConfigured checks if permctl cli has been configured @@ -70,6 +77,9 @@ func Load(file string, profile string) error { profileConfigs.File = file profileConfigs.Profile = profile CliConfig = profileConfigs.Configs[profile] + if err := loadCredentials(profile); err != nil { + return err + } CliConfig.SslEnabled = strings.HasPrefix(CliConfig.PermifyURL, "https") return err } @@ -79,7 +89,7 @@ func New(file string, profile string) error { profileConfigs.Profile = profile profileConfigs.File = file profileConfigs.Configs = make(map[string]CoreConfig) - profileConfigs.Configs[profile] = CliConfig + profileConfigs.Configs[profile] = configWithoutCredentials(CliConfig) newConfigDataByte, err := yaml.Marshal(profileConfigs.Configs) if err != nil { return err @@ -95,11 +105,82 @@ func Write() error { return fmt.Errorf("%s config file does not exist", profileConfigs.File) } profile := profileConfigs.Profile - profileConfigs.Configs[profile] = CliConfig + profileConfigs.Configs[profile] = configWithoutCredentials(CliConfig) newConfigDataByte, err := yaml.Marshal(profileConfigs.Configs) if err != nil { return err } err = os.WriteFile(profileConfigs.File, newConfigDataByte, fs.FileMode(0644)) - return err + if err != nil { + return err + } + return writeCredentials(profile) +} + +func configWithoutCredentials(config CoreConfig) CoreConfig { + config.Token = "" + config.CertificatePath = "" + config.CertificateKeyPath = "" + return config +} + +func credentialsPath() (string, error) { + home, err := os.UserHomeDir() + if err != nil { + return "", err + } + return filepath.Join(home, credentialsDir, credentialsFileName), nil +} + +func loadCredentials(profile string) error { + file, err := credentialsPath() + if err != nil { + return err + } + data, err := os.ReadFile(file) + if err != nil { + if os.IsNotExist(err) { + return nil + } + return err + } + credentials := map[string]CoreConfig{} + if err := yaml.Unmarshal(data, &credentials); err != nil { + return err + } + profileCredentials := credentials[profile] + CliConfig.Token = profileCredentials.Token + CliConfig.CertificatePath = profileCredentials.CertificatePath + CliConfig.CertificateKeyPath = profileCredentials.CertificateKeyPath + return nil +} + +func writeCredentials(profile string) error { + file, err := credentialsPath() + if err != nil { + return err + } + credentials := map[string]CoreConfig{} + data, err := os.ReadFile(file) + if err != nil && !os.IsNotExist(err) { + return err + } + if len(data) > 0 { + if err := yaml.Unmarshal(data, &credentials); err != nil { + return err + } + } + credentials[profile] = CoreConfig{ + Token: CliConfig.Token, + CertificatePath: CliConfig.CertificatePath, + CertificateKeyPath: CliConfig.CertificateKeyPath, + } + newCredentialData, err := yaml.Marshal(credentials) + if err != nil { + return err + } + if err := os.MkdirAll(filepath.Dir(file), fs.FileMode(0700)); err != nil { + return err + } + return os.WriteFile(file, newCredentialData, fs.FileMode(0600)) }