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 }