From 49fe51a9c07a011de88dc724e43bbade147fc672 Mon Sep 17 00:00:00 2001 From: Gautam Kumar Date: Fri, 19 Jun 2026 16:26:06 +0530 Subject: [PATCH] feat: add credential storage for endpoint, token, cert path and cert key Store and retrieve endpoint, token, cert path, and cert key in the CLI configuration. Supports token-based authentication via per-RPC credentials and TLS via certificate/key file pairs. Credentials are prompted during 'permctl configure' and persisted in the YAML config file. Closes #2 Signed-off-by: Gautam Kumar --- core/cli/configure.go | 19 ++++++++ core/client/grpc.go | 82 +++++++++++++++++++++++++++++++++-- core/cmd/data/client.go | 8 +++- core/cmd/permission/client.go | 8 +++- core/cmd/schema/client.go | 8 +++- core/cmd/tenancy/client.go | 8 +++- core/config/config.go | 15 ++++++- 7 files changed, 139 insertions(+), 9 deletions(-) diff --git a/core/cli/configure.go b/core/cli/configure.go index a533aba..ed91265 100644 --- a/core/cli/configure.go +++ b/core/cli/configure.go @@ -124,6 +124,25 @@ func runE(cmd *cobra.Command, _ []string) error { } config.CliConfig.PermifyURL = url config.CliConfig.Tenant = tenantIds[tenant] + + token, err := tui.StringPrompt("enter authentication token (optional, press enter to skip)", "token", config.CliConfig.Token) + if err != nil { + return err + } + config.CliConfig.Token = token + + certPath, err := tui.StringPrompt("enter TLS cert path (optional, press enter to skip)", "/path/to/cert.pem", config.CliConfig.CertPath) + if err != nil { + return err + } + config.CliConfig.CertPath = certPath + + certKey, err := tui.StringPrompt("enter TLS cert key path (optional, press enter to skip)", "/path/to/key.pem", config.CliConfig.CertKey) + if err != nil { + return err + } + config.CliConfig.CertKey = certKey + err = config.Write() if err != nil { logger.Log.Error(err) diff --git a/core/client/grpc.go b/core/client/grpc.go index 11835df..d681561 100644 --- a/core/client/grpc.go +++ b/core/client/grpc.go @@ -2,19 +2,93 @@ package client import ( + "context" + "crypto/tls" + "crypto/x509" + "fmt" + "os" + 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) { +// Credentials holds authentication and TLS configuration for the permify client +type Credentials struct { + Token string + CertPath string + CertKey string +} + +// New initializes a new permify client with the given endpoint and optional credentials +func New(endpoint string, creds ...Credentials) (*permify.Client, error) { + var opts []grpc.DialOption + + if len(creds) > 0 && creds[0].Token != "" { + token := creds[0].Token + opts = append(opts, grpc.WithPerRPCCredentials(newTokenCredentials(token))) + } + + if len(creds) > 0 && creds[0].CertPath != "" { + tlsCreds, err := loadTLSCredentials(creds[0].CertPath, creds[0].CertKey) + if err != nil { + return nil, fmt.Errorf("failed to load TLS credentials: %w", err) + } + opts = append(opts, grpc.WithTransportCredentials(tlsCreds)) + } else { + opts = append(opts, grpc.WithTransportCredentials(insecure.NewCredentials())) + } + client, err := permify.NewClient( permify.Config{ Endpoint: endpoint, }, - // Todo: Implement secure call with tls certificate - grpc.WithTransportCredentials(insecure.NewCredentials()), + opts..., ) return client, err } + +// loadTLSCredentials loads TLS certificate and key from file paths +func loadTLSCredentials(certPath, certKeyPath string) (credentials.TransportCredentials, error) { + cert, err := tls.LoadX509KeyPair(certPath, certKeyPath) + if err != nil { + return nil, fmt.Errorf("failed to load key pair: %w", err) + } + + caCert, err := os.ReadFile(certPath) + if err != nil { + return nil, fmt.Errorf("failed to read CA certificate: %w", err) + } + + caCertPool := x509.NewCertPool() + if !caCertPool.AppendCertsFromPEM(caCert) { + return nil, fmt.Errorf("failed to parse CA certificate") + } + + tlsConfig := &tls.Config{ + Certificates: []tls.Certificate{cert}, + RootCAs: caCertPool, + } + + return credentials.NewTLS(tlsConfig), nil +} + +// tokenCredentials implements credentials.PerRPCCredentials for token-based auth +type tokenCredentials struct { + token string +} + +func newTokenCredentials(token string) *tokenCredentials { + return &tokenCredentials{token: token} +} + +func (t *tokenCredentials) GetRequestMetadata(ctx context.Context, uri ...string) (map[string]string, error) { + return map[string]string{ + "authorization": "Bearer " + t.token, + }, nil +} + +func (t *tokenCredentials) RequireTransportSecurity() bool { + return false +} diff --git a/core/cmd/data/client.go b/core/cmd/data/client.go index a567b61..52e0284 100644 --- a/core/cmd/data/client.go +++ b/core/cmd/data/client.go @@ -10,7 +10,13 @@ import ( ) func Client() v1.DataClient { - c, err := client.New(config.CliConfig.PermifyURL) + token, certPath, certKey := config.GetCredentials() + creds := client.Credentials{ + Token: token, + CertPath: certPath, + CertKey: certKey, + } + c, err := client.New(config.CliConfig.PermifyURL, creds) if err != nil { log.Error("Error initializing permify client. Check the configuration or rerun `permify configure`") os.Exit(-1) diff --git a/core/cmd/permission/client.go b/core/cmd/permission/client.go index 092f240..18b6451 100644 --- a/core/cmd/permission/client.go +++ b/core/cmd/permission/client.go @@ -10,7 +10,13 @@ import ( ) func Client() v1.PermissionClient { - c, err := client.New(config.CliConfig.PermifyURL) + token, certPath, certKey := config.GetCredentials() + creds := client.Credentials{ + Token: token, + CertPath: certPath, + CertKey: certKey, + } + c, err := client.New(config.CliConfig.PermifyURL, creds) if err != nil { log.Error("Error initializing permify client. Check the configuration or rerun `permify configure`") os.Exit(-1) diff --git a/core/cmd/schema/client.go b/core/cmd/schema/client.go index 6d0f3c1..2d4e2f6 100644 --- a/core/cmd/schema/client.go +++ b/core/cmd/schema/client.go @@ -10,7 +10,13 @@ import ( ) func Client() v1.SchemaClient { - c, err := client.New(config.CliConfig.PermifyURL) + token, certPath, certKey := config.GetCredentials() + creds := client.Credentials{ + Token: token, + CertPath: certPath, + CertKey: certKey, + } + c, err := client.New(config.CliConfig.PermifyURL, creds) if err != nil { log.Error("Error initializing permify client. Check the configuration or rerun `permify configure`") os.Exit(-1) diff --git a/core/cmd/tenancy/client.go b/core/cmd/tenancy/client.go index 74c8213..60d01c2 100644 --- a/core/cmd/tenancy/client.go +++ b/core/cmd/tenancy/client.go @@ -10,7 +10,13 @@ import ( ) func Client() v1.TenancyClient { - c, err := client.New(config.CliConfig.PermifyURL) + token, certPath, certKey := config.GetCredentials() + creds := client.Credentials{ + Token: token, + CertPath: certPath, + CertKey: certKey, + } + c, err := client.New(config.CliConfig.PermifyURL, creds) if err != nil { log.Error("Error initializing permify client. Check the configuration or rerun `permify configure`") os.Exit(-1) diff --git a/core/config/config.go b/core/config/config.go index c9bdebb..a11ed8d 100644 --- a/core/config/config.go +++ b/core/config/config.go @@ -27,6 +27,9 @@ type ProfileConfigs struct { type CoreConfig struct { 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:"-"` } @@ -53,6 +56,16 @@ func IsConfigured(file string, profile string) error { return nil } +// HasCredentials checks if credentials are configured for the current profile +func HasCredentials() bool { + return CliConfig.Token != "" || CliConfig.CertPath != "" +} + +// GetCredentials returns the stored credentials +func GetCredentials() (token, certPath, certKey string) { + return CliConfig.Token, CliConfig.CertPath, CliConfig.CertKey +} + // Load the permctl configuration specified by the user into the global variable func Load(file string, profile string) error { _, err := os.Stat(file) @@ -70,7 +83,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 }