github.com/niedbalski/juju@v0.0.0-20190215020005-8ff100488e47/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/gnuflag" 15 "golang.org/x/crypto/ssh/terminal" 16 "gopkg.in/juju/names.v2" 17 "gopkg.in/macaroon-bakery.v2-unstable/httpbakery" 18 19 "github.com/juju/juju/api" 20 "github.com/juju/juju/api/authentication" 21 jujucmd "github.com/juju/juju/cmd" 22 "github.com/juju/juju/cmd/juju/block" 23 "github.com/juju/juju/cmd/modelcmd" 24 "github.com/juju/juju/juju" 25 "github.com/juju/juju/jujuclient" 26 ) 27 28 const userChangePasswordDoc = ` 29 The user is, by default, the current user. The latter can be confirmed with 30 the ` + "`juju show-user`" + ` command. 31 32 If no controller is specified, the current controller will be used. 33 34 A controller administrator can change the password for another user 35 by providing desired username as an argument. 36 37 A controller administrator can also reset the password with a --reset option. 38 This will invalidate any passwords that were previously set 39 and registration strings that were previously issued for a user. 40 This option will issue a new registration string to be used with 41 ` + "`juju register`" + `. 42 43 44 Examples: 45 46 juju change-user-password 47 juju change-user-password bob 48 juju change-user-password bob --reset 49 juju change-user-password -c another-known-controller 50 juju change-user-password bob --controller another-known-controller 51 52 See also: 53 add-user 54 register 55 56 ` 57 58 func NewChangePasswordCommand() cmd.Command { 59 var cmd changePasswordCommand 60 cmd.newAPIConnection = juju.NewAPIConnection 61 return modelcmd.WrapController(&cmd) 62 } 63 64 // changePasswordCommand changes the password for a user. 65 type changePasswordCommand struct { 66 modelcmd.ControllerCommandBase 67 newAPIConnection func(juju.NewAPIConnectionParams) (api.Connection, error) 68 api ChangePasswordAPI 69 70 // Input arguments 71 User string 72 Reset bool 73 74 // Internally initialised and used during run 75 controllerName string 76 userTag names.UserTag 77 accountDetails *jujuclient.AccountDetails 78 } 79 80 func (c *changePasswordCommand) SetFlags(f *gnuflag.FlagSet) { 81 f.BoolVar(&c.Reset, "reset", false, "Reset user password") 82 } 83 84 // Info implements Command.Info. 85 func (c *changePasswordCommand) Info() *cmd.Info { 86 return jujucmd.Info(&cmd.Info{ 87 Name: "change-user-password", 88 Args: "[username]", 89 Purpose: "Changes the password for the current or specified Juju user.", 90 Doc: userChangePasswordDoc, 91 }) 92 } 93 94 // Init implements Command.Init. 95 func (c *changePasswordCommand) Init(args []string) error { 96 var err error 97 c.User, err = cmd.ZeroOrOneArgs(args) 98 if err != nil { 99 return errors.Trace(err) 100 } 101 return nil 102 } 103 104 // ChangePasswordAPI defines the usermanager API methods that the change 105 // password command uses. 106 type ChangePasswordAPI interface { 107 SetPassword(username, password string) error 108 ResetPassword(username string) ([]byte, error) 109 BestAPIVersion() int 110 Close() error 111 } 112 113 // Run implements Command.Run. 114 func (c *changePasswordCommand) Run(ctx *cmd.Context) error { 115 if err := c.prepareRun(); err != nil { 116 return errors.Trace(err) 117 } 118 if c.api == nil { 119 api, err := c.NewUserManagerAPIClient() 120 if err != nil { 121 return errors.Trace(err) 122 } 123 c.api = api 124 defer c.api.Close() 125 } 126 127 if c.Reset { 128 if c.User == "" || (c.accountDetails != nil && c.User == c.accountDetails.User) { 129 ctx.Infof("You cannot reset your own password.\nIf you want to change it, please call `juju change-user-password` without --reset option.") 130 return nil 131 } 132 if c.api.BestAPIVersion() < 2 { 133 return errors.NotSupportedf("on this juju controller, reset password") 134 } 135 return c.resetUserPassword(ctx) 136 } 137 return c.updateUserPassword(ctx) 138 } 139 140 func (c *changePasswordCommand) prepareRun() error { 141 err := c.ensureControllerName() 142 if err != nil { 143 return errors.Trace(err) 144 } 145 146 c.accountDetails, err = c.ClientStore().AccountDetails(c.controllerName) 147 if err != nil { 148 return errors.Trace(err) 149 } 150 151 if c.User != "" { 152 if !names.IsValidUserName(c.User) { 153 return errors.NotValidf("user name %q", c.User) 154 } 155 c.userTag = names.NewUserTag(c.User) 156 if c.userTag.Id() != c.accountDetails.User { 157 // The account details don't correspond to the username 158 // being changed, so we don't need to update the account 159 // locally. 160 c.accountDetails = nil 161 } 162 } else { 163 if !names.IsValidUser(c.accountDetails.User) { 164 return errors.Errorf("invalid user in account %q", c.accountDetails.User) 165 } 166 c.userTag = names.NewUserTag(c.accountDetails.User) 167 if !c.userTag.IsLocal() { 168 operation := "change" 169 if c.Reset { 170 operation = "reset" 171 } 172 return errors.Errorf("cannot %v password for external user %q", operation, c.userTag) 173 } 174 } 175 return nil 176 } 177 178 func (c *changePasswordCommand) ensureControllerName() error { 179 controllerName, err := c.ControllerName() 180 if err != nil { 181 return errors.Trace(err) 182 } 183 c.controllerName = controllerName 184 return nil 185 } 186 187 func (c *changePasswordCommand) resetUserPassword(ctx *cmd.Context) error { 188 key, err := c.api.ResetPassword(c.userTag.Id()) 189 if err != nil { 190 return block.ProcessBlockedError(err, block.BlockChange) 191 } 192 ctx.Infof("Password for %q has been reset.", c.User) 193 base64RegistrationData, err := generateUserControllerAccessToken( 194 c.ControllerCommandBase, 195 c.userTag.Id(), 196 key, 197 ) 198 if err != nil { 199 return errors.Annotate(err, "generating controller user access token") 200 } 201 ctx.Infof("Ask the user to run:\n juju register %s\n", base64RegistrationData) 202 return nil 203 } 204 205 func (c *changePasswordCommand) updateUserPassword(ctx *cmd.Context) error { 206 newPassword, err := readAndConfirmPassword(ctx) 207 if err != nil { 208 return errors.Trace(err) 209 } 210 211 if err := c.api.SetPassword(c.userTag.Id(), newPassword); err != nil { 212 return block.ProcessBlockedError(err, block.BlockChange) 213 } 214 if c.accountDetails == nil { 215 ctx.Infof("Password for %q has been changed.", c.User) 216 } else { 217 if c.accountDetails.Password != "" { 218 // Log back in with macaroon authentication, so we can 219 // discard the password without having to log back in 220 // immediately. 221 if err := c.recordMacaroon(newPassword); err != nil { 222 return errors.Annotate(err, "recording macaroon") 223 } 224 // Wipe the password from disk. In the event of an 225 // error occurring after SetPassword and before the 226 // account details being updated, the user will be 227 // able to recover by running "juju login". 228 c.accountDetails.Password = "" 229 if err := c.ClientStore().UpdateAccount(c.controllerName, *c.accountDetails); err != nil { 230 return errors.Annotate(err, "failed to update client credentials") 231 } 232 } 233 ctx.Infof("Your password has been changed.") 234 } 235 return nil 236 } 237 238 func (c *changePasswordCommand) recordMacaroon(password string) error { 239 accountDetails := &jujuclient.AccountDetails{User: c.accountDetails.User} 240 args, err := c.NewAPIConnectionParams( 241 c.ClientStore(), c.controllerName, "", accountDetails, 242 ) 243 if err != nil { 244 return errors.Trace(err) 245 } 246 args.DialOpts.BakeryClient.WebPageVisitor = httpbakery.NewMultiVisitor( 247 authentication.NewVisitor(accountDetails.User, func(string) (string, error) { 248 return password, nil 249 }), 250 args.DialOpts.BakeryClient.WebPageVisitor, 251 ) 252 api, err := c.newAPIConnection(args) 253 if err != nil { 254 return errors.Annotate(err, "connecting to API") 255 } 256 return api.Close() 257 } 258 259 func readAndConfirmPassword(ctx *cmd.Context) (string, error) { 260 // Don't add the carriage returns before readPassword, but add 261 // them directly after the readPassword so any errors are output 262 // on their own lines. 263 // 264 // TODO(axw) retry/loop on failure 265 fmt.Fprint(ctx.Stderr, "new password: ") 266 password, err := readPassword(ctx.Stdin) 267 fmt.Fprint(ctx.Stderr, "\n") 268 if err != nil { 269 return "", errors.Trace(err) 270 } 271 if password == "" { 272 return "", errors.Errorf("you must enter a password") 273 } 274 275 fmt.Fprint(ctx.Stderr, "type new password again: ") 276 verify, err := readPassword(ctx.Stdin) 277 fmt.Fprint(ctx.Stderr, "\n") 278 if err != nil { 279 return "", errors.Trace(err) 280 } 281 if password != verify { 282 return "", errors.New("Passwords do not match") 283 } 284 return password, nil 285 } 286 287 func readPassword(stdin io.Reader) (string, error) { 288 if f, ok := stdin.(*os.File); ok && terminal.IsTerminal(int(f.Fd())) { 289 password, err := terminal.ReadPassword(int(f.Fd())) 290 if err != nil { 291 return "", errors.Trace(err) 292 } 293 return string(password), nil 294 } 295 return readLine(stdin) 296 } 297 298 func readLine(stdin io.Reader) (string, error) { 299 // Read one byte at a time to avoid reading beyond the delimiter. 300 line, err := bufio.NewReader(byteAtATimeReader{stdin}).ReadString('\n') 301 if err != nil { 302 return "", errors.Trace(err) 303 } 304 return line[:len(line)-1], nil 305 } 306 307 type byteAtATimeReader struct { 308 io.Reader 309 } 310 311 func (r byteAtATimeReader) Read(out []byte) (int, error) { 312 return r.Reader.Read(out[:1]) 313 }