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 }