From 7181c1444c824adbeca121485f4eae0e06c1f622 Mon Sep 17 00:00:00 2001 From: defgtyg <141336881+defgtyg@users.noreply.github.com> Date: Fri, 19 Jun 2026 20:50:11 +0800 Subject: [PATCH 1/6] Add stored client credential fields Store token and TLS certificate paths alongside existing Permify endpoint and tenant configuration. --- core/config/config.go | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/core/config/config.go b/core/config/config.go index c9bdebb..be8b55a 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"` + CertificatePath string `yaml:"certificate_path,omitempty"` + CertificateKeyPath string `yaml:"certificate_key_path,omitempty"` + SslEnabled bool `yaml:"-"` } // IsConfigured checks if permctl cli has been configured From 5ac1856e8bc5481bb183aa91bd251a1d086d0472 Mon Sep 17 00:00:00 2001 From: defgtyg <141336881+defgtyg@users.noreply.github.com> Date: Fri, 19 Jun 2026 20:50:49 +0800 Subject: [PATCH 2/6] Use stored credentials when creating client Load endpoint, token, and TLS certificate settings from CLI config during Permify client initialization. --- core/client/grpc.go | 51 ++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 48 insertions(+), 3 deletions(-) diff --git a/core/client/grpc.go b/core/client/grpc.go index 11835df..f26e75e 100644 --- a/core/client/grpc.go +++ b/core/client/grpc.go @@ -2,19 +2,64 @@ package client import ( + "crypto/tls" + "fmt" + + "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 + } + + 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 +} From 6a1fab316b5c13ed84668dde829214f258a25ac0 Mon Sep 17 00:00:00 2001 From: defgtyg <141336881+defgtyg@users.noreply.github.com> Date: Fri, 19 Jun 2026 20:51:39 +0800 Subject: [PATCH 3/6] Persist credentials from configure command Prompt for optional token and TLS certificate paths and save them into the active profile. --- core/cli/configure.go | 24 ++++++++++++++++++++++-- 1 file changed, 22 insertions(+), 2 deletions(-) diff --git a/core/cli/configure.go b/core/cli/configure.go index a533aba..a296333 100644 --- a/core/cli/configure.go +++ b/core/cli/configure.go @@ -101,8 +101,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 = config.CliConfig.PermifyURL != "" && config.CliConfig.PermifyURL[:5] == "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 +138,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 { From 94824c4b964ca1dad1f4f803472a406ec6d44f1e Mon Sep 17 00:00:00 2001 From: defgtyg <141336881+defgtyg@users.noreply.github.com> Date: Fri, 19 Jun 2026 20:52:12 +0800 Subject: [PATCH 4/6] Avoid unsafe URL prefix check Use strings.HasPrefix when deriving TLS mode from the configured endpoint. --- core/cli/configure.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/core/cli/configure.go b/core/cli/configure.go index a296333..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" @@ -118,7 +119,7 @@ func runE(cmd *cobra.Command, _ []string) error { config.CliConfig.Token = token config.CliConfig.CertificatePath = certificatePath config.CliConfig.CertificateKeyPath = certificateKeyPath - config.CliConfig.SslEnabled = config.CliConfig.PermifyURL != "" && config.CliConfig.PermifyURL[:5] == "https" + config.CliConfig.SslEnabled = strings.HasPrefix(config.CliConfig.PermifyURL, "https") resp, err := client.New(url) if err != nil { From 2c7254677450bf0d266460a774186f305c278c39 Mon Sep 17 00:00:00 2001 From: defgtyg <141336881+defgtyg@users.noreply.github.com> Date: Fri, 19 Jun 2026 20:54:33 +0800 Subject: [PATCH 5/6] Derive TLS mode from effective endpoint Keep client credential selection consistent when New receives an explicit endpoint override. --- core/client/grpc.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/core/client/grpc.go b/core/client/grpc.go index f26e75e..a03b9a4 100644 --- a/core/client/grpc.go +++ b/core/client/grpc.go @@ -4,6 +4,7 @@ package client import ( "crypto/tls" "fmt" + "strings" "github.com/Permify/permify-cli/core/config" permify "github.com/Permify/permify-go/v1" @@ -17,6 +18,7 @@ func New(endpoint string) (*permify.Client, error) { clientConfig := config.CliConfig if endpoint != "" { clientConfig.PermifyURL = endpoint + clientConfig.SslEnabled = strings.HasPrefix(endpoint, "https") } options := []grpc.DialOption{} From c200746c442dfd6c08370408c3f8a54bfc1ace4a Mon Sep 17 00:00:00 2001 From: defgtyg <141336881+defgtyg@users.noreply.github.com> Date: Fri, 19 Jun 2026 21:00:27 +0800 Subject: [PATCH 6/6] Store credentials in dedicated credentials file Persist token and certificate paths under ~/.permify/credentials while keeping endpoint and tenant in the existing config file. --- core/config/config.go | 84 +++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 81 insertions(+), 3 deletions(-) diff --git a/core/config/config.go b/core/config/config.go index be8b55a..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{} @@ -73,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 } @@ -82,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 @@ -98,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)) }