From 6238a992757dc1681565681bde5ef2488565f2bc Mon Sep 17 00:00:00 2001 From: Zach <3724288+zedkipp@users.noreply.github.com> Date: Tue, 25 Nov 2025 18:13:00 -0700 Subject: [PATCH] feat(cli)!: enable keyring usage by default (#20851) Make keyring usage for session token storage on by default for supported platforms (Windows and macOS), with the ability to opt-out via --use-keyring=false. This change will be a breaking change for any users depending on the session token being stored on disk, though users can restore file usage via the flag above. This change will also require CLI users to authenticate after updating. --- cli/clitest/clitest.go | 24 +- cli/keyring_test.go | 335 ++++++++++++-------- cli/login.go | 6 +- cli/root.go | 29 +- cli/sessionstore/sessionstore.go | 16 +- cli/testdata/coder_--help.golden | 7 +- cli/testdata/coder_login_--help.golden | 6 +- docs/reference/cli/index.md | 3 +- docs/reference/cli/login.md | 2 +- enterprise/cli/testdata/coder_--help.golden | 7 +- 10 files changed, 263 insertions(+), 172 deletions(-) diff --git a/cli/clitest/clitest.go b/cli/clitest/clitest.go index 8d1f5302ce..20db312101 100644 --- a/cli/clitest/clitest.go +++ b/cli/clitest/clitest.go @@ -28,7 +28,9 @@ import ( ) // New creates a CLI instance with a configuration pointed to a -// temporary testing directory. +// temporary testing directory. The invocation is set up to use a +// global config directory for the given testing.TB, and keyring +// usage disabled. func New(t testing.TB, args ...string) (*serpent.Invocation, config.Root) { var root cli.RootCmd @@ -59,6 +61,15 @@ func NewWithCommand( t testing.TB, cmd *serpent.Command, args ...string, ) (*serpent.Invocation, config.Root) { configDir := config.Root(t.TempDir()) + // Keyring usage is disabled here because many existing tests expect the session token + // to be stored on disk and is not properly instrumented for parallel testing against + // the actual operating system keyring. + invArgs := append([]string{"--global-config", string(configDir), "--use-keyring=false"}, args...) + return setupInvocation(t, cmd, invArgs...), configDir +} + +func setupInvocation(t testing.TB, cmd *serpent.Command, args ...string, +) *serpent.Invocation { // I really would like to fail test on error logs, but realistically, turning on by default // in all our CLI tests is going to create a lot of flaky noise. logger := slogtest.Make(t, &slogtest.Options{IgnoreErrors: true}). @@ -66,16 +77,21 @@ func NewWithCommand( Named("cli") i := &serpent.Invocation{ Command: cmd, - Args: append([]string{"--global-config", string(configDir)}, args...), + Args: args, Stdin: io.LimitReader(nil, 0), Stdout: (&logWriter{prefix: "stdout", log: logger}), Stderr: (&logWriter{prefix: "stderr", log: logger}), Logger: logger, } t.Logf("invoking command: %s %s", cmd.Name(), strings.Join(i.Args, " ")) + return i +} - // These can be overridden by the test. - return i, configDir +func NewWithDefaultKeyringCommand(t testing.TB, cmd *serpent.Command, args ...string, +) (*serpent.Invocation, config.Root) { + configDir := config.Root(t.TempDir()) + invArgs := append([]string{"--global-config", string(configDir)}, args...) + return setupInvocation(t, cmd, invArgs...), configDir } // SetupConfig applies the URL and SessionToken of the client to the config. diff --git a/cli/keyring_test.go b/cli/keyring_test.go index 646f4ae19a..27b7c12d53 100644 --- a/cli/keyring_test.go +++ b/cli/keyring_test.go @@ -2,62 +2,84 @@ package cli_test import ( "bytes" + "crypto/rand" + "encoding/binary" + "fmt" "net/url" "os" "path" "runtime" "testing" + "time" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/coder/coder/v2/cli" "github.com/coder/coder/v2/cli/clitest" + "github.com/coder/coder/v2/cli/config" + "github.com/coder/coder/v2/cli/sessionstore" "github.com/coder/coder/v2/coderd/coderdtest" "github.com/coder/coder/v2/pty/ptytest" + "github.com/coder/serpent" ) -// mockKeyring is a mock sessionstore.Backend implementation. -type mockKeyring struct { - credentials map[string]string // service name -> credential -} - -const mockServiceName = "mock-service-name" - -func newMockKeyring() *mockKeyring { - return &mockKeyring{credentials: make(map[string]string)} -} - -func (m *mockKeyring) Read(_ *url.URL) (string, error) { - cred, ok := m.credentials[mockServiceName] - if !ok { - return "", os.ErrNotExist +// keyringTestServiceName generates a unique service name for keyring tests +// using the test name and a nanosecond timestamp to prevent collisions. +func keyringTestServiceName(t *testing.T) string { + t.Helper() + var n uint32 + err := binary.Read(rand.Reader, binary.BigEndian, &n) + if err != nil { + t.Fatal(err) } - return cred, nil + return fmt.Sprintf("%s_%v_%d", t.Name(), time.Now().UnixNano(), n) } -func (m *mockKeyring) Write(_ *url.URL, token string) error { - m.credentials[mockServiceName] = token - return nil +type keyringTestEnv struct { + serviceName string + keyring sessionstore.Keyring + inv *serpent.Invocation + cfg config.Root + clientURL *url.URL } -func (m *mockKeyring) Delete(_ *url.URL) error { - _, ok := m.credentials[mockServiceName] - if !ok { - return os.ErrNotExist - } - delete(m.credentials, mockServiceName) - return nil +func setupKeyringTestEnv(t *testing.T, clientURL string, args ...string) keyringTestEnv { + t.Helper() + + var root cli.RootCmd + + cmd, err := root.Command(root.AGPL()) + require.NoError(t, err) + + serviceName := keyringTestServiceName(t) + root.WithKeyringServiceName(serviceName) + + inv, cfg := clitest.NewWithDefaultKeyringCommand(t, cmd, args...) + + parsedURL, err := url.Parse(clientURL) + require.NoError(t, err) + + backend := sessionstore.NewKeyringWithService(serviceName) + t.Cleanup(func() { + _ = backend.Delete(parsedURL) + }) + + return keyringTestEnv{serviceName, backend, inv, cfg, parsedURL} } func TestUseKeyring(t *testing.T) { - // Verify that the --use-keyring flag opts into using a keyring backend for - // storing session tokens instead of plain text files. + // Verify that the --use-keyring flag default opts into using a keyring backend + // for storing session tokens instead of plain text files. t.Parallel() t.Run("Login", func(t *testing.T) { t.Parallel() + if runtime.GOOS != "windows" && runtime.GOOS != "darwin" { + t.Skip("keyring is not supported on this OS") + } + // Create a test server client := coderdtest.New(t, nil) coderdtest.CreateFirstUser(t, client) @@ -65,25 +87,16 @@ func TestUseKeyring(t *testing.T) { // Create a pty for interactive prompts pty := ptytest.New(t) - // Create CLI invocation with --use-keyring flag - inv, cfg := clitest.New(t, + // Create CLI invocation which defaults to using the keyring + env := setupKeyringTestEnv(t, client.URL.String(), "login", "--force-tty", - "--use-keyring", "--no-open", - client.URL.String(), - ) + client.URL.String()) + inv := env.inv inv.Stdin = pty.Input() inv.Stdout = pty.Output() - // Inject the mock backend before running the command - var root cli.RootCmd - cmd, err := root.Command(root.AGPL()) - require.NoError(t, err) - mockBackend := newMockKeyring() - root.WithSessionStorageBackend(mockBackend) - inv.Command = cmd - // Run login in background doneChan := make(chan struct{}) go func() { @@ -99,19 +112,23 @@ func TestUseKeyring(t *testing.T) { <-doneChan // Verify that session file was NOT created (using keyring instead) - sessionFile := path.Join(string(cfg), "session") - _, err = os.Stat(sessionFile) + sessionFile := path.Join(string(env.cfg), "session") + _, err := os.Stat(sessionFile) require.True(t, os.IsNotExist(err), "session file should not exist when using keyring") - // Verify that the credential IS stored in mock keyring - cred, err := mockBackend.Read(nil) - require.NoError(t, err, "credential should be stored in mock keyring") + // Verify that the credential IS stored in OS keyring + cred, err := env.keyring.Read(env.clientURL) + require.NoError(t, err, "credential should be stored in OS keyring") require.Equal(t, client.SessionToken(), cred, "stored token should match login token") }) t.Run("Logout", func(t *testing.T) { t.Parallel() + if runtime.GOOS != "windows" && runtime.GOOS != "darwin" { + t.Skip("keyring is not supported on this OS") + } + // Create a test server client := coderdtest.New(t, nil) coderdtest.CreateFirstUser(t, client) @@ -119,25 +136,17 @@ func TestUseKeyring(t *testing.T) { // Create a pty for interactive prompts pty := ptytest.New(t) - // First, login with --use-keyring - loginInv, cfg := clitest.New(t, + // First, login with the keyring (default) + env := setupKeyringTestEnv(t, client.URL.String(), "login", "--force-tty", - "--use-keyring", "--no-open", client.URL.String(), ) + loginInv := env.inv loginInv.Stdin = pty.Input() loginInv.Stdout = pty.Output() - // Inject the mock backend - var loginRoot cli.RootCmd - loginCmd, err := loginRoot.Command(loginRoot.AGPL()) - require.NoError(t, err) - mockBackend := newMockKeyring() - loginRoot.WithSessionStorageBackend(mockBackend) - loginInv.Command = loginCmd - doneChan := make(chan struct{}) go func() { defer close(doneChan) @@ -150,25 +159,22 @@ func TestUseKeyring(t *testing.T) { pty.ExpectMatch("Welcome to Coder") <-doneChan - // Verify credential exists in mock keyring - cred, err := mockBackend.Read(nil) + // Verify credential exists in OS keyring + cred, err := env.keyring.Read(env.clientURL) require.NoError(t, err, "read credential should succeed before logout") - require.NotEmpty(t, cred, "credential should exist after logout") + require.NotEmpty(t, cred, "credential should exist before logout") - // Now run logout with --use-keyring - logoutInv, _ := clitest.New(t, - "logout", - "--use-keyring", - "--yes", - "--global-config", string(cfg), - ) - - // Inject the same mock backend + // Now logout using the same keyring service name var logoutRoot cli.RootCmd logoutCmd, err := logoutRoot.Command(logoutRoot.AGPL()) require.NoError(t, err) - logoutRoot.WithSessionStorageBackend(mockBackend) - logoutInv.Command = logoutCmd + logoutRoot.WithKeyringServiceName(env.serviceName) + + logoutInv, _ := clitest.NewWithDefaultKeyringCommand(t, logoutCmd, + "logout", + "--yes", + "--global-config", string(env.cfg), + ) var logoutOut bytes.Buffer logoutInv.Stdout = &logoutOut @@ -176,14 +182,18 @@ func TestUseKeyring(t *testing.T) { err = logoutInv.Run() require.NoError(t, err, "logout should succeed") - // Verify the credential was deleted from mock keyring - _, err = mockBackend.Read(nil) + // Verify the credential was deleted from OS keyring + _, err = env.keyring.Read(env.clientURL) require.ErrorIs(t, err, os.ErrNotExist, "credential should be deleted from keyring after logout") }) - t.Run("OmitFlag", func(t *testing.T) { + t.Run("DefaultFileStorage", func(t *testing.T) { t.Parallel() + if runtime.GOOS != "linux" { + t.Skip("file storage is the default for Linux") + } + // Create a test server client := coderdtest.New(t, nil) coderdtest.CreateFirstUser(t, client) @@ -191,13 +201,13 @@ func TestUseKeyring(t *testing.T) { // Create a pty for interactive prompts pty := ptytest.New(t) - // --use-keyring flag omitted (should use file-based storage) - inv, cfg := clitest.New(t, + env := setupKeyringTestEnv(t, client.URL.String(), "login", "--force-tty", "--no-open", client.URL.String(), ) + inv := env.inv inv.Stdin = pty.Input() inv.Stdout = pty.Output() @@ -214,9 +224,9 @@ func TestUseKeyring(t *testing.T) { <-doneChan // Verify that session file WAS created (not using keyring) - sessionFile := path.Join(string(cfg), "session") + sessionFile := path.Join(string(env.cfg), "session") _, err := os.Stat(sessionFile) - require.NoError(t, err, "session file should exist when NOT using --use-keyring") + require.NoError(t, err, "session file should exist when NOT using --use-keyring on Linux") // Read and verify the token from file content, err := os.ReadFile(sessionFile) @@ -234,24 +244,18 @@ func TestUseKeyring(t *testing.T) { // Create a pty for interactive prompts pty := ptytest.New(t) - // Login using CODER_USE_KEYRING environment variable instead of flag - inv, cfg := clitest.New(t, + // Login using CODER_USE_KEYRING environment variable set to disable keyring usage, + // which should have the same behavior on all platforms. + env := setupKeyringTestEnv(t, client.URL.String(), "login", "--force-tty", "--no-open", client.URL.String(), ) + inv := env.inv inv.Stdin = pty.Input() inv.Stdout = pty.Output() - inv.Environ.Set("CODER_USE_KEYRING", "true") - - // Inject the mock backend - var root cli.RootCmd - cmd, err := root.Command(root.AGPL()) - require.NoError(t, err) - mockBackend := newMockKeyring() - root.WithSessionStorageBackend(mockBackend) - inv.Command = cmd + inv.Environ.Set("CODER_USE_KEYRING", "false") doneChan := make(chan struct{}) go func() { @@ -265,21 +269,64 @@ func TestUseKeyring(t *testing.T) { pty.ExpectMatch("Welcome to Coder") <-doneChan - // Verify that session file was NOT created (using keyring via env var) - sessionFile := path.Join(string(cfg), "session") - _, err = os.Stat(sessionFile) - require.True(t, os.IsNotExist(err), "session file should not exist when using keyring via env var") + // Verify that session file WAS created (not using keyring) + sessionFile := path.Join(string(env.cfg), "session") + _, err := os.Stat(sessionFile) + require.NoError(t, err, "session file should exist when CODER_USE_KEYRING set to false") - // Verify credential is in mock keyring - cred, err := mockBackend.Read(nil) - require.NoError(t, err, "credential should be stored in keyring when CODER_USE_KEYRING=true") - require.NotEmpty(t, cred) + // Read and verify the token from file + content, err := os.ReadFile(sessionFile) + require.NoError(t, err, "should be able to read session file") + require.Equal(t, client.SessionToken(), string(content), "file should contain the session token") + }) + + t.Run("DisableKeyringWithFlag", func(t *testing.T) { + t.Parallel() + + client := coderdtest.New(t, nil) + coderdtest.CreateFirstUser(t, client) + pty := ptytest.New(t) + + // Login with --use-keyring=false to explicitly disable keyring usage, which + // should have the same behavior on all platforms. + env := setupKeyringTestEnv(t, client.URL.String(), + "login", + "--use-keyring=false", + "--force-tty", + "--no-open", + client.URL.String(), + ) + inv := env.inv + inv.Stdin = pty.Input() + inv.Stdout = pty.Output() + + doneChan := make(chan struct{}) + go func() { + defer close(doneChan) + err := inv.Run() + assert.NoError(t, err) + }() + + pty.ExpectMatch("Paste your token here:") + pty.WriteLine(client.SessionToken()) + pty.ExpectMatch("Welcome to Coder") + <-doneChan + + // Verify that session file WAS created (not using keyring) + sessionFile := path.Join(string(env.cfg), "session") + _, err := os.Stat(sessionFile) + require.NoError(t, err, "session file should exist when --use-keyring=false is specified") + + // Read and verify the token from file + content, err := os.ReadFile(sessionFile) + require.NoError(t, err, "should be able to read session file") + require.Equal(t, client.SessionToken(), string(content), "file should contain the session token") }) } func TestUseKeyringUnsupportedOS(t *testing.T) { - // Verify that trying to use --use-keyring on an unsupported operating system produces - // a helpful error message. + // Verify that on unsupported operating systems, file-based storage is used + // automatically even when --use-keyring is set to true (the default). t.Parallel() // Only run this on an unsupported OS. @@ -287,43 +334,60 @@ func TestUseKeyringUnsupportedOS(t *testing.T) { t.Skipf("Skipping unsupported OS test on %s where keyring is supported", runtime.GOOS) } - const expMessage = "keyring storage is not supported on this operating system; remove the --use-keyring flag" - - t.Run("LoginWithUnsupportedKeyring", func(t *testing.T) { - t.Parallel() - - client := coderdtest.New(t, nil) - coderdtest.CreateFirstUser(t, client) - - // Try to login with --use-keyring on an unsupported OS - inv, _ := clitest.New(t, - "login", - "--use-keyring", - client.URL.String(), - ) - - // The error should occur immediately, before any prompts - loginErr := inv.Run() - - // Verify we got an error about unsupported OS - require.Error(t, loginErr) - require.Contains(t, loginErr.Error(), expMessage) - }) - - t.Run("LogoutWithUnsupportedKeyring", func(t *testing.T) { + t.Run("LoginWithDefaultKeyring", func(t *testing.T) { t.Parallel() client := coderdtest.New(t, nil) coderdtest.CreateFirstUser(t, client) pty := ptytest.New(t) - // First login without keyring to create a session - loginInv, cfg := clitest.New(t, + env := setupKeyringTestEnv(t, client.URL.String(), "login", "--force-tty", "--no-open", client.URL.String(), ) + inv := env.inv + inv.Stdin = pty.Input() + inv.Stdout = pty.Output() + + doneChan := make(chan struct{}) + go func() { + defer close(doneChan) + err := inv.Run() + assert.NoError(t, err) + }() + + pty.ExpectMatch("Paste your token here:") + pty.WriteLine(client.SessionToken()) + pty.ExpectMatch("Welcome to Coder") + <-doneChan + + // Verify that session file WAS created (automatic fallback to file storage) + sessionFile := path.Join(string(env.cfg), "session") + _, err := os.Stat(sessionFile) + require.NoError(t, err, "session file should exist due to automatic fallback to file storage") + + content, err := os.ReadFile(sessionFile) + require.NoError(t, err, "should be able to read session file") + require.Equal(t, client.SessionToken(), string(content), "file should contain the session token") + }) + + t.Run("LogoutWithDefaultKeyring", func(t *testing.T) { + t.Parallel() + + client := coderdtest.New(t, nil) + coderdtest.CreateFirstUser(t, client) + pty := ptytest.New(t) + + // First login to create a session (will use file storage due to automatic fallback) + env := setupKeyringTestEnv(t, client.URL.String(), + "login", + "--force-tty", + "--no-open", + client.URL.String(), + ) + loginInv := env.inv loginInv.Stdin = pty.Input() loginInv.Stdout = pty.Output() @@ -339,17 +403,22 @@ func TestUseKeyringUnsupportedOS(t *testing.T) { pty.ExpectMatch("Welcome to Coder") <-doneChan - // Now try to logout with --use-keyring on an unsupported OS - logoutInv, _ := clitest.New(t, + // Verify session file exists + sessionFile := path.Join(string(env.cfg), "session") + _, err := os.Stat(sessionFile) + require.NoError(t, err, "session file should exist before logout") + + // Now logout - should succeed and delete the file + logoutEnv := setupKeyringTestEnv(t, client.URL.String(), "logout", - "--use-keyring", "--yes", - "--global-config", string(cfg), + "--global-config", string(env.cfg), ) - err := logoutInv.Run() - // Verify we got an error about unsupported OS - require.Error(t, err) - require.Contains(t, err.Error(), expMessage) + err = logoutEnv.inv.Run() + require.NoError(t, err, "logout should succeed with automatic file storage fallback") + + _, err = os.Stat(sessionFile) + require.True(t, os.IsNotExist(err), "session file should be deleted after logout") }) } diff --git a/cli/login.go b/cli/login.go index 1a95d04035..d95eb7475d 100644 --- a/cli/login.go +++ b/cli/login.go @@ -154,9 +154,9 @@ func (r *RootCmd) login() *serpent.Command { cmd := &serpent.Command{ Use: "login []", Short: "Authenticate with Coder deployment", - Long: "By default, the session token is stored in a plain text file. Use the " + - "--use-keyring flag or set CODER_USE_KEYRING=true to store the token in " + - "the operating system keyring instead.", + Long: "By default, the session token is stored in the operating system keyring on " + + "macOS and Windows and a plain text file on Linux. Use the --use-keyring flag " + + "or CODER_USE_KEYRING environment variable to change the storage mechanism.", Middleware: serpent.RequireRangeArgs(0, 1), Handler: func(inv *serpent.Invocation) error { ctx := inv.Context() diff --git a/cli/root.go b/cli/root.go index b04570956c..2ec035d64a 100644 --- a/cli/root.go +++ b/cli/root.go @@ -56,7 +56,7 @@ var ( // anything. ErrSilent = xerrors.New("silent error") - errKeyringNotSupported = xerrors.New("keyring storage is not supported on this operating system; remove the --use-keyring flag to use file-based storage") + errKeyringNotSupported = xerrors.New("keyring storage is not supported on this operating system; omit --use-keyring to use file-based storage") ) const ( @@ -483,10 +483,12 @@ func (r *RootCmd) Command(subcommands []*serpent.Command) (*serpent.Command, err Flag: varUseKeyring, Env: envUseKeyring, Description: "Store and retrieve session tokens using the operating system " + - "keyring. Currently only supported on Windows. By default, tokens are " + - "stored in plain text files.", - Value: serpent.BoolOf(&r.useKeyring), - Group: globalGroup, + "keyring. Enabled by default. If the keyring is not supported on the " + + "current platform, file-based storage is used automatically. Set to " + + "false to force file-based storage.", + Default: "true", + Value: serpent.BoolOf(&r.useKeyring), + Group: globalGroup, }, { Flag: "debug-http", @@ -538,6 +540,7 @@ type RootCmd struct { noVersionCheck bool noFeatureWarning bool useKeyring bool + keyringServiceName string } // InitClient creates and configures a new client with authentication, telemetry, @@ -718,8 +721,13 @@ func (r *RootCmd) createUnauthenticatedClient(ctx context.Context, serverURL *ur // flag. func (r *RootCmd) ensureTokenBackend() sessionstore.Backend { if r.tokenBackend == nil { - if r.useKeyring { - r.tokenBackend = sessionstore.NewKeyring() + keyringSupported := runtime.GOOS == "windows" || runtime.GOOS == "darwin" + if r.useKeyring && keyringSupported { + serviceName := sessionstore.DefaultServiceName + if r.keyringServiceName != "" { + serviceName = r.keyringServiceName + } + r.tokenBackend = sessionstore.NewKeyringWithService(serviceName) } else { r.tokenBackend = sessionstore.NewFile(r.createConfig) } @@ -727,8 +735,11 @@ func (r *RootCmd) ensureTokenBackend() sessionstore.Backend { return r.tokenBackend } -func (r *RootCmd) WithSessionStorageBackend(backend sessionstore.Backend) { - r.tokenBackend = backend +// WithKeyringServiceName sets a custom keyring service name for testing purposes. +// This allows tests to use isolated keyring storage while still exercising the +// genuine storage backend selection logic in ensureTokenBackend(). +func (r *RootCmd) WithKeyringServiceName(serviceName string) { + r.keyringServiceName = serviceName } type AgentAuth struct { diff --git a/cli/sessionstore/sessionstore.go b/cli/sessionstore/sessionstore.go index 029e86ad7e..57f1c269bf 100644 --- a/cli/sessionstore/sessionstore.go +++ b/cli/sessionstore/sessionstore.go @@ -47,9 +47,9 @@ var ( ) const ( - // defaultServiceName is the service name used in keyrings for storing Coder CLI session + // DefaultServiceName is the service name used in keyrings for storing Coder CLI session // tokens. - defaultServiceName = "coder-v2-credentials" + DefaultServiceName = "coder-v2-credentials" ) // keyringProvider represents an operating system keyring. The expectation @@ -108,17 +108,9 @@ type Keyring struct { serviceName string } -// NewKeyring creates a Keyring with the default service name for production use. -func NewKeyring() Keyring { - return Keyring{ - provider: operatingSystemKeyring{}, - serviceName: defaultServiceName, - } -} - // NewKeyringWithService creates a Keyring Backend that stores credentials under the -// specified service name. This is primarily intended for testing to avoid conflicts -// with production credentials and collisions between tests. +// specified service name. Generally, DefaultServiceName should be provided as the service +// name except in tests which may need parameterization to avoid conflicting keyring use. func NewKeyringWithService(serviceName string) Keyring { return Keyring{ provider: operatingSystemKeyring{}, diff --git a/cli/testdata/coder_--help.golden b/cli/testdata/coder_--help.golden index 1d6aa621fe..0057fdc15d 100644 --- a/cli/testdata/coder_--help.golden +++ b/cli/testdata/coder_--help.golden @@ -109,10 +109,11 @@ variables or flags. --url url, $CODER_URL URL to a deployment. - --use-keyring bool, $CODER_USE_KEYRING + --use-keyring bool, $CODER_USE_KEYRING (default: true) Store and retrieve session tokens using the operating system keyring. - Currently only supported on Windows. By default, tokens are stored in - plain text files. + Enabled by default. If the keyring is not supported on the current + platform, file-based storage is used automatically. Set to false to + force file-based storage. -v, --verbose bool, $CODER_VERBOSE Enable verbose output. diff --git a/cli/testdata/coder_login_--help.golden b/cli/testdata/coder_login_--help.golden index 8709f60987..96129d8a55 100644 --- a/cli/testdata/coder_login_--help.golden +++ b/cli/testdata/coder_login_--help.golden @@ -5,9 +5,9 @@ USAGE: Authenticate with Coder deployment - By default, the session token is stored in a plain text file. Use the - --use-keyring flag or set CODER_USE_KEYRING=true to store the token in the - operating system keyring instead. + By default, the session token is stored in the operating system keyring on + macOS and Windows and a plain text file on Linux. Use the --use-keyring flag + or CODER_USE_KEYRING environment variable to change the storage mechanism. OPTIONS: --first-user-email string, $CODER_FIRST_USER_EMAIL diff --git a/docs/reference/cli/index.md b/docs/reference/cli/index.md index 6a3ec16c9c..e22e2deeec 100644 --- a/docs/reference/cli/index.md +++ b/docs/reference/cli/index.md @@ -177,8 +177,9 @@ Disable network telemetry. Network telemetry is collected when connecting to wor |-------------|---------------------------------| | Type | bool | | Environment | $CODER_USE_KEYRING | +| Default | true | -Store and retrieve session tokens using the operating system keyring. Currently only supported on Windows. By default, tokens are stored in plain text files. +Store and retrieve session tokens using the operating system keyring. Enabled by default. If the keyring is not supported on the current platform, file-based storage is used automatically. Set to false to force file-based storage. ### --global-config diff --git a/docs/reference/cli/login.md b/docs/reference/cli/login.md index 459a332e06..1371ebae1b 100644 --- a/docs/reference/cli/login.md +++ b/docs/reference/cli/login.md @@ -12,7 +12,7 @@ coder login [flags] [] ## Description ```console -By default, the session token is stored in a plain text file. Use the --use-keyring flag or set CODER_USE_KEYRING=true to store the token in the operating system keyring instead. +By default, the session token is stored in the operating system keyring on macOS and Windows and a plain text file on Linux. Use the --use-keyring flag or CODER_USE_KEYRING environment variable to change the storage mechanism. ``` ## Options diff --git a/enterprise/cli/testdata/coder_--help.golden b/enterprise/cli/testdata/coder_--help.golden index 78d27661f9..41ed320968 100644 --- a/enterprise/cli/testdata/coder_--help.golden +++ b/enterprise/cli/testdata/coder_--help.golden @@ -68,10 +68,11 @@ variables or flags. --url url, $CODER_URL URL to a deployment. - --use-keyring bool, $CODER_USE_KEYRING + --use-keyring bool, $CODER_USE_KEYRING (default: true) Store and retrieve session tokens using the operating system keyring. - Currently only supported on Windows. By default, tokens are stored in - plain text files. + Enabled by default. If the keyring is not supported on the current + platform, file-based storage is used automatically. Set to false to + force file-based storage. -v, --verbose bool, $CODER_VERBOSE Enable verbose output.