Skip to content

feat(client): credential storage for endpoint/token/cert paths (#2)#30

Open
khoaminh2957 wants to merge 1 commit into
Permify:mainfrom
khoaminh2957:feature/credential-storage-issue-2
Open

feat(client): credential storage for endpoint/token/cert paths (#2)#30
khoaminh2957 wants to merge 1 commit into
Permify:mainfrom
khoaminh2957:feature/credential-storage-issue-2

Conversation

@khoaminh2957
Copy link
Copy Markdown

Closes #2

/claim #2

Summary

Implements local credential storage and retrieval for permctl, satisfying the requirements in #2 (storing endpoint, token, cert path, and cert key in the CLI and consuming them from client.New).

What's new

  • core/credentials/ new package

    • Credentials struct: endpoint, token, cert_path, cert_key (YAML-tagged)
    • Load(profile) / Save(profile, *Credentials) / Delete(profile) plus *From(path, ...) variants for explicit paths (used by tests)
    • GetCredentialsPath() resolves $PERMIFY_CREDENTIALS_FILE then $HOME/.permctl-credentials.yaml (falls back to os.UserHomeDir on Windows)
    • ErrNotFound sentinel for errors.Is callers
    • Security: file is created with 0600, parent dir with 0700; on POSIX, Load refuses to read a file that is world/group readable and prints chmod 600 instructions
    • Atomicity: writes go through CreateTemp + Chmod + Rename in the same directory
    • Multi-profile aware: existing entries are preserved when a different profile is saved; deleting the last profile removes the file
  • core/client/grpc.go modified New()

    • Loads stored credentials for the active profile (PERMIFY_PROFILE env override)
    • Builds grpc.DialOptions based on what's stored: mTLS via grpccreds.NewClientTLSFromFile when cert_path + cert_key are present, otherwise the previous insecure.NewCredentials()
    • Adds per-RPC bearer-token credentials when token is set (reusing the existing secureTokenCredentials / nonSecureTokenCredentials types already in core/client/utils.go); secure flavor over TLS, non-secure over plaintext
    • The endpoint argument still wins; falls back to the stored endpoint when blank; returns a clear error when both are empty
    • Backward compatible: if no credentials file exists, client.New behaves exactly as before (insecure transport, no token)
    • Explicit error when only one of cert_path / cert_key is configured
  • core/cli/auth.go new Cobra commands wired into cli.New()

    • permctl login --endpoint X --token Y --cert-path Z --cert-key K (merge-updates only the fields you pass; supports --profile)
    • permctl logout (removes the active profile; deletes the file when no profiles remain)
    • permctl whoami (prints the stored values; token is masked to first4...last4, short tokens become ****)
    • All three override the root PersistentPreRun so they remain usable before permctl configure has ever been run

Tests

core/credentials/credentials_test.go (13 cases, all passing on Windows; POSIX-only cases are skipped on Windows):

  • Missing file -> ErrNotFound
  • Missing profile -> ErrNotFound
  • Save -> Load round-trip
  • Multi-profile preservation across writes
  • File mode is 0600 after Save (POSIX)
  • Load refuses 0644 file with descriptive error (POSIX)
  • Save(nil) is rejected
  • Delete removes only the targeted profile
  • Delete removes the file when no profiles remain
  • Delete on a missing file is a no-op
  • GetCredentialsPath honors PERMIFY_CREDENTIALS_FILE
  • GetCredentialsPath defaults to $HOME/.permctl-credentials.yaml
  • Empty profile argument falls back to default

Dependencies

No new module dependencies. Uses already-vendored gopkg.in/yaml.v3, github.com/spf13/cobra, and google.golang.org/grpc/credentials (the last was added implicitly via the existing grpc import).

Notes for the maintainer

  • The issue text references internal/client/client.go. The actual path in this repo is core/client/grpc.go (there is no internal/ tree), so the modification landed there. Happy to rename / restructure if the project plans a move to internal/.
  • I kept the credentials file co-located with the existing ~/.permctl config rather than under ~/.config/permify/ to match the existing convention set by cmd/permctl/permctl.go. Easy to switch to XDG if preferred.
  • permctl whoami masks the token but prints cert paths verbatim, since cert paths are filesystem locations rather than secrets.
  • permctl logout removes credentials from disk but does not touch the ~/.permctl config file produced by permctl configure (those are separate concerns).

Test plan

  • go build ./... clean
  • go test ./... passes (1 new package: core/credentials -> ok)
  • go vet ./... no warnings
  • gofmt -l core/credentials core/cli/auth.go core/client/grpc.go empty
  • Manual end-to-end on Windows: login -> file written with masked output -> whoami shows masked token, full endpoint/cert paths -> logout -> whoami returns the expected "no credentials stored" error and exits non-zero
  • Existing permctl --help lists the three new commands alongside configure, data, permission, schema, tenant
  • (maintainer) Verify with a real Permify server that token + mTLS dialing reaches the server

Generated with Claude Code.

Introduces a new core/credentials package that persists permify connection
material (endpoint, bearer token, mTLS cert path/key) to disk as YAML at
$HOME/.permctl-credentials.yaml with 0600 permissions. Reads enforce the
permission contract on POSIX systems and refuse to load world/group-readable
files. Writes are atomic via temp file + rename so an interrupted save
never leaves a half-written file.

core/client/grpc.go's New() now loads stored credentials and uses them to
build appropriate grpc.DialOptions: mTLS transport credentials when cert
material is present, insecure transport otherwise, plus per-RPC bearer
token credentials (secure flavor over TLS, non-secure over plaintext) when
a token is stored. The endpoint argument still wins; if blank it falls back
to the stored endpoint, and an explicit error is returned when neither is
set. Backward compatibility: when no credentials file exists, behavior is
unchanged from the previous insecure-only client.

Three new Cobra commands wire the storage to the CLI:
  - permctl login   --endpoint/--token/--cert-path/--cert-key (merge-update)
  - permctl logout  (deletes profile entry; removes file when last)
  - permctl whoami  (prints stored creds with token masked)

These commands override the root PersistentPreRun so they remain usable
before `permctl configure` has ever been run.

Tests cover round-trip, multi-profile preservation, 0600 file mode,
rejection of insecure file permissions, nil-credential guard, profile
deletion (last-profile removes file), missing-file delete no-op, env
override, and default-profile fallback.

No new module dependencies were introduced; uses the already-vendored
gopkg.in/yaml.v3, spf13/cobra, and google.golang.org/grpc/credentials.

Closes Permify#2

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Endpoint, Token, Cert Path, and Cert Key Storage for CLI Tool

1 participant