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 logo
- Permify CLI - Command Line Interface for Permify -

- -

- Permify CLI Go Version  - Permify CLI Licence  - Permify Discord Channel  -

- -## 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 logo
+ Permify CLI - Command Line Interface for Permify +

+ +

+ Permify CLI Go Version  + Permify CLI Licence  + Permify Discord Channel  +

+ +## 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: - -

- - permify | Discord - - - permify | Twitter - - - permify | Linkedin - -

\ No newline at end of file + +

+ + permify | Discord + + + permify | Twitter + + + permify | Linkedin + +

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") + } +}