From 27e3eec0e3f42ec0cb7ee36aaacc8b26af9628f4 Mon Sep 17 00:00:00 2001
From: lin-zhiying <531554092@qq.com>
Date: Sat, 6 Jun 2026 15:20:35 +0800
Subject: [PATCH] feat: persist permify cli credentials
---
README.md | 87 ++++++++++++++++++++------------------
core/cli/configure.go | 28 +++++++++++-
core/client/grpc.go | 72 ++++++++++++++++++++++++++++++-
core/client/grpc_test.go | 41 ++++++++++++++++++
core/config/config.go | 9 ++--
core/config/config_test.go | 51 ++++++++++++++++++++++
6 files changed, 241 insertions(+), 47 deletions(-)
create mode 100644 core/client/grpc_test.go
create mode 100644 core/config/config_test.go
diff --git a/README.md b/README.md
index 67356b9..0f24e29 100644
--- a/README.md
+++ b/README.md
@@ -1,44 +1,51 @@
-
- 
- Permify CLI - Command Line Interface for Permify
-
-
-
-
-
-
-
-
-## What is Permify?
-
-[Permify](https://github.com/Permify/permify) is a open-source authorization service for creating and managing fine-grained permissions in your applications and services. Inspired by Google’s consistent, global authorization system, [Google Zanzibar](https://storage.googleapis.com/pub-tools-public-publication-data/pdf/41f08f03da59f5518802898f68730e247e23c331.pdf)
-
-Our goal is to make Google's Zanzibar available to everyone and help them build robust, flexible, and easily auditable authorization systems that perform well in scaled environments.
-
-### With Permify, you can:
-
-🔮 Create permissions and policies using [Permify's flexible authorization language](https://docs.permify.co/docs/getting-started/modeling) that is compatible with traditional roles and permissions (RBAC), arbitrary relations between users and objects (ReBAC), and attributes (ABAC).
-
-🔐 [Manage and store authorization data](https://docs.permify.co/docs/getting-started/sync-data) in your preferred database with high availability and consistency.
-
-✅ [Interact with the Permify API](https://docs.permify.co/docs/getting-started/enforcement) to perform access checks, filter your resources with specific permissions, perform bulk permission checks for various resources, and more.
+
+ 
+ Permify CLI - Command Line Interface for Permify
+
+
+
+
+
+
+
+
+## What is Permify?
+
+[Permify](https://github.com/Permify/permify) is a open-source authorization service for creating and managing fine-grained permissions in your applications and services. Inspired by Google’s consistent, global authorization system, [Google Zanzibar](https://storage.googleapis.com/pub-tools-public-publication-data/pdf/41f08f03da59f5518802898f68730e247e23c331.pdf)
+
+Our goal is to make Google's Zanzibar available to everyone and help them build robust, flexible, and easily auditable authorization systems that perform well in scaled environments.
+
+### With Permify, you can:
+
+🔮 Create permissions and policies using [Permify's flexible authorization language](https://docs.permify.co/docs/getting-started/modeling) that is compatible with traditional roles and permissions (RBAC), arbitrary relations between users and objects (ReBAC), and attributes (ABAC).
+
+🔐 [Manage and store authorization data](https://docs.permify.co/docs/getting-started/sync-data) in your preferred database with high availability and consistency.
+
+✅ [Interact with the Permify API](https://docs.permify.co/docs/getting-started/enforcement) to perform access checks, filter your resources with specific permissions, perform bulk permission checks for various resources, and more.
+
+🧪 Test your authorization logic with [Permify's schema testing](https://docs.permify.co/docs/getting-started/testing). You can conduct scenario-based testing, policy coverage analysis, and IDL parser integration to achieve end-to-end validations for your desired authorization schema.
+
+⚙️ Create custom and isolated authorization models for different applications using Permify [Multi-Tenancy](https://docs.permify.co/docs/use-cases/multi-tenancy) support, all managed within a single place, Permify instance.
+
+## Communication Channels
-🧪 Test your authorization logic with [Permify's schema testing](https://docs.permify.co/docs/getting-started/testing). You can conduct scenario-based testing, policy coverage analysis, and IDL parser integration to achieve end-to-end validations for your desired authorization schema.
+## Configuration Credentials
-⚙️ Create custom and isolated authorization models for different applications using Permify [Multi-Tenancy](https://docs.permify.co/docs/use-cases/multi-tenancy) support, all managed within a single place, Permify instance.
-
-## Communication Channels
+`permctl configure` stores the selected endpoint and tenant in the active
+profile. It also supports optional `token`, `cert_path`, and `cert_key_path`
+values so later client creation can reuse API credentials and TLS settings from
+the same config file.
If you like Permify, please consider giving us a :star:
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/core/cli/configure.go b/core/cli/configure.go
index a533aba..6d7a9fe 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"
@@ -102,7 +103,31 @@ func runE(cmd *cobra.Command, _ []string) error {
return err
}
+ token, err := tui.StringPrompt("enter auth token (optional)", "", config.CliConfig.Token)
+ if err != nil {
+ return err
+ }
+
+ certPath, err := tui.StringPrompt("enter TLS cert path (optional)", "", config.CliConfig.CertPath)
+ if err != nil {
+ return err
+ }
+
+ certKeyPath, err := tui.StringPrompt("enter TLS cert key path (optional)", "", config.CliConfig.CertKeyPath)
+ if err != nil {
+ return err
+ }
+
+ config.CliConfig.PermifyURL = url
+ config.CliConfig.Token = token
+ config.CliConfig.CertPath = certPath
+ config.CliConfig.CertKeyPath = certKeyPath
+ config.CliConfig.SslEnabled = strings.HasPrefix(url, "https")
+
resp, err := client.New(url)
+ if err != nil {
+ return err
+ }
// Todo: Implement pagination
tenants, err := resp.Tenancy.List(context.Background(), &v1.TenantListRequest{})
@@ -117,12 +142,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..0cd627e 100644
--- a/core/client/grpc.go
+++ b/core/client/grpc.go
@@ -2,19 +2,87 @@
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) {
+ if endpoint == "" {
+ endpoint = config.CliConfig.PermifyURL
+ }
+ if endpoint == "" {
+ return nil, fmt.Errorf("permify endpoint is missing")
+ }
+
+ opts, err := dialOptions(config.CliConfig)
+ if err != nil {
+ return nil, err
+ }
+
client, err := permify.NewClient(
permify.Config{
Endpoint: endpoint,
},
- // Todo: Implement secure call with tls certificate
- grpc.WithTransportCredentials(insecure.NewCredentials()),
+ opts...,
)
return client, err
}
+
+func dialOptions(cfg config.CoreConfig) ([]grpc.DialOption, error) {
+ opts := []grpc.DialOption{}
+
+ transportCredentials, err := transportCredentials(cfg)
+ if err != nil {
+ return nil, err
+ }
+ opts = append(opts, grpc.WithTransportCredentials(transportCredentials))
+
+ if cfg.Token != "" {
+ token := cfg.Token
+ if !strings.HasPrefix(strings.ToLower(token), "bearer ") {
+ token = "Bearer " + token
+ }
+ if cfg.SslEnabled || cfg.CertPath != "" {
+ opts = append(opts, grpc.WithPerRPCCredentials(secureTokenCredentials{"authorization": token}))
+ } else {
+ opts = append(opts, grpc.WithPerRPCCredentials(nonSecureTokenCredentials{"authorization": token}))
+ }
+ }
+
+ return opts, nil
+}
+
+func transportCredentials(cfg config.CoreConfig) (credentials.TransportCredentials, error) {
+ if cfg.CertPath != "" && cfg.CertKeyPath != "" {
+ cert, err := tls.LoadX509KeyPair(cfg.CertPath, cfg.CertKeyPath)
+ if err != nil {
+ return nil, fmt.Errorf("failed to load TLS certificate pair: %w", err)
+ }
+ return credentials.NewTLS(&tls.Config{
+ Certificates: []tls.Certificate{cert},
+ MinVersion: tls.VersionTLS12,
+ }), nil
+ }
+
+ if cfg.CertKeyPath != "" {
+ return nil, fmt.Errorf("cert_key_path requires cert_path")
+ }
+
+ if cfg.CertPath != "" {
+ return credentials.NewClientTLSFromFile(cfg.CertPath, "")
+ }
+
+ if cfg.SslEnabled {
+ return credentials.NewTLS(&tls.Config{MinVersion: tls.VersionTLS12}), nil
+ }
+
+ return insecure.NewCredentials(), nil
+}
diff --git a/core/client/grpc_test.go b/core/client/grpc_test.go
new file mode 100644
index 0000000..9816235
--- /dev/null
+++ b/core/client/grpc_test.go
@@ -0,0 +1,41 @@
+package client
+
+import (
+ "testing"
+
+ "github.com/Permify/permify-cli/core/config"
+)
+
+func TestDialOptionsAllowStoredTokenWithoutTLS(t *testing.T) {
+ opts, err := dialOptions(config.CoreConfig{
+ Token: "secret-token",
+ })
+ if err != nil {
+ t.Fatal(err)
+ }
+ if len(opts) != 2 {
+ t.Fatalf("expected transport and token options, got %d", len(opts))
+ }
+}
+
+func TestDialOptionsAllowStoredTokenWithTLS(t *testing.T) {
+ opts, err := dialOptions(config.CoreConfig{
+ SslEnabled: true,
+ Token: "secret-token",
+ })
+ if err != nil {
+ t.Fatal(err)
+ }
+ if len(opts) != 2 {
+ t.Fatalf("expected TLS transport and token options, got %d", len(opts))
+ }
+}
+
+func TestDialOptionsRejectCertKeyWithoutCert(t *testing.T) {
+ _, err := dialOptions(config.CoreConfig{
+ CertKeyPath: "/tmp/client.key",
+ })
+ if err == nil {
+ t.Fatal("expected cert_key_path without cert_path to fail")
+ }
+}
diff --git a/core/config/config.go b/core/config/config.go
index c9bdebb..4e7c961 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"`
+ CertKeyPath string `yaml:"cert_key_path,omitempty"`
+ SslEnabled bool `yaml:"-"`
}
// IsConfigured checks if permctl cli has been configured
diff --git a/core/config/config_test.go b/core/config/config_test.go
new file mode 100644
index 0000000..8b60a2d
--- /dev/null
+++ b/core/config/config_test.go
@@ -0,0 +1,51 @@
+package config
+
+import (
+ "os"
+ "testing"
+)
+
+func TestConfigPersistsConnectionCredentials(t *testing.T) {
+ file, err := os.CreateTemp(t.TempDir(), "permctl-*.yaml")
+ if err != nil {
+ t.Fatal(err)
+ }
+ file.Close()
+
+ CliConfig = CoreConfig{
+ PermifyURL: "https://permify.example.com:3478",
+ Tenant: "tenant-1",
+ Token: "secret-token",
+ CertPath: "/tmp/client.crt",
+ CertKeyPath: "/tmp/client.key",
+ }
+
+ if err := New(file.Name(), "default"); err != nil {
+ t.Fatal(err)
+ }
+
+ CliConfig = CoreConfig{}
+
+ if err := Load(file.Name(), "default"); err != nil {
+ t.Fatal(err)
+ }
+
+ if CliConfig.PermifyURL != "https://permify.example.com:3478" {
+ t.Fatalf("unexpected endpoint: %q", CliConfig.PermifyURL)
+ }
+ if CliConfig.Tenant != "tenant-1" {
+ t.Fatalf("unexpected tenant: %q", CliConfig.Tenant)
+ }
+ if CliConfig.Token != "secret-token" {
+ t.Fatalf("unexpected token: %q", CliConfig.Token)
+ }
+ if CliConfig.CertPath != "/tmp/client.crt" {
+ t.Fatalf("unexpected cert path: %q", CliConfig.CertPath)
+ }
+ if CliConfig.CertKeyPath != "/tmp/client.key" {
+ t.Fatalf("unexpected cert key path: %q", CliConfig.CertKeyPath)
+ }
+ if !CliConfig.SslEnabled {
+ t.Fatal("expected https endpoint to enable TLS")
+ }
+}