Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 34 additions & 0 deletions SUBMISSION_PACKET_2.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
# Submission Packet — Permify/permify-cli Issue #2

Issue: https://github.com/Permify/permify-cli/issues/2
Algora org page: https://algora.io/Permify

## Goal
Implement persistent storage (via the existing CLI config YAML) for:
- endpoint
- bearer token (optional)
- TLS client cert path (optional)
- TLS client cert key path (optional)

Then ensure the permify client uses the stored values during client creation.

## Implementation summary
- Extend `core/config.CoreConfig` to include `token`, `cert_path`, `cert_key` fields in YAML.
- Update `permctl configure` flow to prompt for token/cert paths before listing tenants, store these values to config, then persist to YAML.
- Update `core/client.New` to:
- use TLS when endpoint is `https://`
- optionally attach `authorization: Bearer <token>` via `grpc.WithPerRPCCredentials` (TLS-only)
- optionally present a client cert (mTLS) when both `cert_path` and `cert_key` are provided

## Patch
Local patch file (generated from branch `feat/bounty-2-credential-storage`):
- `Permify-permify-cli-2-credential-storage.patch`

## How to submit (manual)
1) In `/Users/mavenai/Desktop/Singaw Sity Labs/Singaw Sity Codex/bounty-work/permify-cli`, push branch `feat/bounty-2-credential-storage` to your fork.
2) Open a PR to `Permify/permify-cli` targeting `main`.
3) Post a concise public comment on the issue linking the PR (and `/attempt` if Algora requires it).

## Notes / safety
- Token is only sent over TLS; if endpoint is not `https://`, client creation errors.
- No credentials are embedded in code; this only changes config schema + client wiring.
44 changes: 42 additions & 2 deletions core/cli/configure.go
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,31 @@ func runE(cmd *cobra.Command, _ []string) error {
return err
}

token, err := tui.StringPrompt("enter auth token (optional, bearer token)", "", config.CliConfig.Token)
if err != nil {
return err
}

certPath, err := tui.StringPrompt("enter tls client cert path (optional)", "", config.CliConfig.CertPath)
if err != nil {
return err
}

certKey, err := tui.StringPrompt("enter tls client cert key path (optional)", "", config.CliConfig.CertKey)
if err != nil {
return err
}

// Use the provided values to validate connectivity and list tenants.
config.CliConfig.PermifyURL = url
config.CliConfig.Token = token
config.CliConfig.CertPath = certPath
config.CliConfig.CertKey = certKey

resp, err := client.New(url)
if err != nil {
return err
}

