mirror of
https://github.com/coder/coder.git
synced 2026-06-02 20:48:20 +00:00
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.
This commit is contained in:
+20
-4
@@ -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.
|
||||
|
||||
+202
-133
@@ -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")
|
||||
})
|
||||
}
|
||||
|
||||
+3
-3
@@ -154,9 +154,9 @@ func (r *RootCmd) login() *serpent.Command {
|
||||
cmd := &serpent.Command{
|
||||
Use: "login [<url>]",
|
||||
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()
|
||||
|
||||
+20
-9
@@ -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 {
|
||||
|
||||
@@ -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{},
|
||||
|
||||
Vendored
+4
-3
@@ -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.
|
||||
|
||||
+3
-3
@@ -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
|
||||
|
||||
Generated
+2
-1
@@ -177,8 +177,9 @@ Disable network telemetry. Network telemetry is collected when connecting to wor
|
||||
|-------------|---------------------------------|
|
||||
| Type | <code>bool</code> |
|
||||
| Environment | <code>$CODER_USE_KEYRING</code> |
|
||||
| Default | <code>true</code> |
|
||||
|
||||
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
|
||||
|
||||
|
||||
Generated
+1
-1
@@ -12,7 +12,7 @@ coder login [flags] [<url>]
|
||||
## 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
|
||||
|
||||
+4
-3
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user