Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions internal/cli/logout.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"github.com/spf13/cobra"

"github.com/auth0/auth0-cli/internal/keyring"
"github.com/auth0/auth0-cli/internal/plugins/approvals"
)

func logoutCmd(cli *cli) *cobra.Command {
Expand All @@ -23,6 +24,14 @@ func logoutCmd(cli *cli) *cobra.Command {
return err
}

// The approvals go first, so that a failure leaves the tenant logged in and
// logging out can be tried again. Consent never outlives the session it
// was given in.
pluginApprovals := &approvals.Approvals{Dir: cli.Config.PluginsDir()}
if err := pluginApprovals.RemoveTenant(selectedTenant); err != nil {
return fmt.Errorf("failed to remove the plugin approvals of the tenant %q: %w", selectedTenant, err)
}

if err := cli.Config.RemoveTenant(selectedTenant); err != nil {
return fmt.Errorf("failed to log out from the tenant %q: %w", selectedTenant, err)
}
Expand Down
104 changes: 104 additions & 0 deletions internal/cli/logout_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
package cli

import (
"io"
"os"
"path/filepath"
"runtime"
"testing"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
gokeyring "github.com/zalando/go-keyring"

"github.com/auth0/auth0-cli/internal/config"
"github.com/auth0/auth0-cli/internal/display"
"github.com/auth0/auth0-cli/internal/plugins/approvals"
)

func TestLogoutRemovesPluginApprovals(t *testing.T) {
const (
tenant = "acme.us.auth0.com"
other = "other.us.auth0.com"
id = "auth0/checkmate"
hash = "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
)

// Setup logs in to two tenants in a new home directory, and returns the CLI
// and the approvals in the plugins directory next to its config file. The path
// is written out here so that the test notices if logout looks somewhere else.
setup := func(t *testing.T) (*cli, *approvals.Approvals) {
t.Helper()

gokeyring.MockInit()

home := t.TempDir()
t.Setenv("HOME", home)

// The config remembers that its file was missing, so it is saved through
// one config and read by the CLI through another.
var saved config.Config
for _, domain := range []string{tenant, other} {
require.NoError(t, saved.AddTenant(config.Tenant{Domain: domain}))
}

return &cli{renderer: display.NewRenderer()}, &approvals.Approvals{Dir: filepath.Join(home, ".config", "auth0", "plugins")}
}

logout := func(c *cli) error {
cmd := logoutCmd(c)
cmd.SetArgs([]string{tenant})
cmd.SetOut(io.Discard)
cmd.SetErr(io.Discard)

return cmd.Execute()
}

t.Run("approvals of the tenant go and the others stay", func(t *testing.T) {
c, plugins := setup(t)
require.NoError(t, plugins.Approve(t.Context(), tenant, id, hash))
require.NoError(t, plugins.Approve(t.Context(), other, id, hash))

require.NoError(t, logout(c))

assert.Error(t, plugins.Check(tenant, id, hash))
assert.NoError(t, plugins.Check(other, id, hash))

_, err := c.Config.GetTenant(tenant)
assert.Error(t, err, "the tenant is logged out")
})

t.Run("a failing removal stops the logout and the tenant stays configured", func(t *testing.T) {
c, plugins := setup(t)

if runtime.GOOS == "windows" || os.Geteuid() == 0 {
t.Skip("a directory that cannot be written needs a user who is not root, on a system with permission bits")
}

// A damaged file is deleted, which fails when its directory cannot be
// written. The lock file exists already, so that taking the lock still works.
require.NoError(t, os.MkdirAll(plugins.Dir, 0o700))
require.NoError(t, os.WriteFile(filepath.Join(plugins.Dir, "approvals.json"), []byte("not json"), 0o600))
require.NoError(t, os.WriteFile(filepath.Join(plugins.Dir, ".approvals.lock"), nil, 0o600))
require.NoError(t, os.Chmod(plugins.Dir, 0o500))

t.Cleanup(func() { _ = os.Chmod(plugins.Dir, 0o700) })

err := logout(c)

require.ErrorContains(t, err, "failed to remove the plugin approvals")
assert.ErrorContains(t, err, "failed to remove the damaged plugin approvals file")

_, err = c.Config.GetTenant(tenant)
assert.NoError(t, err, "the tenant is still logged in, so logout can be tried again")
})

t.Run("a user who never used plugins sees no change", func(t *testing.T) {
c, plugins := setup(t)

require.NoError(t, logout(c))

_, err := os.Stat(plugins.Dir)
assert.ErrorIs(t, err, os.ErrNotExist)
})
}
11 changes: 11 additions & 0 deletions internal/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -225,6 +225,17 @@ func (c *Config) ensureInstallIDAssigned() {
c.InstallID = uuid.NewString()
}

// PluginsDir returns the directory of the plugin store, which sits next to the
// config file. It does not create the directory or change the config.
func (c *Config) PluginsDir() string {
configPath := c.path
if configPath == "" {
configPath = defaultPath()
}

return filepath.Join(filepath.Dir(configPath), "plugins")
}

func (c *Config) loadFromDisk() error {
if c.path == "" {
c.path = defaultPath()
Expand Down
18 changes: 18 additions & 0 deletions internal/config/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"fmt"
"os"
"path"
"path/filepath"
"testing"
"time"

Expand All @@ -23,6 +24,23 @@ func TestDefaultPath(t *testing.T) {
assert.Equal(t, expectedPath, actualPath)
}

func TestConfig_PluginsDir(t *testing.T) {
t.Run("it sits next to the config file", func(t *testing.T) {
dir := t.TempDir()
config := &Config{path: path.Join(dir, "auth0", "config.json")}

assert.Equal(t, filepath.Join(dir, "auth0", "plugins"), config.PluginsDir())
assert.Equal(t, path.Join(dir, "auth0", "config.json"), config.path, "the path is left alone")
})

t.Run("it sits next to the default config file", func(t *testing.T) {
home := t.TempDir()
t.Setenv("HOME", home)

assert.Equal(t, filepath.Join(home, ".config", "auth0", "plugins"), (&Config{}).PluginsDir())
})
}

func TestConfig_LoadFromDisk(t *testing.T) {
t.Run("it fails to load a non existent config file", func(t *testing.T) {
config := &Config{path: "i-am-a-non-existent-config.json"}
Expand Down
Loading
Loading