diff --git a/cli/clitest/clitest.go b/cli/clitest/clitest.go index 8d1f5302ce7..20db3121018 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 646f4ae19a0..27b7c12d53c 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 +// 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 fmt.Sprintf("%s_%v_%d", t.Name(), time.Now().UnixNano(), n) } -const mockServiceName = "mock-service-name" - -func newMockKeyring() *mockKeyring { - return &mockKeyring{credentials: make(map[string]string)} +type keyringTestEnv struct { + serviceName string + keyring sessionstore.Keyring + inv *serpent.Invocation + cfg config.Root + clientURL *url.URL } -func (m *mockKeyring) Read(_ *url.URL) (string, error) { - cred, ok := m.credentials[mockServiceName] - if !ok { - return "", os.ErrNotExist - } - return cred, nil -} +func setupKeyringTestEnv(t *testing.T, clientURL string, args ...string) keyringTestEnv { + t.Helper() -func (m *mockKeyring) Write(_ *url.URL, token string) error { - m.credentials[mockServiceName] = token - return nil -} + var root cli.RootCmd -func (m *mockKeyring) Delete(_ *url.URL) error { - _, ok := m.credentials[mockServiceName] - if !ok { - return os.ErrNotExist - } - delete(m.credentials, mockServiceName) - return nil + 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") - - // Now run logout with --use-keyring - logoutInv, _ := clitest.New(t, - "logout", - "--use-keyring", - "--yes", - "--global-config", string(cfg), - ) + require.NotEmpty(t, cred, "credential should exist before logout") - // 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,61 @@ 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") + inv.Environ.Set("CODER_USE_KEYRING", "false") - // 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 + 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 CODER_USE_KEYRING set to false") + + // 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() { @@ -265,21 +312,21 @@ 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 --use-keyring=false is specified") - // 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") }) } 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.Run("LoginWithDefaultKeyring", func(t *testing.T) { t.Parallel() client := coderdtest.New(t, nil) coderdtest.CreateFirstUser(t, client) + pty := ptytest.New(t) - // Try to login with --use-keyring on an unsupported OS - inv, _ := clitest.New(t, + env := setupKeyringTestEnv(t, client.URL.String(), "login", - "--use-keyring", + "--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 - // The error should occur immediately, before any prompts - loginErr := inv.Run() + // 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") - // Verify we got an error about unsupported OS - require.Error(t, loginErr) - require.Contains(t, loginErr.Error(), expMessage) + 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("LogoutWithUnsupportedKeyring", func(t *testing.T) { + t.Run("LogoutWithDefaultKeyring", 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, + // 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 1a95d040358..d95eb7475de 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 fe6d5c4ccd8..ea1401c3c25 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 029e86ad7e9..57f1c269bf8 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 e457aca8570..e3cfec70c0f 100644 --- a/cli/testdata/coder_--help.golden +++ b/cli/testdata/coder_--help.golden @@ -108,10 +108,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 8709f60987c..96129d8a55c 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 1005da991dc..72fbd608225 100644 --- a/docs/reference/cli/index.md +++ b/docs/reference/cli/index.md @@ -176,8 +176,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 459a332e062..1371ebae1bf 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 51ee58258f8..9c24639db1f 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.