github.com/wallyworld/juju@v0.0.0-20161013125918-6cf1bc9d917a/cmd/juju/user/change_password.go (about)

     1  // Copyright 2014 Canonical Ltd.
     2  // Licensed under the AGPLv3, see LICENCE file for details.
     3  
     4  package user
     5  
     6  import (
     7  	"bufio"
     8  	"fmt"
     9  	"io"
    10  	"os"
    11  
    12  	"github.com/juju/cmd"
    13  	"github.com/juju/errors"
    14  	"golang.org/x/crypto/ssh/terminal"
    15  	"gopkg.in/juju/names.v2"
    16  	"gopkg.in/macaroon-bakery.v1/httpbakery"
    17  
    18  	"github.com/juju/juju/api"
    19  	"github.com/juju/juju/api/authentication"
    20  	"github.com/juju/juju/cmd/juju/block"
    21  	"github.com/juju/juju/cmd/modelcmd"
    22  	"github.com/juju/juju/juju"
    23  	"github.com/juju/juju/jujuclient"
    24  )
    25  
    26  const userChangePasswordDoc = `
    27  The user is, by default, the current user. The latter can be confirmed with
    28  the ` + "`juju show-user`" + ` command.
    29  
    30  A controller administrator can change the password for another user (on
    31  that controller).
    32  
    33  Examples:
    34  
    35      juju change-user-password
    36      juju change-user-password bob
    37  
    38  See also:
    39      add-user
    40  
    41  `
    42  
    43  func NewChangePasswordCommand() cmd.Command {
    44  	var cmd changePasswordCommand
    45  	cmd.newAPIConnection = juju.NewAPIConnection
    46  	return modelcmd.WrapController(&cmd)
    47  }
    48  
    49  // changePasswordCommand changes the password for a user.
    50  type changePasswordCommand struct {
    51  	modelcmd.ControllerCommandBase
    52  	newAPIConnection func(juju.NewAPIConnectionParams) (api.Connection, error)
    53  	api              ChangePasswordAPI
    54  	User             string
    55  }
    56  
    57  // Info implements Command.Info.
    58  func (c *changePasswordCommand) Info() *cmd.Info {
    59  	return &cmd.Info{
    60  		Name:    "change-user-password",
    61  		Args:    "[username]",
    62  		Purpose: "Changes the password for the current or specified Juju user",
    63  		Doc:     userChangePasswordDoc,
    64  	}
    65  }
    66  
    67  // Init implements Command.Init.
    68  func (c *changePasswordCommand) Init(args []string) error {
    69  	var err error
    70  	c.User, err = cmd.ZeroOrOneArgs(args)
    71  	if err != nil {
    72  		return errors.Trace(err)
    73  	}
    74  	return nil
    75  }
    76  
    77  // ChangePasswordAPI defines the usermanager API methods that the change
    78  // password command uses.
    79  type ChangePasswordAPI interface {
    80  	SetPassword(username, password string) error
    81  	Close() error
    82  }
    83  
    84  // Run implements Command.Run.
    85  func (c *changePasswordCommand) Run(ctx *cmd.Context) error {
    86  	if c.api == nil {
    87  		api, err := c.NewUserManagerAPIClient()
    88  		if err != nil {
    89  			return errors.Trace(err)
    90  		}
    91  		c.api = api
    92  		defer c.api.Close()
    93  	}
    94  
    95  	newPassword, err := readAndConfirmPassword(ctx)
    96  	if err != nil {
    97  		return errors.Trace(err)
    98  	}
    99  
   100  	controllerName := c.ControllerName()
   101  	store := c.ClientStore()
   102  	accountDetails, err := store.AccountDetails(controllerName)
   103  	if err != nil {
   104  		return errors.Trace(err)
   105  	}
   106  
   107  	var userTag names.UserTag
   108  	if c.User != "" {
   109  		if !names.IsValidUserName(c.User) {
   110  			return errors.NotValidf("user name %q", c.User)
   111  		}
   112  		userTag = names.NewUserTag(c.User)
   113  		if userTag.Id() != accountDetails.User {
   114  			// The account details don't correspond to the username
   115  			// being changed, so we don't need to update the account
   116  			// locally.
   117  			accountDetails = nil
   118  		}
   119  	} else {
   120  		if !names.IsValidUser(accountDetails.User) {
   121  			return errors.Errorf("invalid user in account %q", accountDetails.User)
   122  		}
   123  		userTag = names.NewUserTag(accountDetails.User)
   124  		if !userTag.IsLocal() {
   125  			return errors.Errorf("cannot change password for external user %q", userTag)
   126  		}
   127  	}
   128  	if err := c.api.SetPassword(userTag.Id(), newPassword); err != nil {
   129  		return block.ProcessBlockedError(err, block.BlockChange)
   130  	}
   131  
   132  	if accountDetails == nil {
   133  		ctx.Infof("Password for %q has been updated.", c.User)
   134  	} else {
   135  		if accountDetails.Password != "" {
   136  			// Log back in with macaroon authentication, so we can
   137  			// discard the password without having to log back in
   138  			// immediately.
   139  			if err := c.recordMacaroon(accountDetails.User, newPassword); err != nil {
   140  				return errors.Annotate(err, "recording macaroon")
   141  			}
   142  			// Wipe the password from disk. In the event of an
   143  			// error occurring after SetPassword and before the
   144  			// account details being updated, the user will be
   145  			// able to recover by running "juju login".
   146  			accountDetails.Password = ""
   147  			if err := store.UpdateAccount(controllerName, *accountDetails); err != nil {
   148  				return errors.Annotate(err, "failed to update client credentials")
   149  			}
   150  		}
   151  		ctx.Infof("Your password has been updated.")
   152  	}
   153  	return nil
   154  }
   155  
   156  func (c *changePasswordCommand) recordMacaroon(user, password string) error {
   157  	accountDetails := &jujuclient.AccountDetails{User: user}
   158  	args, err := c.NewAPIConnectionParams(
   159  		c.ClientStore(), c.ControllerName(), "", accountDetails,
   160  	)
   161  	if err != nil {
   162  		return errors.Trace(err)
   163  	}
   164  	args.DialOpts.BakeryClient.WebPageVisitor = httpbakery.NewMultiVisitor(
   165  		authentication.NewVisitor(accountDetails.User, func(string) (string, error) {
   166  			return password, nil
   167  		}),
   168  		args.DialOpts.BakeryClient.WebPageVisitor,
   169  	)
   170  	api, err := c.newAPIConnection(args)
   171  	if err != nil {
   172  		return errors.Annotate(err, "connecting to API")
   173  	}
   174  	return api.Close()
   175  }
   176  
   177  func readAndConfirmPassword(ctx *cmd.Context) (string, error) {
   178  	// Don't add the carriage returns before readPassword, but add
   179  	// them directly after the readPassword so any errors are output
   180  	// on their own lines.
   181  	//
   182  	// TODO(axw) retry/loop on failure
   183  	fmt.Fprint(ctx.Stderr, "new password: ")
   184  	password, err := readPassword(ctx.Stdin)
   185  	fmt.Fprint(ctx.Stderr, "\n")
   186  	if err != nil {
   187  		return "", errors.Trace(err)
   188  	}
   189  	if password == "" {
   190  		return "", errors.Errorf("you must enter a password")
   191  	}
   192  
   193  	fmt.Fprint(ctx.Stderr, "type new password again: ")
   194  	verify, err := readPassword(ctx.Stdin)
   195  	fmt.Fprint(ctx.Stderr, "\n")
   196  	if err != nil {
   197  		return "", errors.Trace(err)
   198  	}
   199  	if password != verify {
   200  		return "", errors.New("Passwords do not match")
   201  	}
   202  	return password, nil
   203  }
   204  
   205  func readPassword(stdin io.Reader) (string, error) {
   206  	if f, ok := stdin.(*os.File); ok && terminal.IsTerminal(int(f.Fd())) {
   207  		password, err := terminal.ReadPassword(int(f.Fd()))
   208  		if err != nil {
   209  			return "", errors.Trace(err)
   210  		}
   211  		return string(password), nil
   212  	}
   213  	return readLine(stdin)
   214  }
   215  
   216  func readLine(stdin io.Reader) (string, error) {
   217  	// Read one byte at a time to avoid reading beyond the delimiter.
   218  	line, err := bufio.NewReader(byteAtATimeReader{stdin}).ReadString('\n')
   219  	if err != nil {
   220  		return "", errors.Trace(err)
   221  	}
   222  	return line[:len(line)-1], nil
   223  }
   224  
   225  type byteAtATimeReader struct {
   226  	io.Reader
   227  }
   228  
   229  func (r byteAtATimeReader) Read(out []byte) (int, error) {
   230  	return r.Reader.Read(out[:1])
   231  }