github.com/niedbalski/juju@v0.0.0-20190215020005-8ff100488e47/cmd/juju/user/login.go (about)

     1  // Copyright 2016 Canonical Ltd.
     2  // Licensed under the AGPLv3, see LICENCE file for details.
     3  
     4  package user
     5  
     6  import (
     7  	"fmt"
     8  	"net/http"
     9  	"os"
    10  	"strings"
    11  
    12  	"github.com/juju/cmd"
    13  	"github.com/juju/collections/set"
    14  	"github.com/juju/errors"
    15  	"github.com/juju/gnuflag"
    16  	"github.com/juju/httprequest"
    17  	"gopkg.in/juju/names.v2"
    18  
    19  	"github.com/juju/juju/api"
    20  	apibase "github.com/juju/juju/api/base"
    21  	"github.com/juju/juju/api/modelmanager"
    22  	"github.com/juju/juju/apiserver/params"
    23  	jujucmd "github.com/juju/juju/cmd"
    24  	"github.com/juju/juju/cmd/juju/common"
    25  	"github.com/juju/juju/cmd/modelcmd"
    26  	"github.com/juju/juju/juju"
    27  	"github.com/juju/juju/jujuclient"
    28  )
    29  
    30  const loginDoc = `
    31  By default, the juju login command logs the user into a controller.
    32  The argument to the command can be a public controller
    33  host name or alias (see Aliases below).
    34  
    35  If no argument is provided, the controller specified with
    36  the -c argument will be used, or the current controller
    37  if that's not provided.
    38  
    39  On success, the current controller is switched to the logged-in
    40  controller.
    41  
    42  If the user is already logged in, the juju login command does nothing
    43  except verify that fact.
    44  
    45  If the -u option is provided, the juju login command will attempt to log
    46  into the controller as that user.
    47  
    48  After login, a token ("macaroon") will become active. It has an expiration
    49  time of 24 hours. Upon expiration, no further Juju commands can be issued
    50  and the user will be prompted to log in again.
    51  
    52  Aliases
    53  -------
    54  
    55  Public controller aliases are provided by a directory service
    56  that is queried to find the host name for a given alias.
    57  The URL for the directory service may be configured
    58  by setting the environment variable JUJU_DIRECTORY.
    59  
    60  Examples:
    61  
    62      juju login somepubliccontroller
    63      juju login jimm.jujucharms.com
    64      juju login -u bob
    65  
    66  See also:
    67      disable-user
    68      enable-user
    69      logout
    70      register
    71      unregister
    72  `
    73  
    74  // Functions defined as variables so they can be overridden in tests.
    75  var (
    76  	apiOpen          = (*modelcmd.CommandBase).APIOpen
    77  	newAPIConnection = juju.NewAPIConnection
    78  	listModels       = func(c api.Connection, userName string) ([]apibase.UserModel, error) {
    79  		return modelmanager.NewClient(c).ListModels(userName)
    80  	}
    81  	// loginClientStore is used as the client store. When it is nil,
    82  	// the default client store will be used.
    83  	loginClientStore jujuclient.ClientStore
    84  )
    85  
    86  // NewLoginCommand returns a new cmd.Command to handle "juju login".
    87  func NewLoginCommand() cmd.Command {
    88  	var c loginCommand
    89  	c.SetClientStore(loginClientStore)
    90  	c.CanClearCurrentModel = true
    91  	return modelcmd.WrapController(&c, modelcmd.WrapControllerSkipControllerFlags)
    92  }
    93  
    94  // loginCommand changes the password for a user.
    95  type loginCommand struct {
    96  	modelcmd.ControllerCommandBase
    97  	domain   string
    98  	username string
    99  
   100  	// controllerName holds the name of the current controller.
   101  	// We define this and the --controller flag here because
   102  	// the controller does not necessarily exist when the command
   103  	// is executed.
   104  	controllerName string
   105  
   106  	// onRunError is executed if non-nil if there is an error at the end
   107  	// of the Run method.
   108  	onRunError func()
   109  }
   110  
   111  // Info implements Command.Info.
   112  func (c *loginCommand) Info() *cmd.Info {
   113  	return jujucmd.Info(&cmd.Info{
   114  		Name:    "login",
   115  		Args:    "[controller host name or alias]",
   116  		Purpose: "Logs a user in to a controller.",
   117  		Doc:     loginDoc,
   118  	})
   119  }
   120  
   121  func (c *loginCommand) SetFlags(fset *gnuflag.FlagSet) {
   122  	c.ControllerCommandBase.SetFlags(fset)
   123  	fset.StringVar(&c.controllerName, "c", "", "Controller to operate in")
   124  	fset.StringVar(&c.controllerName, "controller", "", "")
   125  	fset.StringVar(&c.username, "u", "", "log in as this local user")
   126  	fset.StringVar(&c.username, "user", "", "")
   127  }
   128  
   129  // Init implements Command.Init.
   130  func (c *loginCommand) Init(args []string) error {
   131  	domain, err := cmd.ZeroOrOneArgs(args)
   132  	if err != nil {
   133  		return errors.Trace(err)
   134  	}
   135  	c.domain = domain
   136  	return nil
   137  }
   138  
   139  // Run implements Command.Run.
   140  func (c *loginCommand) Run(ctx *cmd.Context) error {
   141  	err := c.run(ctx)
   142  	if err != nil && c.onRunError != nil {
   143  		c.onRunError()
   144  	}
   145  	return err
   146  }
   147  
   148  func (c *loginCommand) run(ctx *cmd.Context) error {
   149  	store := c.ClientStore()
   150  	switch {
   151  	case c.controllerName == "" && c.domain == "":
   152  		current, err := store.CurrentController()
   153  		if err != nil && !errors.IsNotFound(err) {
   154  			return errors.Annotatef(err, "cannot get current controller")
   155  		}
   156  		c.controllerName = current
   157  	case c.controllerName == "":
   158  		c.controllerName = c.domain
   159  	}
   160  	if strings.Contains(c.controllerName, ":") {
   161  		return errors.Errorf("cannot use %q as a controller name - use -c option to choose a different one", c.controllerName)
   162  	}
   163  
   164  	// Find out details on the specified controller if there is one.
   165  	var controllerDetails *jujuclient.ControllerDetails
   166  	if c.controllerName != "" {
   167  		d, err := store.ControllerByName(c.controllerName)
   168  		if err != nil && !errors.IsNotFound(err) {
   169  			return errors.Trace(err)
   170  		}
   171  		controllerDetails = d
   172  	}
   173  
   174  	// Find out details of the controller domain if it's specified.
   175  	var (
   176  		conn                    api.Connection
   177  		publicControllerDetails *jujuclient.ControllerDetails
   178  		accountDetails          *jujuclient.AccountDetails
   179  		oldAccountDetails       *jujuclient.AccountDetails
   180  		err                     error
   181  	)
   182  	if controllerDetails != nil {
   183  		// Fetch current details for the specified controller name so we
   184  		// can tell if the logged in user has changed.
   185  		d, err := store.AccountDetails(c.controllerName)
   186  		if err != nil && !errors.IsNotFound(err) {
   187  			return errors.Trace(err)
   188  		}
   189  		oldAccountDetails = d
   190  	}
   191  	switch {
   192  	case c.domain != "":
   193  		// Note: the controller name is guaranteed to be non-empty
   194  		// in this case via the test at the start of this function.
   195  		conn, publicControllerDetails, accountDetails, err = c.publicControllerLogin(ctx, c.domain, c.controllerName, oldAccountDetails)
   196  		if err != nil {
   197  			return errors.Annotatef(err, "cannot log into %q", c.domain)
   198  		}
   199  	case controllerDetails == nil && c.controllerName != "":
   200  		// No controller found and no domain specified - we
   201  		// have no idea where we should be logging in.
   202  		return errors.Errorf("controller %q does not exist", c.controllerName)
   203  	case controllerDetails == nil:
   204  		return errors.Errorf("no current controller")
   205  	default:
   206  		conn, accountDetails, err = c.existingControllerLogin(ctx, store, c.controllerName, oldAccountDetails)
   207  		if err != nil {
   208  			return errors.Annotatef(err, "cannot log into controller %q", c.controllerName)
   209  		}
   210  	}
   211  	defer conn.Close()
   212  	if controllerDetails != nil && publicControllerDetails != nil && controllerDetails.ControllerUUID != publicControllerDetails.ControllerUUID {
   213  		// The domain we're trying to log into doesn't match the
   214  		// existing controller.
   215  		return errors.Errorf(`
   216  controller at %q does not match existing controller.
   217  Please choose a different controller name with the -c option, or
   218  use "juju unregister %s" to remove the existing controller.`[1:], c.domain, c.controllerName)
   219  	}
   220  	if controllerDetails == nil {
   221  		// The controller did not exist previously, so create it.
   222  		// Note that the "controllerDetails == nil"
   223  		// test above means that we will always have a valid publicControllerDetails
   224  		// value here.
   225  		if err := store.AddController(c.controllerName, *publicControllerDetails); err != nil {
   226  			return errors.Trace(err)
   227  		}
   228  	}
   229  	accountDetails.LastKnownAccess = conn.ControllerAccess()
   230  	if err := store.UpdateAccount(c.controllerName, *accountDetails); err != nil {
   231  		return errors.Annotatef(err, "cannot update account information: %v", err)
   232  	}
   233  	if err := store.SetCurrentController(c.controllerName); err != nil {
   234  		return errors.Annotatef(err, "cannot switch")
   235  	}
   236  	if controllerDetails != nil && oldAccountDetails != nil && oldAccountDetails.User == accountDetails.User {
   237  		// We're still using the same controller and the same user name,
   238  		// so no need to list models or set the current controller
   239  		return nil
   240  	}
   241  	// Now list the models available so we can show them and store their
   242  	// details locally.
   243  	models, err := listModels(conn, accountDetails.User)
   244  	if err != nil {
   245  		return errors.Trace(err)
   246  	}
   247  	if err := c.SetControllerModels(store, c.controllerName, models); err != nil {
   248  		return errors.Annotate(err, "storing model details")
   249  	}
   250  	fmt.Fprintf(
   251  		ctx.Stderr, "Welcome, %s. You are now logged into %q.\n",
   252  		friendlyUserName(accountDetails.User), c.controllerName,
   253  	)
   254  	return c.maybeSetCurrentModel(ctx, store, c.controllerName, accountDetails.User, models)
   255  }
   256  
   257  func (c *loginCommand) existingControllerLogin(ctx *cmd.Context, store jujuclient.ClientStore, controllerName string, currentAccountDetails *jujuclient.AccountDetails) (api.Connection, *jujuclient.AccountDetails, error) {
   258  	dial := func(accountDetails *jujuclient.AccountDetails) (api.Connection, error) {
   259  		args, err := c.NewAPIConnectionParams(store, controllerName, "", accountDetails)
   260  		if err != nil {
   261  			return nil, errors.Trace(err)
   262  		}
   263  		return newAPIConnection(args)
   264  	}
   265  	return c.login(ctx, currentAccountDetails, dial)
   266  }
   267  
   268  // publicControllerLogin logs into the public controller at the given
   269  // host. The currentAccountDetails parameter holds existing account
   270  // information about the controller account.
   271  func (c *loginCommand) publicControllerLogin(
   272  	ctx *cmd.Context,
   273  	host string,
   274  	controllerName string,
   275  	currentAccountDetails *jujuclient.AccountDetails,
   276  ) (api.Connection, *jujuclient.ControllerDetails, *jujuclient.AccountDetails, error) {
   277  	fail := func(err error) (api.Connection, *jujuclient.ControllerDetails, *jujuclient.AccountDetails, error) {
   278  		return nil, nil, nil, err
   279  	}
   280  	if !strings.ContainsAny(host, ".:") {
   281  		host1, err := c.getKnownControllerDomain(host, controllerName)
   282  		if errors.IsNotFound(err) {
   283  			return fail(errors.Errorf("%q is not a known public controller", host))
   284  		}
   285  		if err != nil {
   286  			return fail(errors.Annotatef(err, "could not determine controller host name"))
   287  		}
   288  		host = host1
   289  	} else if !strings.Contains(host, ":") {
   290  		host += ":443"
   291  	}
   292  
   293  	// Make a direct API connection because we don't yet know the
   294  	// controller UUID so can't store the thus-incomplete controller
   295  	// details to make a conventional connection.
   296  	//
   297  	// Unfortunately this means we'll connect twice to the controller
   298  	// but it's probably best to go through the conventional path the
   299  	// second time.
   300  	bclient, err := c.CommandBase.BakeryClient(c.ClientStore(), controllerName)
   301  	if err != nil {
   302  		return fail(errors.Trace(err))
   303  	}
   304  	dialOpts := api.DefaultDialOpts()
   305  	dialOpts.BakeryClient = bclient
   306  
   307  	dial := func(d *jujuclient.AccountDetails) (api.Connection, error) {
   308  		var tag names.Tag
   309  		if d.User != "" {
   310  			tag = names.NewUserTag(d.User)
   311  		}
   312  		return apiOpen(&c.CommandBase, &api.Info{
   313  			Tag:      tag,
   314  			Password: d.Password,
   315  			Addrs:    []string{host},
   316  		}, dialOpts)
   317  	}
   318  	conn, accountDetails, err := c.login(ctx, currentAccountDetails, dial)
   319  	if err != nil {
   320  		return fail(errors.Trace(err))
   321  	}
   322  	// If we get to here, then we have a cached macaroon for the registered
   323  	// user. If we encounter an error after here, we need to clear it.
   324  	c.onRunError = func() {
   325  		if err := c.ClearControllerMacaroons(c.ClientStore(), controllerName); err != nil {
   326  			logger.Errorf("failed to clear macaroon: %v", err)
   327  		}
   328  	}
   329  	return conn,
   330  		&jujuclient.ControllerDetails{
   331  			APIEndpoints:   []string{host},
   332  			ControllerUUID: conn.ControllerTag().Id(),
   333  		}, accountDetails, nil
   334  }
   335  
   336  // login logs into a controller using the given account details by
   337  // default, but falling back to prompting for a username and password if
   338  // necessary. The details of making an API connection are abstracted out
   339  // into the dial function because we need to dial differently depending
   340  // on whether we have some existing local controller information or not.
   341  //
   342  // The dial function should make API connection using the account
   343  // details that it is passed.
   344  func (c *loginCommand) login(
   345  	ctx *cmd.Context,
   346  	accountDetails *jujuclient.AccountDetails,
   347  	dial func(*jujuclient.AccountDetails) (api.Connection, error),
   348  ) (api.Connection, *jujuclient.AccountDetails, error) {
   349  	username := c.username
   350  	if c.username != "" && accountDetails != nil && accountDetails.User != c.username {
   351  		// The user has specified a different username than the
   352  		// user we've found in the controller's account details.
   353  		return nil, nil, errors.Errorf(`already logged in as %s.
   354  
   355  Run "juju logout" first before attempting to log in as a different user.`,
   356  			accountDetails.User)
   357  	}
   358  
   359  	if accountDetails != nil && accountDetails.Password != "" {
   360  		// We've been provided some account details that
   361  		// contain a password, so try that first.
   362  		conn, err := dial(accountDetails)
   363  		if err == nil {
   364  			return conn, accountDetails, nil
   365  		}
   366  		if !errors.IsUnauthorized(err) {
   367  			return nil, nil, errors.Trace(err)
   368  		}
   369  	}
   370  	if c.username == "" {
   371  		// No username specified, so try external-user login first.
   372  		conn, err := dial(&jujuclient.AccountDetails{})
   373  		if err == nil {
   374  			user, ok := conn.AuthTag().(names.UserTag)
   375  			if !ok {
   376  				conn.Close()
   377  				return nil, nil, errors.Errorf("logged in as %v, not a user", conn.AuthTag())
   378  			}
   379  			return conn, &jujuclient.AccountDetails{
   380  				User: user.Id(),
   381  			}, nil
   382  		}
   383  		if !params.IsCodeNoCreds(err) {
   384  			return nil, nil, errors.Trace(err)
   385  		}
   386  		// CodeNoCreds was returned, which means that external
   387  		// users are not supported. Fall back to prompting the
   388  		// user for their username and password.
   389  
   390  		fmt.Fprint(ctx.Stderr, "username: ")
   391  		u, err := readLine(ctx.Stdin)
   392  		if err != nil {
   393  			return nil, nil, errors.Trace(err)
   394  		}
   395  		if u == "" {
   396  			return nil, nil, errors.Errorf("you must specify a username")
   397  		}
   398  		username = u
   399  	}
   400  	// Log in without specifying a password in the account details. This
   401  	// will trigger macaroon-based authentication, which will prompt the
   402  	// user for their password.
   403  	accountDetails = &jujuclient.AccountDetails{
   404  		User: username,
   405  	}
   406  	conn, err := dial(accountDetails)
   407  	return conn, accountDetails, errors.Trace(err)
   408  }
   409  
   410  const noModelsMessage = `
   411  There are no models available. You can add models with
   412  "juju add-model", or you can ask an administrator or owner
   413  of a model to grant access to that model with "juju grant".
   414  `
   415  
   416  func (c *loginCommand) maybeSetCurrentModel(ctx *cmd.Context, store jujuclient.ClientStore, controllerName, userName string, models []apibase.UserModel) error {
   417  	if len(models) == 0 {
   418  		fmt.Fprint(ctx.Stderr, noModelsMessage)
   419  		return nil
   420  	}
   421  
   422  	// If we get to here, there is at least one model.
   423  	if len(models) == 1 {
   424  		// There is exactly one model shared,
   425  		// so set it as the current model.
   426  		model := models[0]
   427  		owner := names.NewUserTag(model.Owner)
   428  		modelName := jujuclient.JoinOwnerModelName(owner, model.Name)
   429  		err := store.SetCurrentModel(controllerName, modelName)
   430  		if err != nil {
   431  			return errors.Trace(err)
   432  		}
   433  		fmt.Fprintf(ctx.Stderr, "\nCurrent model set to %q.\n", modelName)
   434  		return nil
   435  	}
   436  	fmt.Fprintf(ctx.Stderr, `
   437  There are %d models available. Use "juju switch" to select
   438  one of them:
   439  `, len(models))
   440  	user := names.NewUserTag(userName)
   441  	ownerModelNames := make(set.Strings)
   442  	otherModelNames := make(set.Strings)
   443  	for _, model := range models {
   444  		if model.Owner == userName {
   445  			ownerModelNames.Add(model.Name)
   446  			continue
   447  		}
   448  		owner := names.NewUserTag(model.Owner)
   449  		modelName := common.OwnerQualifiedModelName(model.Name, owner, user)
   450  		otherModelNames.Add(modelName)
   451  	}
   452  	for _, modelName := range ownerModelNames.SortedValues() {
   453  		fmt.Fprintf(ctx.Stderr, "  - juju switch %s\n", modelName)
   454  	}
   455  	for _, modelName := range otherModelNames.SortedValues() {
   456  		fmt.Fprintf(ctx.Stderr, "  - juju switch %s\n", modelName)
   457  	}
   458  	return nil
   459  }
   460  
   461  type controllerDomainResponse struct {
   462  	Host string `json:"host"`
   463  }
   464  
   465  const defaultJujuDirectory = "https://api.jujucharms.com/directory"
   466  
   467  // getKnownControllerDomain returns the list of known
   468  // controller domain aliases.
   469  func (c *loginCommand) getKnownControllerDomain(name, controllerName string) (string, error) {
   470  	if strings.Contains(name, ".") || strings.Contains(name, ":") {
   471  		return "", errors.NotFoundf("controller %q", name)
   472  	}
   473  	baseURL := defaultJujuDirectory
   474  	if u := os.Getenv("JUJU_DIRECTORY"); u != "" {
   475  		baseURL = u
   476  	}
   477  	client, err := c.CommandBase.BakeryClient(c.ClientStore(), controllerName)
   478  	if err != nil {
   479  		return "", errors.Trace(err)
   480  	}
   481  	req, err := http.NewRequest("GET", baseURL+"/v1/controller/"+name, nil)
   482  	if err != nil {
   483  		return "", errors.Trace(err)
   484  	}
   485  	httpResp, err := client.Do(req)
   486  	if err != nil {
   487  		return "", errors.Trace(err)
   488  	}
   489  	defer httpResp.Body.Close()
   490  	if httpResp.StatusCode != http.StatusOK {
   491  		if httpResp.StatusCode == http.StatusNotFound {
   492  			return "", errors.NotFoundf("controller %q", name)
   493  		}
   494  		return "", errors.Errorf("unexpected HTTP response %q", httpResp.Status)
   495  	}
   496  	var resp controllerDomainResponse
   497  	if err := httprequest.UnmarshalJSONResponse(httpResp, &resp); err != nil {
   498  		return "", errors.Trace(err)
   499  	}
   500  	if resp.Host == "" {
   501  		return "", errors.Errorf("no host field found in response")
   502  	}
   503  	return resp.Host, nil
   504  }
   505  
   506  func friendlyUserName(user string) string {
   507  	u := names.NewUserTag(user)
   508  	if u.IsLocal() {
   509  		return u.Name()
   510  	}
   511  	return u.Id()
   512  }