diff --git a/core/cli/configure.go b/core/cli/configure.go index a533aba..c404949 100644 --- a/core/cli/configure.go +++ b/core/cli/configure.go @@ -102,7 +102,34 @@ func runE(cmd *cobra.Command, _ []string) error { return err } - resp, err := client.New(url) + // Prompt for optional auth token (leave blank to skip) + token, err := tui.StringPrompt("enter auth token (leave blank to skip)", "", config.CliConfig.Token) + if err != nil { + return err + } + + // Prompt for optional TLS certificate path + certPath, err := tui.StringPrompt("enter TLS cert path (leave blank to skip)", "", config.CliConfig.CertPath) + if err != nil { + return err + } + + // Prompt for optional TLS certificate key path + certKeyPath, err := tui.StringPrompt("enter TLS cert key path (leave blank to skip)", "", config.CliConfig.CertKeyPath) + if err != nil { + return err + } + + opts := client.Options{ + Token: token, + CertPath: certPath, + CertKeyPath: certKeyPath, + } + + resp, err := client.New(url, opts) + if err != nil { + return fmt.Errorf("connecting to permify: %w", err) + } // Todo: Implement pagination tenants, err := resp.Tenancy.List(context.Background(), &v1.TenantListRequest{}) @@ -117,13 +144,18 @@ 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.Token = token + config.CliConfig.CertPath = certPath + config.CliConfig.CertKeyPath = certKeyPath config.CliConfig.Tenant = tenantIds[tenant] + err = config.Write() if err != nil { logger.Log.Error(err) diff --git a/core/client/grpc.go b/core/client/grpc.go index 11835df..3841812 100644 --- a/core/client/grpc.go +++ b/core/client/grpc.go @@ -2,19 +2,95 @@ 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) { +// tokenCredentials implements grpc.PerRPCCredentials for bearer token auth. +type tokenCredentials struct { + token string +} + +func (t tokenCredentials) GetRequestMetadata(_ context.Context, _ ...string) (map[string]string, error) { + return map[string]string{ + "authorization": "Bearer " + t.token, + }, nil +} + +func (t tokenCredentials) RequireTransportSecurity() bool { + return false +} + +// Options holds optional connection parameters loaded from stored credentials. +type Options struct { + Token string + CertPath string + CertKeyPath string +} + +// New initializes a new permify client using the provided endpoint and optional +// credentials. When CertPath and CertKeyPath are set, mTLS is used. When only +// Token is set, a bearer token is attached to every request. +func New(endpoint string, opts Options) (*permify.Client, error) { + var dialOpts []grpc.DialOption + + hasCert := opts.CertPath != "" && opts.CertKeyPath != "" + + if hasCert { + cert, err := tls.LoadX509KeyPair(opts.CertPath, opts.CertKeyPath) + if err != nil { + return nil, fmt.Errorf("loading TLS key pair: %w", err) + } + + tlsCfg := &tls.Config{ + Certificates: []tls.Certificate{cert}, + RootCAs: x509.NewCertPool(), + } + + // Attempt to append system root CAs; proceed without them if unavailable. + if pool, err := x509.SystemCertPool(); err == nil { + tlsCfg.RootCAs = pool + } + + dialOpts = append(dialOpts, grpc.WithTransportCredentials(credentials.NewTLS(tlsCfg))) + } else { + dialOpts = append(dialOpts, grpc.WithTransportCredentials(insecure.NewCredentials())) + } + + if opts.Token != "" { + dialOpts = append(dialOpts, grpc.WithPerRPCCredentials(tokenCredentials{token: opts.Token})) + } + client, err := permify.NewClient( permify.Config{ Endpoint: endpoint, }, - // Todo: Implement secure call with tls certificate - grpc.WithTransportCredentials(insecure.NewCredentials()), + dialOpts..., ) return client, err } + +// NewFromEnv initializes a permify client, preferring environment variables +// PERMIFY_TOKEN, PERMIFY_CERT_PATH, and PERMIFY_CERT_KEY_PATH over the stored +// options when they are set. +func NewFromEnv(endpoint string, stored Options) (*permify.Client, error) { + opts := stored + if v := os.Getenv("PERMIFY_TOKEN"); v != "" { + opts.Token = v + } + if v := os.Getenv("PERMIFY_CERT_PATH"); v != "" { + opts.CertPath = v + } + if v := os.Getenv("PERMIFY_CERT_KEY_PATH"); v != "" { + opts.CertKeyPath = v + } + return New(endpoint, opts) +} diff --git a/core/cmd/data/client.go b/core/cmd/data/client.go index a567b61..8854a72 100644 --- a/core/cmd/data/client.go +++ b/core/cmd/data/client.go @@ -10,10 +10,15 @@ import ( ) func Client() v1.DataClient { - c, err := client.New(config.CliConfig.PermifyURL) + opts := client.Options{ + Token: config.CliConfig.Token, + CertPath: config.CliConfig.CertPath, + CertKeyPath: config.CliConfig.CertKeyPath, + } + c, err := client.NewFromEnv(config.CliConfig.PermifyURL, opts) if err != nil { log.Error("Error initializing permify client. Check the configuration or rerun `permify configure`") - os.Exit(-1) + os.Exit(-1) } return c.Data -} \ No newline at end of file +} diff --git a/core/cmd/permission/client.go b/core/cmd/permission/client.go index 092f240..33dc4e8 100644 --- a/core/cmd/permission/client.go +++ b/core/cmd/permission/client.go @@ -10,10 +10,15 @@ import ( ) func Client() v1.PermissionClient { - c, err := client.New(config.CliConfig.PermifyURL) + opts := client.Options{ + Token: config.CliConfig.Token, + CertPath: config.CliConfig.CertPath, + CertKeyPath: config.CliConfig.CertKeyPath, + } + c, err := client.NewFromEnv(config.CliConfig.PermifyURL, opts) if err != nil { log.Error("Error initializing permify client. Check the configuration or rerun `permify configure`") - os.Exit(-1) + os.Exit(-1) } return c.Permission -} \ No newline at end of file +} diff --git a/core/cmd/schema/client.go b/core/cmd/schema/client.go index 6d0f3c1..9d34a0e 100644 --- a/core/cmd/schema/client.go +++ b/core/cmd/schema/client.go @@ -10,10 +10,15 @@ import ( ) func Client() v1.SchemaClient { - c, err := client.New(config.CliConfig.PermifyURL) + opts := client.Options{ + Token: config.CliConfig.Token, + CertPath: config.CliConfig.CertPath, + CertKeyPath: config.CliConfig.CertKeyPath, + } + c, err := client.NewFromEnv(config.CliConfig.PermifyURL, opts) if err != nil { log.Error("Error initializing permify client. Check the configuration or rerun `permify configure`") - os.Exit(-1) + os.Exit(-1) } return c.Schema -} \ No newline at end of file +} diff --git a/core/cmd/tenancy/client.go b/core/cmd/tenancy/client.go index 74c8213..f729250 100644 --- a/core/cmd/tenancy/client.go +++ b/core/cmd/tenancy/client.go @@ -10,10 +10,15 @@ import ( ) func Client() v1.TenancyClient { - c, err := client.New(config.CliConfig.PermifyURL) + opts := client.Options{ + Token: config.CliConfig.Token, + CertPath: config.CliConfig.CertPath, + CertKeyPath: config.CliConfig.CertKeyPath, + } + c, err := client.NewFromEnv(config.CliConfig.PermifyURL, opts) if err != nil { log.Error("Error initializing permify client. Check the configuration or rerun `permify configure`") - os.Exit(-1) + os.Exit(-1) } return c.Tenancy -} \ No newline at end of file +} 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