github.com/makyo/juju@v0.0.0-20160425123129-2608902037e9/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  	"github.com/juju/names"
    15  	"golang.org/x/crypto/ssh/terminal"
    16  	"gopkg.in/macaroon.v1"
    17  
    18  	"github.com/juju/juju/cmd/juju/block"
    19  	"github.com/juju/juju/cmd/modelcmd"
    20  )
    21  
    22  const userChangePasswordDoc = `
    23  Change the password for the user you are currently logged in as;
    24  or as an admin, change the password for another user.
    25  
    26  Examples:
    27    # Change the password for the user you are logged in as.
    28    juju change-user-password
    29  
    30    # Change the password for bob.
    31    juju change-user-password bob
    32  
    33  `
    34  
    35  func NewChangePasswordCommand() cmd.Command {
    36  	return modelcmd.WrapController(&changePasswordCommand{})
    37  }
    38  
    39  // changePasswordCommand changes the password for a user.
    40  type changePasswordCommand struct {
    41  	modelcmd.ControllerCommandBase
    42  	api  ChangePasswordAPI
    43  	User string
    44  }
    45  
    46  // Info implements Command.Info.
    47  func (c *changePasswordCommand) Info() *cmd.Info {
    48  	return &cmd.Info{
    49  		Name:    "change-user-password",
    50  		Args:    "[username]",
    51  		Purpose: "changes the password for a user",
    52  		Doc:     userChangePasswordDoc,
    53  	}
    54  }
    55  
    56  // Init implements Command.Init.
    57  func (c *changePasswordCommand) Init(args []string) error {
    58  	var err error
    59  	c.User, err = cmd.ZeroOrOneArgs(args)
    60  	if err != nil {
    61  		return errors.Trace(err)
    62  	}
    63  	return nil
    64  }
    65  
    66  // ChangePasswordAPI defines the usermanager API methods that the change
    67  // password command uses.
    68  type ChangePasswordAPI interface {
    69  	CreateLocalLoginMacaroon(names.UserTag) (*macaroon.Macaroon, error)
    70  	SetPassword(username, password string) error
    71  	Close() error
    72  }
    73  
    74  // Run implements Command.Run.
    75  func (c *changePasswordCommand) Run(ctx *cmd.Context) error {
    76  	if c.api == nil {
    77  		api, err := c.NewUserManagerAPIClient()
    78  		if err != nil {
    79  			return errors.Trace(err)
    80  		}
    81  		c.api = api
    82  		defer c.api.Close()
    83  	}
    84  
    85  	newPassword, err := readAndConfirmPassword(ctx)
    86  	if err != nil {
    87  		return errors.Trace(err)
    88  	}
    89  
    90  	var accountName string
    91  	controllerName := c.ControllerName()
    92  	store := c.ClientStore()
    93  	if c.User != "" {
    94  		if !names.IsValidUserName(c.User) {
    95  			return errors.NotValidf("user name %q", c.User)
    96  		}
    97  		accountName = names.NewUserTag(c.User).Canonical()
    98  	} else {
    99  		accountName, err = store.CurrentAccount(controllerName)
   100  		if err != nil {
   101  			return errors.Trace(err)
   102  		}
   103  	}
   104  	accountDetails, err := store.AccountByName(controllerName, accountName)
   105  	if err != nil && !errors.IsNotFound(err) {
   106  		return errors.Trace(err)
   107  	}
   108  
   109  	if accountDetails != nil && accountDetails.Macaroon == "" {
   110  		// Generate a macaroon first to guard against I/O failures
   111  		// occurring after the password has been changed, preventing
   112  		// future logins.
   113  		userTag := names.NewUserTag(accountName)
   114  		macaroon, err := c.api.CreateLocalLoginMacaroon(userTag)
   115  		if err != nil {
   116  			return errors.Trace(err)
   117  		}
   118  		accountDetails.Password = ""
   119  
   120  		// TODO(axw) update jujuclient with code for marshalling
   121  		// and unmarshalling macaroons as YAML.
   122  		macaroonJSON, err := macaroon.MarshalJSON()
   123  		if err != nil {
   124  			return errors.Trace(err)
   125  		}
   126  		accountDetails.Macaroon = string(macaroonJSON)
   127  
   128  		if err := store.UpdateAccount(controllerName, accountName, *accountDetails); err != nil {
   129  			return errors.Annotate(err, "failed to update client credentials")
   130  		}
   131  	}
   132  
   133  	if err := c.api.SetPassword(accountName, newPassword); err != nil {
   134  		return block.ProcessBlockedError(err, block.BlockChange)
   135  	}
   136  	if accountDetails == nil {
   137  		ctx.Infof("Password for %q has been updated.", c.User)
   138  	} else {
   139  		ctx.Infof("Your password has been updated.")
   140  	}
   141  	return nil
   142  }
   143  
   144  func readAndConfirmPassword(ctx *cmd.Context) (string, error) {
   145  	// Don't add the carriage returns before readPassword, but add
   146  	// them directly after the readPassword so any errors are output
   147  	// on their own lines.
   148  	//
   149  	// TODO(axw) retry/loop on failure
   150  	fmt.Fprint(ctx.Stderr, "password: ")
   151  	password, err := readPassword(ctx.Stdin)
   152  	fmt.Fprint(ctx.Stderr, "\n")
   153  	if err != nil {
   154  		return "", errors.Trace(err)
   155  	}
   156  	if password == "" {
   157  		return "", errors.Errorf("you must enter a password")
   158  	}
   159  
   160  	fmt.Fprint(ctx.Stderr, "type password again: ")
   161  	verify, err := readPassword(ctx.Stdin)
   162  	fmt.Fprint(ctx.Stderr, "\n")
   163  	if err != nil {
   164  		return "", errors.Trace(err)
   165  	}
   166  	if password != verify {
   167  		return "", errors.New("Passwords do not match")
   168  	}
   169  	return password, nil
   170  }
   171  
   172  func readPassword(stdin io.Reader) (string, error) {
   173  	if f, ok := stdin.(*os.File); ok && terminal.IsTerminal(int(f.Fd())) {
   174  		password, err := terminal.ReadPassword(int(f.Fd()))
   175  		if err != nil {
   176  			return "", errors.Trace(err)
   177  		}
   178  		return string(password), nil
   179  	}
   180  	return readLine(stdin)
   181  }
   182  
   183  func readLine(stdin io.Reader) (string, error) {
   184  	// Read one byte at a time to avoid reading beyond the delimiter.
   185  	line, err := bufio.NewReader(byteAtATimeReader{stdin}).ReadString('\n')
   186  	if err != nil {
   187  		return "", errors.Trace(err)
   188  	}
   189  	return line[:len(line)-1], nil
   190  }
   191  
   192  type byteAtATimeReader struct {
   193  	io.Reader
   194  }
   195  
   196  func (r byteAtATimeReader) Read(out []byte) (int, error) {
   197  	return r.Reader.Read(out[:1])
   198  }