mirror of
https://github.com/coder/coder.git
synced 2026-06-02 20:48:20 +00:00
7ecfd1aa07
This change ensures keyring tests that utilize the real OS keyring use credentials that are isolated by process ID so that parallel test processes do not access the same credentials. https://github.com/coder/internal/issues/1192
401 lines
11 KiB
Go
401 lines
11 KiB
Go
package sessionstore_test
|
|
|
|
import (
|
|
"encoding/json"
|
|
"errors"
|
|
"net/url"
|
|
"os"
|
|
"path"
|
|
"runtime"
|
|
"testing"
|
|
|
|
"github.com/stretchr/testify/require"
|
|
|
|
"github.com/coder/coder/v2/cli/config"
|
|
"github.com/coder/coder/v2/cli/sessionstore"
|
|
"github.com/coder/coder/v2/cli/sessionstore/testhelpers"
|
|
)
|
|
|
|
type storedCredentials map[string]struct {
|
|
CoderURL string `json:"coder_url"`
|
|
APIToken string `json:"api_token"`
|
|
}
|
|
|
|
func TestKeyring(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
if runtime.GOOS != "windows" && runtime.GOOS != "darwin" {
|
|
t.Skip("linux is not supported yet")
|
|
}
|
|
|
|
// This test exercises use of the operating system keyring. As a result,
|
|
// the operating system keyring is expected to be available.
|
|
|
|
const (
|
|
testURL = "http://127.0.0.1:1337"
|
|
testURL2 = "http://127.0.0.1:1338"
|
|
)
|
|
|
|
t.Run("ReadNonExistent", func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
backend := sessionstore.NewKeyringWithService(testhelpers.KeyringServiceName(t))
|
|
srvURL, err := url.Parse(testURL)
|
|
require.NoError(t, err)
|
|
t.Cleanup(func() { _ = backend.Delete(srvURL) })
|
|
|
|
_, err = backend.Read(srvURL)
|
|
require.Error(t, err)
|
|
require.True(t, os.IsNotExist(err), "expected os.ErrNotExist when reading non-existent token")
|
|
})
|
|
|
|
t.Run("DeleteNonExistent", func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
backend := sessionstore.NewKeyringWithService(testhelpers.KeyringServiceName(t))
|
|
srvURL, err := url.Parse(testURL)
|
|
require.NoError(t, err)
|
|
t.Cleanup(func() { _ = backend.Delete(srvURL) })
|
|
|
|
err = backend.Delete(srvURL)
|
|
require.Error(t, err)
|
|
require.True(t, errors.Is(err, os.ErrNotExist), "expected os.ErrNotExist when deleting non-existent token")
|
|
})
|
|
|
|
t.Run("WriteAndRead", func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
backend := sessionstore.NewKeyringWithService(testhelpers.KeyringServiceName(t))
|
|
srvURL, err := url.Parse(testURL)
|
|
require.NoError(t, err)
|
|
t.Cleanup(func() { _ = backend.Delete(srvURL) })
|
|
|
|
dir := t.TempDir()
|
|
expSessionFile := path.Join(dir, "session")
|
|
|
|
const inputToken = "test-keyring-token-12345"
|
|
err = backend.Write(srvURL, inputToken)
|
|
require.NoError(t, err)
|
|
|
|
// Verify no session file was created (keyring stores in OS keyring, not file)
|
|
_, err = os.Stat(expSessionFile)
|
|
require.True(t, errors.Is(err, os.ErrNotExist), "expected session token file to not exist when using keyring")
|
|
|
|
token, err := backend.Read(srvURL)
|
|
require.NoError(t, err)
|
|
require.Equal(t, inputToken, token)
|
|
|
|
// Clean up
|
|
err = backend.Delete(srvURL)
|
|
require.NoError(t, err)
|
|
})
|
|
|
|
t.Run("WriteAndDelete", func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
backend := sessionstore.NewKeyringWithService(testhelpers.KeyringServiceName(t))
|
|
srvURL, err := url.Parse(testURL)
|
|
require.NoError(t, err)
|
|
t.Cleanup(func() { _ = backend.Delete(srvURL) })
|
|
|
|
const inputToken = "test-keyring-token-67890"
|
|
err = backend.Write(srvURL, inputToken)
|
|
require.NoError(t, err)
|
|
|
|
token, err := backend.Read(srvURL)
|
|
require.NoError(t, err)
|
|
require.Equal(t, inputToken, token)
|
|
|
|
err = backend.Delete(srvURL)
|
|
require.NoError(t, err)
|
|
|
|
_, err = backend.Read(srvURL)
|
|
require.Error(t, err)
|
|
require.True(t, os.IsNotExist(err), "expected os.ErrNotExist after deleting token")
|
|
})
|
|
|
|
t.Run("OverwriteToken", func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
backend := sessionstore.NewKeyringWithService(testhelpers.KeyringServiceName(t))
|
|
srvURL, err := url.Parse(testURL)
|
|
require.NoError(t, err)
|
|
t.Cleanup(func() { _ = backend.Delete(srvURL) })
|
|
|
|
// Write first token
|
|
const firstToken = "first-keyring-token"
|
|
err = backend.Write(srvURL, firstToken)
|
|
require.NoError(t, err)
|
|
|
|
token, err := backend.Read(srvURL)
|
|
require.NoError(t, err)
|
|
require.Equal(t, firstToken, token)
|
|
|
|
// Overwrite with second token
|
|
const secondToken = "second-keyring-token"
|
|
err = backend.Write(srvURL, secondToken)
|
|
require.NoError(t, err)
|
|
|
|
token, err = backend.Read(srvURL)
|
|
require.NoError(t, err)
|
|
require.Equal(t, secondToken, token)
|
|
|
|
// Clean up
|
|
err = backend.Delete(srvURL)
|
|
require.NoError(t, err)
|
|
})
|
|
|
|
t.Run("MultipleServers", func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
backend := sessionstore.NewKeyringWithService(testhelpers.KeyringServiceName(t))
|
|
srvURL, err := url.Parse(testURL)
|
|
require.NoError(t, err)
|
|
srvURL2, err := url.Parse(testURL2)
|
|
require.NoError(t, err)
|
|
|
|
t.Cleanup(func() {
|
|
_ = backend.Delete(srvURL)
|
|
_ = backend.Delete(srvURL2)
|
|
})
|
|
|
|
// Write token for server 1
|
|
const token1 = "token-for-server-1"
|
|
err = backend.Write(srvURL, token1)
|
|
require.NoError(t, err)
|
|
|
|
// Write token for server 2 (should NOT overwrite server 1)
|
|
const token2 = "token-for-server-2"
|
|
err = backend.Write(srvURL2, token2)
|
|
require.NoError(t, err)
|
|
|
|
// Read server 1's credential
|
|
token, err := backend.Read(srvURL)
|
|
require.NoError(t, err)
|
|
require.Equal(t, token1, token)
|
|
|
|
// Read server 2's credential
|
|
token, err = backend.Read(srvURL2)
|
|
require.NoError(t, err)
|
|
require.Equal(t, token2, token)
|
|
|
|
// Delete server 1's credential
|
|
err = backend.Delete(srvURL)
|
|
require.NoError(t, err)
|
|
|
|
// Verify server 1's credential is gone
|
|
_, err = backend.Read(srvURL)
|
|
require.Error(t, err)
|
|
require.True(t, os.IsNotExist(err))
|
|
|
|
// Verify server 2's credential still exists
|
|
token, err = backend.Read(srvURL2)
|
|
require.NoError(t, err)
|
|
require.Equal(t, token2, token)
|
|
|
|
// Clean up remaining credentials
|
|
err = backend.Delete(srvURL2)
|
|
require.NoError(t, err)
|
|
})
|
|
|
|
t.Run("StorageFormat", func(t *testing.T) {
|
|
t.Parallel()
|
|
// The storage format must remain consistent to ensure we don't break
|
|
// compatibility with other Coder related applications that may read
|
|
// or decode the same credential.
|
|
|
|
const testURL1 = "http://127.0.0.1:1337"
|
|
srv1URL, err := url.Parse(testURL1)
|
|
require.NoError(t, err)
|
|
|
|
const testURL2 = "http://127.0.0.1:1338"
|
|
srv2URL, err := url.Parse(testURL2)
|
|
require.NoError(t, err)
|
|
|
|
serviceName := testhelpers.KeyringServiceName(t)
|
|
backend := sessionstore.NewKeyringWithService(serviceName)
|
|
t.Cleanup(func() {
|
|
_ = backend.Delete(srv1URL)
|
|
_ = backend.Delete(srv2URL)
|
|
})
|
|
|
|
// Write token for server 1
|
|
const token1 = "token-server-1"
|
|
err = backend.Write(srv1URL, token1)
|
|
require.NoError(t, err)
|
|
|
|
// Write token for server 2 (should NOT overwrite server 1's token)
|
|
const token2 = "token-server-2"
|
|
err = backend.Write(srv2URL, token2)
|
|
require.NoError(t, err)
|
|
|
|
// Verify both credentials are stored in the raw format and can
|
|
// be extracted through the Backend API.
|
|
rawCredential := readRawKeychainCredential(t, serviceName)
|
|
|
|
storedCreds := make(storedCredentials)
|
|
err = json.Unmarshal(rawCredential, &storedCreds)
|
|
require.NoError(t, err, "unmarshalling stored credentials")
|
|
|
|
// Both credentials should exist
|
|
require.Len(t, storedCreds, 2)
|
|
require.Equal(t, token1, storedCreds[srv1URL.Host].APIToken)
|
|
require.Equal(t, token2, storedCreds[srv2URL.Host].APIToken)
|
|
|
|
// Read individual credentials
|
|
token, err := backend.Read(srv1URL)
|
|
require.NoError(t, err)
|
|
require.Equal(t, token1, token)
|
|
|
|
token, err = backend.Read(srv2URL)
|
|
require.NoError(t, err)
|
|
require.Equal(t, token2, token)
|
|
|
|
// Cleanup
|
|
err = backend.Delete(srv1URL)
|
|
require.NoError(t, err)
|
|
err = backend.Delete(srv2URL)
|
|
require.NoError(t, err)
|
|
})
|
|
}
|
|
|
|
func TestFile(t *testing.T) {
|
|
const (
|
|
testURL = "http://127.0.0.1:1337"
|
|
testURL2 = "http://127.0.0.1:1338"
|
|
)
|
|
|
|
t.Parallel()
|
|
|
|
t.Run("ReadNonExistent", func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
dir := t.TempDir()
|
|
backend := sessionstore.NewFile(func() config.Root { return config.Root(dir) })
|
|
srvURL, err := url.Parse(testURL)
|
|
require.NoError(t, err)
|
|
|
|
_, err = backend.Read(srvURL)
|
|
require.Error(t, err)
|
|
require.True(t, os.IsNotExist(err))
|
|
})
|
|
|
|
t.Run("WriteAndRead", func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
dir := t.TempDir()
|
|
backend := sessionstore.NewFile(func() config.Root { return config.Root(dir) })
|
|
srvURL, err := url.Parse(testURL)
|
|
require.NoError(t, err)
|
|
|
|
// Write a token
|
|
const inputToken = "test-token-12345"
|
|
err = backend.Write(srvURL, inputToken)
|
|
require.NoError(t, err)
|
|
|
|
// Verify the session file was created
|
|
sessionFile := config.Root(dir).Session()
|
|
require.True(t, sessionFile.Exists())
|
|
|
|
// Read the token back
|
|
token, err := backend.Read(srvURL)
|
|
require.NoError(t, err)
|
|
require.Equal(t, inputToken, token)
|
|
})
|
|
|
|
t.Run("WriteAndDelete", func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
dir := t.TempDir()
|
|
backend := sessionstore.NewFile(func() config.Root { return config.Root(dir) })
|
|
srvURL, err := url.Parse(testURL)
|
|
require.NoError(t, err)
|
|
|
|
// Write a token
|
|
const inputToken = "test-token-67890"
|
|
err = backend.Write(srvURL, inputToken)
|
|
require.NoError(t, err)
|
|
|
|
// Verify the token was written
|
|
token, err := backend.Read(srvURL)
|
|
require.NoError(t, err)
|
|
require.Equal(t, inputToken, token)
|
|
|
|
// Delete the token
|
|
err = backend.Delete(srvURL)
|
|
require.NoError(t, err)
|
|
|
|
// Verify the token is gone
|
|
_, err = backend.Read(srvURL)
|
|
require.Error(t, err)
|
|
require.True(t, os.IsNotExist(err))
|
|
})
|
|
|
|
t.Run("DeleteNonExistent", func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
dir := t.TempDir()
|
|
backend := sessionstore.NewFile(func() config.Root { return config.Root(dir) })
|
|
srvURL, err := url.Parse(testURL)
|
|
require.NoError(t, err)
|
|
|
|
// Attempt to delete a non-existent token
|
|
err = backend.Delete(srvURL)
|
|
require.Error(t, err)
|
|
require.True(t, os.IsNotExist(err))
|
|
})
|
|
|
|
t.Run("OverwriteToken", func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
dir := t.TempDir()
|
|
backend := sessionstore.NewFile(func() config.Root { return config.Root(dir) })
|
|
srvURL, err := url.Parse(testURL)
|
|
require.NoError(t, err)
|
|
|
|
// Write first token
|
|
const firstToken = "first-token"
|
|
err = backend.Write(srvURL, firstToken)
|
|
require.NoError(t, err)
|
|
|
|
token, err := backend.Read(srvURL)
|
|
require.NoError(t, err)
|
|
require.Equal(t, firstToken, token)
|
|
|
|
// Overwrite with second token
|
|
const secondToken = "second-token"
|
|
err = backend.Write(srvURL, secondToken)
|
|
require.NoError(t, err)
|
|
|
|
token, err = backend.Read(srvURL)
|
|
require.NoError(t, err)
|
|
require.Equal(t, secondToken, token)
|
|
})
|
|
|
|
t.Run("WriteIgnoresURL", func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
dir := t.TempDir()
|
|
backend := sessionstore.NewFile(func() config.Root { return config.Root(dir) })
|
|
srvURL, err := url.Parse(testURL)
|
|
require.NoError(t, err)
|
|
srvURL2, err := url.Parse(testURL2)
|
|
require.NoError(t, err)
|
|
|
|
//nolint:gosec // Write with first URL test token
|
|
const firstToken = "token-for-url1"
|
|
err = backend.Write(srvURL, firstToken)
|
|
require.NoError(t, err)
|
|
|
|
//nolint:gosec // Write with second URL - should overwrite
|
|
const secondToken = "token-for-url2"
|
|
err = backend.Write(srvURL2, secondToken)
|
|
require.NoError(t, err)
|
|
|
|
// Should have the second token (File backend doesn't differentiate by URL)
|
|
token, err := backend.Read(srvURL)
|
|
require.NoError(t, err)
|
|
require.Equal(t, secondToken, token)
|
|
})
|
|
}
|