// Copyright (c) The OpenTofu Authors // SPDX-License-Identifier: MPL-2.0 // Copyright (c) 2023 HashiCorp, Inc. // SPDX-License-Identifier: MPL-2.0 package command import ( "fmt" "path/filepath" "strings" svchost "github.com/hashicorp/terraform-svchost" "github.com/opentofu/opentofu/internal/command/cliconfig" "github.com/opentofu/opentofu/internal/tfdiags" ) // LogoutCommand is a Command implementation which removes stored credentials // for a remote service host. type LogoutCommand struct { Meta } // Run implements cli.Command. func (c *LogoutCommand) Run(args []string) int { args = c.Meta.process(args) cmdFlags := c.Meta.defaultFlagSet("logout") cmdFlags.Usage = func() { c.Ui.Error(c.Help()) } if err := cmdFlags.Parse(args); err != nil { return 1 } args = cmdFlags.Args() if len(args) != 1 { c.Ui.Error( "The logout command expects exactly one argument: the host to log out of.") cmdFlags.Usage() return 1 } var diags tfdiags.Diagnostics givenHostname := args[0] hostname, err := svchost.ForComparison(givenHostname) if err != nil { diags = diags.Append(tfdiags.Sourceless( tfdiags.Error, "Invalid hostname", fmt.Sprintf("The given hostname %q is not valid: %s.", givenHostname, err.Error()), )) c.showDiagnostics(diags) return 1 } // From now on, since we've validated the given hostname, we should use // dispHostname in the UI to ensure we're presenting it in the canonical // form, in case that helps users with debugging when things aren't // working as expected. (Perhaps the normalization is part of the cause.) dispHostname := hostname.ForDisplay() creds := c.Services.CredentialsSource().(*cliconfig.CredentialsSource) filename, _ := creds.CredentialsFilePath() credsCtx := &loginCredentialsContext{ Location: creds.HostCredentialsLocation(hostname), LocalFilename: filename, // empty in the very unlikely event that we can't select a config directory for this user HelperType: creds.CredentialsHelperType(), } if credsCtx.Location == cliconfig.CredentialsInOtherFile { diags = diags.Append(tfdiags.Sourceless( tfdiags.Error, fmt.Sprintf("Credentials for %s are manually configured", dispHostname), "The \"tofu logout\" command cannot log out because credentials for this host are manually configured in a CLI configuration file.\n\nTo log out, revoke the existing credentials and remove that block from the CLI configuration.", )) } if diags.HasErrors() { c.showDiagnostics(diags) return 1 } switch credsCtx.Location { case cliconfig.CredentialsNotAvailable: c.Ui.Output(fmt.Sprintf("No credentials for %s are stored.\n", dispHostname)) return 0 case cliconfig.CredentialsViaHelper: c.Ui.Output(fmt.Sprintf("Removing the stored credentials for %s from the configured\n%q credentials helper.\n", dispHostname, credsCtx.HelperType)) case cliconfig.CredentialsInPrimaryFile: c.Ui.Output(fmt.Sprintf("Removing the stored credentials for %s from the following file:\n %s\n", dispHostname, credsCtx.LocalFilename)) } err = creds.ForgetForHost(hostname) if err != nil { diags = diags.Append(tfdiags.Sourceless( tfdiags.Error, "Failed to remove API token", fmt.Sprintf("Unable to remove stored API token: %s", err), )) } c.showDiagnostics(diags) if diags.HasErrors() { return 1 } c.Ui.Output( fmt.Sprintf( c.Colorize().Color(strings.TrimSpace(` [green][bold]Success![reset] [bold]OpenTofu has removed the stored API token for %s.[reset] `)), dispHostname, ) + "\n", ) return 0 } // Help implements cli.Command. func (c *LogoutCommand) Help() string { defaultFile := c.defaultOutputFile() if defaultFile == "" { // Because this is just for the help message and it's very unlikely // that a user wouldn't have a functioning home directory anyway, // we'll just use a placeholder here. The real command has some // more complex behavior for this case. This result is not correct // on all platforms, but given how unlikely we are to hit this case // that seems okay. defaultFile = "~/.terraform/credentials.tfrc.json" } helpText := ` Usage: tofu [global options] logout [hostname] Removes locally-stored credentials for specified hostname. Note: the API token is only removed from local storage, not destroyed on the remote server, so it will remain valid until manually revoked. %s ` return strings.TrimSpace(helpText) } // Synopsis implements cli.Command. func (c *LogoutCommand) Synopsis() string { return "Remove locally-stored credentials for a remote host" } func (c *LogoutCommand) defaultOutputFile() string { if c.CLIConfigDir == "" { return "" // no default available } return filepath.Join(c.CLIConfigDir, "credentials.tfrc.json") }