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  }