github.com/mhilton/juju-juju@v0.0.0-20150901100907-a94dd2c73455/cmd/juju/system/login.go (about)

     1  // Copyright 2015 Canonical Ltd.
     2  // Licensed under the AGPLv3, see LICENCE file for details.
     3  
     4  package system
     5  
     6  import (
     7  	"github.com/juju/cmd"
     8  	"github.com/juju/errors"
     9  	"github.com/juju/juju/api/usermanager"
    10  	"github.com/juju/names"
    11  	"github.com/juju/utils"
    12  	goyaml "gopkg.in/yaml.v1"
    13  	"launchpad.net/gnuflag"
    14  
    15  	"github.com/juju/juju/api"
    16  	"github.com/juju/juju/cmd/envcmd"
    17  	"github.com/juju/juju/environs/configstore"
    18  	"github.com/juju/juju/juju"
    19  	"github.com/juju/juju/network"
    20  )
    21  
    22  // GetUserManagerFunc defines a function that takes an api connection
    23  // and returns the (locally defined) UserManager interface.
    24  type GetUserManagerFunc func(conn api.Connection) (UserManager, error)
    25  
    26  // LoginCommand logs in to a Juju system and caches the connection
    27  // information.
    28  type LoginCommand struct {
    29  	cmd.CommandBase
    30  	apiOpen        api.OpenFunc
    31  	getUserManager GetUserManagerFunc
    32  	// TODO (thumper): when we support local cert definitions
    33  	// allow the use to specify the user and server address.
    34  	// user      string
    35  	// address   string
    36  	Server       cmd.FileVar
    37  	Name         string
    38  	KeepPassword bool
    39  }
    40  
    41  var loginDoc = `
    42  login connects to a juju system and caches the information that juju
    43  needs to connect to the api server in the $(JUJU_HOME)/environments directory.
    44  
    45  In order to login to a system, you need to have a user already created for you
    46  in that system. The way that this occurs is for an existing user on the system
    47  to create you as a user. This will generate a file that contains the
    48  information needed to connect.
    49  
    50  If you have been sent one of these server files, you can login by doing the
    51  following:
    52  
    53      # if you have saved the server file as ~/erica.server
    54      juju system login --server=~/erica.server test-system
    55  
    56  A new strong random password is generated to replace the password defined in
    57  the server file. The 'test-system' will also become the current system that
    58  the juju command will talk to by default.
    59  
    60  If you have used the 'api-info' command to generate a copy of your current
    61  credentials for a system, you should use the --keep-password option as it will
    62  mean that you will still be able to connect to the api server from the
    63  computer where you ran api-info.
    64  
    65  See Also:
    66      juju help system environments
    67      juju help system use-environment
    68      juju help system create-environment
    69      juju help user add
    70      juju help switch
    71  `
    72  
    73  // Info implements Command.Info
    74  func (c *LoginCommand) Info() *cmd.Info {
    75  	return &cmd.Info{
    76  		Name: "login",
    77  		// TODO(thumper): support user and address options
    78  		// Args: "<name> [<server address>[:<server port>]]"
    79  		Args:    "<name>",
    80  		Purpose: "login to a Juju System",
    81  		Doc:     loginDoc,
    82  	}
    83  }
    84  
    85  // SetFlags implements Command.SetFlags.
    86  func (c *LoginCommand) SetFlags(f *gnuflag.FlagSet) {
    87  	f.Var(&c.Server, "server", "path to yaml-formatted server file")
    88  	f.BoolVar(&c.KeepPassword, "keep-password", false, "do not generate a new random password")
    89  }
    90  
    91  // SetFlags implements Command.Init.
    92  func (c *LoginCommand) Init(args []string) error {
    93  	if c.apiOpen == nil {
    94  		c.apiOpen = apiOpen
    95  	}
    96  	if c.getUserManager == nil {
    97  		c.getUserManager = getUserManager
    98  	}
    99  	if len(args) == 0 {
   100  		return errors.New("no name specified")
   101  	}
   102  
   103  	c.Name, args = args[0], args[1:]
   104  	return cmd.CheckEmpty(args)
   105  }
   106  
   107  // Run implements Command.Run
   108  func (c *LoginCommand) Run(ctx *cmd.Context) error {
   109  	// TODO(thumper): as we support the user and address
   110  	// change this check here.
   111  	if c.Server.Path == "" {
   112  		return errors.New("no server file specified")
   113  	}
   114  
   115  	serverYAML, err := c.Server.Read(ctx)
   116  	if err != nil {
   117  		return errors.Trace(err)
   118  	}
   119  
   120  	var serverDetails envcmd.ServerFile
   121  	if err := goyaml.Unmarshal(serverYAML, &serverDetails); err != nil {
   122  		return errors.Trace(err)
   123  	}
   124  
   125  	// Construct the api.Info struct from the provided values
   126  	// and attempt to connect to the remote server before we do anything else.
   127  	if !names.IsValidUser(serverDetails.Username) {
   128  		return errors.Errorf("%q is not a valid username", serverDetails.Username)
   129  	}
   130  
   131  	userTag := names.NewUserTag(serverDetails.Username)
   132  	if userTag.Provider() != names.LocalProvider {
   133  		// Remove users do not have their passwords stored in Juju
   134  		// so we never attempt to change them.
   135  		c.KeepPassword = true
   136  	}
   137  
   138  	info := api.Info{
   139  		Addrs:    serverDetails.Addresses,
   140  		CACert:   serverDetails.CACert,
   141  		Tag:      userTag,
   142  		Password: serverDetails.Password,
   143  	}
   144  
   145  	apiState, err := c.apiOpen(&info, api.DefaultDialOpts())
   146  	if err != nil {
   147  		return errors.Trace(err)
   148  	}
   149  	defer apiState.Close()
   150  
   151  	// If we get to here, the credentials supplied were sufficient to connect
   152  	// to the Juju System and login. Now we cache the details.
   153  	serverInfo, err := c.cacheConnectionInfo(serverDetails, apiState)
   154  	if err != nil {
   155  		return errors.Trace(err)
   156  	}
   157  	ctx.Infof("cached connection details as system %q", c.Name)
   158  
   159  	// If we get to here, we have been able to connect to the API server, and
   160  	// also have been able to write the cached information. Now we can change
   161  	// the user's password to a new randomly generated strong password, and
   162  	// update the cached information knowing that the likelihood of failure is
   163  	// minimal.
   164  	if !c.KeepPassword {
   165  		if err := c.updatePassword(ctx, apiState, userTag, serverInfo); err != nil {
   166  			return errors.Trace(err)
   167  		}
   168  	}
   169  
   170  	return errors.Trace(envcmd.SetCurrentSystem(ctx, c.Name))
   171  }
   172  
   173  func (c *LoginCommand) cacheConnectionInfo(serverDetails envcmd.ServerFile, apiState api.Connection) (configstore.EnvironInfo, error) {
   174  	store, err := configstore.Default()
   175  	if err != nil {
   176  		return nil, errors.Trace(err)
   177  	}
   178  	serverInfo := store.CreateInfo(c.Name)
   179  
   180  	serverTag, err := apiState.ServerTag()
   181  	if err != nil {
   182  		return nil, errors.Wrap(err, errors.New("juju system too old to support login"))
   183  	}
   184  
   185  	connectedAddresses, err := network.ParseHostPorts(apiState.Addr())
   186  	if err != nil {
   187  		// Should never happen, since we've just connected with it.
   188  		return nil, errors.Annotatef(err, "invalid API address %q", apiState.Addr())
   189  	}
   190  	addressConnectedTo := connectedAddresses[0]
   191  
   192  	addrs, hosts, changed := juju.PrepareEndpointsForCaching(serverInfo, apiState.APIHostPorts(), addressConnectedTo)
   193  	if !changed {
   194  		logger.Infof("api addresses: %v", apiState.APIHostPorts())
   195  		logger.Infof("address connected to: %v", addressConnectedTo)
   196  		return nil, errors.New("no addresses returned from prepare for caching")
   197  	}
   198  
   199  	serverInfo.SetAPICredentials(
   200  		configstore.APICredentials{
   201  			User:     serverDetails.Username,
   202  			Password: serverDetails.Password,
   203  		})
   204  
   205  	serverInfo.SetAPIEndpoint(configstore.APIEndpoint{
   206  		Addresses:  addrs,
   207  		Hostnames:  hosts,
   208  		CACert:     serverDetails.CACert,
   209  		ServerUUID: serverTag.Id(),
   210  	})
   211  
   212  	if err = serverInfo.Write(); err != nil {
   213  		return nil, errors.Trace(err)
   214  	}
   215  	return serverInfo, nil
   216  }
   217  
   218  func (c *LoginCommand) updatePassword(ctx *cmd.Context, conn api.Connection, userTag names.UserTag, serverInfo configstore.EnvironInfo) error {
   219  	password, err := utils.RandomPassword()
   220  	if err != nil {
   221  		return errors.Annotate(err, "failed to generate random password")
   222  	}
   223  
   224  	userManager, err := c.getUserManager(conn)
   225  	if err != nil {
   226  		return errors.Trace(err)
   227  	}
   228  	if err := userManager.SetPassword(userTag.Name(), password); err != nil {
   229  		errors.Trace(err)
   230  	}
   231  	ctx.Infof("password updated\n")
   232  	creds := serverInfo.APICredentials()
   233  	creds.Password = password
   234  	serverInfo.SetAPICredentials(creds)
   235  	if err = serverInfo.Write(); err != nil {
   236  		return errors.Trace(err)
   237  	}
   238  	return nil
   239  }
   240  
   241  func apiOpen(info *api.Info, opts api.DialOpts) (api.Connection, error) {
   242  	return api.Open(info, opts)
   243  }
   244  
   245  // UserManager defines the calls that the Login command makes to the user
   246  // manager client. It is returned by a helper function that is overridden in
   247  // tests.
   248  type UserManager interface {
   249  	SetPassword(username, password string) error
   250  }
   251  
   252  func getUserManager(conn api.Connection) (UserManager, error) {
   253  	return usermanager.NewClient(conn), nil
   254  }