diff --git a/internal/auth/auth.go b/internal/auth/auth.go index 4877c550a..878c6f57b 100644 --- a/internal/auth/auth.go +++ b/internal/auth/auth.go @@ -36,6 +36,8 @@ type SecretStore interface { Set(namespace, key, value string) error // Get gets the secret Get(namespace, key string) (string, error) + // Delete removes the secret + Delete(namespace, key string) error } type Authenticator struct { diff --git a/internal/auth/secrets.go b/internal/auth/secrets.go index 06f76820e..d3967107b 100644 --- a/internal/auth/secrets.go +++ b/internal/auth/secrets.go @@ -13,3 +13,8 @@ func (k *Keyring) Set(namespace, key, value string) error { func (k *Keyring) Get(namespace, key string) (string, error) { return keyring.Get(namespace, key) } + +// Delete deletes a value for the given namespace and key. +func (k *Keyring) Delete(namespace, key string) error { + return keyring.Delete(namespace, key) +} diff --git a/internal/auth/token.go b/internal/auth/token.go index 7fee8d290..3395810d9 100644 --- a/internal/auth/token.go +++ b/internal/auth/token.go @@ -22,6 +22,11 @@ type TokenRetriever struct { Client *http.Client } +// Delete deletes the given tenant from the secrets storage. +func (t *TokenRetriever) Delete(tenant string) error { + return t.Secrets.Delete(secretsNamespace, tenant) +} + // Refresh gets a new access token from the provided refresh token, // The request is used the default client_id and endpoint for device authentication. func (t *TokenRetriever) Refresh(ctx context.Context, tenant string) (TokenResponse, error) { diff --git a/internal/cli/cli.go b/internal/cli/cli.go index 5705fcefb..8ef322dc7 100644 --- a/internal/cli/cli.go +++ b/internal/cli/cli.go @@ -229,6 +229,47 @@ func (c *cli) addTenant(ten tenant) error { return nil } +func (c *cli) removeTenant(ten string) error { + // init will fail here with a `no tenant found` error if we're logging + // in for the first time and that's expected. + _ = c.init() + + // If we're dealing with an empty file, we'll need to initialize this + // map. + if c.config.Tenants == nil { + c.config.Tenants = map[string]tenant{} + } + + delete(c.config.Tenants, ten) + + // If the default tenant is being removed, we'll pick the first tenant + // that's not the one being removed, and make that the new default. + if c.config.DefaultTenant == ten { + if len(c.config.Tenants) == 0 { + c.config.DefaultTenant = "" + } else { + Loop: + for t := range c.config.Tenants { + if t != ten { + c.config.DefaultTenant = t + break Loop + } + } + } + } + + if err := c.persistConfig(); err != nil { + return fmt.Errorf("Unexpected error persisting config: %w", err) + } + + tr := &auth.TokenRetriever{Secrets: &auth.Keyring{}} + if err := tr.Delete(ten); err != nil { + return fmt.Errorf("Unexpected error clearing tenant information: %w", err) + } + + return nil +} + func (c *cli) persistConfig() error { dir := filepath.Dir(c.path) if _, err := os.Stat(dir); os.IsNotExist(err) { diff --git a/internal/cli/logout.go b/internal/cli/logout.go new file mode 100644 index 000000000..4f7d7c1fb --- /dev/null +++ b/internal/cli/logout.go @@ -0,0 +1,53 @@ +package cli + +import ( + "fmt" + + "github.com/auth0/auth0-cli/internal/prompt" + "github.com/spf13/cobra" +) + +func logoutCmd(cli *cli) *cobra.Command { + cmd := &cobra.Command{ + Use: "logout", + Short: "Logout of a tenant's session", + Long: `auth0 logout `, + RunE: func(cmd *cobra.Command, args []string) error { + // NOTE(cyx): This was mostly copy/pasted from tenants + // use command. Consider refactoring. + var selectedTenant string + if len(args) == 0 { + tens, err := cli.listTenants() + if err != nil { + return fmt.Errorf("Unable to load tenants due to an unexpected error: %w", err) + } + + tenNames := make([]string, len(tens)) + for i, t := range tens { + tenNames[i] = t.Name + } + + input := prompt.SelectInput("tenant", "Tenant:", "Tenant to activate", tenNames, true) + if err := prompt.AskOne(input, &selectedTenant); err != nil { + return fmt.Errorf("An unexpected error occurred: %w", err) + } + } else { + requestedTenant := args[0] + t, ok := cli.config.Tenants[requestedTenant] + if !ok { + return fmt.Errorf("Unable to find tenant %s; run `auth0 tenants use` to see your configured tenants or run `auth0 login` to configure a new tenant", requestedTenant) + } + selectedTenant = t.Name + } + + if err := cli.removeTenant(selectedTenant); err != nil { + return fmt.Errorf("Unexpected error logging out tenant: %s: %v", selectedTenant, err) + } + + cli.renderer.Infof("Successfully logged out tenant: %s", selectedTenant) + return nil + }, + } + + return cmd +} diff --git a/internal/cli/root.go b/internal/cli/root.go index e18bceb75..9c3292e17 100644 --- a/internal/cli/root.go +++ b/internal/cli/root.go @@ -34,6 +34,12 @@ func Execute() { return nil } + // If the user is trying to logout, session information + // isn't important as well. + if cmd.Use == "logout" && cmd.Parent().Use == "auth0" { + return nil + } + // Selecting tenants shouldn't really trigger a login. if cmd.Use == "use" && cmd.Parent().Use == "tenants" { return nil @@ -72,6 +78,7 @@ func Execute() { rootCmd.AddCommand(testCmd(cli)) rootCmd.AddCommand(logsCmd(cli)) rootCmd.AddCommand(actionsCmd(cli)) + rootCmd.AddCommand(logoutCmd(cli)) // keep completion at the bottom: rootCmd.AddCommand(completionCmd(cli))