// Todo: Implement pagination
tenants, err := resp.Tenancy.List(context.Background(), &v1.TenantListRequest{})
Expand All @@ -122,12 +146,28 @@ func runE(cmd *cobra.Command, _ []string) error {
if err != nil {
logger.Log.Error(err)
}
config.CliConfig.PermifyURL = url
config.CliConfig.Tenant = tenantIds[tenant]
err = config.Write()

// Store connection credentials separately to avoid repeatedly prompting users.
credentialsFile, err := config.WriteCredentials(config.Credentials{
Endpoint: url,
Token: token,
CertPath: certPath,
CertKey: certKey,
})
if err != nil {
return err
}

// Avoid persisting tokens/cert paths in the main config YAML.
config.CliConfig.Token = ""
config.CliConfig.CertPath = ""
config.CliConfig.CertKey = ""

if err := config.Write(); err != nil {
logger.Log.Error(err)
}
logger.Log.Info("successfully configured ", "config file", configFile)
logger.Log.Info("saved connection credentials", "credentials file", credentialsFile)
return nil
}
64 changes: 62 additions & 2 deletions core/client/grpc.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,19 +2,79 @@
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) {
creds, err := config.LoadCredentials()
if err != nil {
return nil, err
}

if endpoint == "" {
if creds.Endpoint != "" {
endpoint = creds.Endpoint
} else {
endpoint = config.CliConfig.PermifyURL
}
}

dialOptions := []grpc.DialOption{}
isTLS := strings.HasPrefix(endpoint, "https://")

if isTLS {
tlsConfig := &tls.Config{}

certPath := creds.CertPath
certKey := creds.CertKey
if certPath == "" && certKey == "" {
certPath = config.CliConfig.CertPath
certKey = config.CliConfig.CertKey
}

if certPath != "" || certKey != "" {
if certPath == "" || certKey == "" {
return nil, fmt.Errorf("both cert_path and cert_key must be set for mTLS")
}
certPair, err := tls.LoadX509KeyPair(certPath, certKey)
if err != nil {
return nil, fmt.Errorf("load mTLS cert/key: %w", err)
}
tlsConfig.Certificates = []tls.Certificate{certPair}
}

dialOptions = append(dialOptions, grpc.WithTransportCredentials(credentials.NewTLS(tlsConfig)))
} else {
dialOptions = append(dialOptions, grpc.WithTransportCredentials(insecure.NewCredentials()))
}

token := creds.Token
if token == "" {
token = config.CliConfig.Token
}
if token != "" {
if !isTLS {
return nil, fmt.Errorf("refusing to send token over insecure transport (set an https:// endpoint)")
}
dialOptions = append(dialOptions, grpc.WithPerRPCCredentials(secureTokenCredentials{
"authorization": "Bearer " + token,
}))
}

client, err := permify.NewClient(
permify.Config{
Endpoint: endpoint,
},
// Todo: Implement secure call with tls certificate
grpc.WithTransportCredentials(insecure.NewCredentials()),
dialOptions...,
)
return client, err
}
14 changes: 11 additions & 3 deletions core/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,17 @@ 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 is an optional bearer token used for API authentication.
Token string `yaml:"token,omitempty"`

// CertPath/CertKey are optional paths to a TLS client certificate/key (mTLS).
CertPath string `yaml:"cert_path,omitempty"`
CertKey string `yaml:"cert_key,omitempty"`

SslEnabled bool `yaml:"-"`
}

// IsConfigured checks if permctl cli has been configured
Expand Down
71 changes: 71 additions & 0 deletions core/config/credentials.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
package config

import (
"errors"
"fmt"
"io/fs"
"os"
"path/filepath"
"runtime"

"gopkg.in/yaml.v3"
)

// Credentials are stored separately from the main profile YAML config to avoid
// repeatedly prompting users for connection details.
type Credentials struct {
Endpoint string `yaml:"endpoint,omitempty"`
Token string `yaml:"token,omitempty"`
CertPath string `yaml:"cert_path,omitempty"`
CertKey string `yaml:"cert_key,omitempty"`
}

func credentialsPath() (string, error) {
home, err := os.UserHomeDir()
if err != nil {
return "", err
}
if runtime.GOOS == "windows" {
return filepath.Join(home, ".permify", "credentials"), nil
}
return filepath.Join(home, ".permify", "credentials"), nil
}

func LoadCredentials() (Credentials, error) {
path, err := credentialsPath()
if err != nil {
return Credentials{}, err
}
data, err := os.ReadFile(path)
if err != nil {
if errors.Is(err, os.ErrNotExist) {
return Credentials{}, nil
}
return Credentials{}, err
}
var creds Credentials
if err := yaml.Unmarshal(data, &creds); err != nil {
return Credentials{}, fmt.Errorf("unmarshal credentials: %w", err)
}
return creds, nil
}

func WriteCredentials(creds Credentials) (string, error) {
path, err := credentialsPath()
if err != nil {
return "", err
}
if err := os.MkdirAll(filepath.Dir(path), 0o700); err != nil {
return "", err
}
data, err := yaml.Marshal(creds)
if err != nil {
return "", err
}
// Credentials may include tokens; restrict permissions.
if err := os.WriteFile(path, data, fs.FileMode(0o600)); err != nil {
return "", err
}
return path, nil
}