github.com/axw/juju@v0.0.0-20161005053422-4bd6544d08d4/cmd/juju/controller/register.go (about)

     1  // Copyright 2016 Canonical Ltd.
     2  // Licensed under the AGPLv3, see LICENCE file for details.
     3  
     4  package controller
     5  
     6  import (
     7  	"bufio"
     8  	"bytes"
     9  	"crypto/rand"
    10  	"encoding/asn1"
    11  	"encoding/base64"
    12  	"encoding/json"
    13  	"fmt"
    14  	"io"
    15  	"net/http"
    16  	"os"
    17  	"strings"
    18  
    19  	"github.com/juju/cmd"
    20  	"github.com/juju/errors"
    21  	"github.com/juju/utils"
    22  	"github.com/juju/utils/set"
    23  	"golang.org/x/crypto/nacl/secretbox"
    24  	"golang.org/x/crypto/ssh/terminal"
    25  	"gopkg.in/juju/names.v2"
    26  
    27  	"github.com/juju/juju/api"
    28  	"github.com/juju/juju/api/base"
    29  	"github.com/juju/juju/api/modelmanager"
    30  	"github.com/juju/juju/apiserver/params"
    31  	"github.com/juju/juju/cmd/juju/common"
    32  	"github.com/juju/juju/cmd/modelcmd"
    33  	"github.com/juju/juju/jujuclient"
    34  	"github.com/juju/juju/permission"
    35  )
    36  
    37  var errNoModels = errors.New(`
    38  There are no models available. You can add models with
    39  "juju add-model", or you can ask an administrator or owner
    40  of a model to grant access to that model with "juju grant".`[1:])
    41  
    42  // NewRegisterCommand returns a command to allow the user to register a controller.
    43  func NewRegisterCommand() cmd.Command {
    44  	cmd := &registerCommand{}
    45  	cmd.apiOpen = cmd.APIOpen
    46  	cmd.listModelsFunc = cmd.listModels
    47  	cmd.store = jujuclient.NewFileClientStore()
    48  	return modelcmd.WrapBase(cmd)
    49  }
    50  
    51  // registerCommand logs in to a Juju controller and caches the connection
    52  // information.
    53  type registerCommand struct {
    54  	modelcmd.JujuCommandBase
    55  	apiOpen        api.OpenFunc
    56  	listModelsFunc func(_ jujuclient.ClientStore, controller, user string) ([]base.UserModel, error)
    57  	store          jujuclient.ClientStore
    58  	EncodedData    string
    59  }
    60  
    61  var usageRegisterSummary = `
    62  Registers a Juju user to a controller.`[1:]
    63  
    64  var usageRegisterDetails = `
    65  Connects to a controller and completes the user registration process that began
    66  with the `[1:] + "`juju add-user`" + ` command. The latter prints out the 'string' that is
    67  referred to in Usage.
    68  
    69  The user will be prompted for a password, which, once set, causes the
    70  registration string to be voided. In order to start using Juju the user can now
    71  either add a model or wait for a model to be shared with them.  Some machine
    72  providers will require the user to be in possession of certain credentials in
    73  order to add a model.
    74  
    75  Examples:
    76  
    77      juju register MFATA3JvZDAnExMxMDQuMTU0LjQyLjQ0OjE3MDcwExAxMC4xMjguMC4yOjE3MDcwBCBEFCaXerhNImkKKabuX5ULWf2Bp4AzPNJEbXVWgraLrAA=
    78  
    79  See also: 
    80      add-user
    81      change-user-password
    82      unregister`
    83  
    84  // Info implements Command.Info
    85  // `register` may seem generic, but is seen as simple and without potential
    86  // naming collisions in any current or planned features.
    87  func (c *registerCommand) Info() *cmd.Info {
    88  	return &cmd.Info{
    89  		Name:    "register",
    90  		Args:    "<string>",
    91  		Purpose: usageRegisterSummary,
    92  		Doc:     usageRegisterDetails,
    93  	}
    94  }
    95  
    96  // SetFlags implements Command.Init.
    97  func (c *registerCommand) Init(args []string) error {
    98  	if len(args) < 1 {
    99  		return errors.New("registration data missing")
   100  	}
   101  	c.EncodedData, args = args[0], args[1:]
   102  	if err := cmd.CheckEmpty(args); err != nil {
   103  		return err
   104  	}
   105  	return nil
   106  }
   107  
   108  func (c *registerCommand) Run(ctx *cmd.Context) error {
   109  
   110  	store := modelcmd.QualifyingClientStore{c.store}
   111  	registrationParams, err := c.getParameters(ctx, store)
   112  	if err != nil {
   113  		return errors.Trace(err)
   114  	}
   115  	_, err = store.ControllerByName(registrationParams.controllerName)
   116  	if err == nil {
   117  		return errors.AlreadyExistsf("controller %q", registrationParams.controllerName)
   118  	} else if !errors.IsNotFound(err) {
   119  		return errors.Trace(err)
   120  	}
   121  
   122  	// During registration we must set a new password. This has to be done
   123  	// atomically with the clearing of the secret key.
   124  	payloadBytes, err := json.Marshal(params.SecretKeyLoginRequestPayload{
   125  		registrationParams.newPassword,
   126  	})
   127  	if err != nil {
   128  		return errors.Trace(err)
   129  	}
   130  
   131  	// Make the registration call. If this is successful, the client's
   132  	// cookie jar will be populated with a macaroon that may be used
   133  	// to log in below without the user having to type in the password
   134  	// again.
   135  	req := params.SecretKeyLoginRequest{
   136  		Nonce: registrationParams.nonce[:],
   137  		User:  registrationParams.userTag.String(),
   138  		PayloadCiphertext: secretbox.Seal(
   139  			nil, payloadBytes,
   140  			&registrationParams.nonce,
   141  			&registrationParams.key,
   142  		),
   143  	}
   144  	resp, err := c.secretKeyLogin(registrationParams.controllerAddrs, req)
   145  	if err != nil {
   146  		return errors.Trace(err)
   147  	}
   148  
   149  	// Decrypt the response to authenticate the controller and
   150  	// obtain its CA certificate.
   151  	if len(resp.Nonce) != len(registrationParams.nonce) {
   152  		return errors.NotValidf("response nonce")
   153  	}
   154  	var respNonce [24]byte
   155  	copy(respNonce[:], resp.Nonce)
   156  	payloadBytes, ok := secretbox.Open(nil, resp.PayloadCiphertext, &respNonce, &registrationParams.key)
   157  	if !ok {
   158  		return errors.NotValidf("response payload")
   159  	}
   160  	var responsePayload params.SecretKeyLoginResponsePayload
   161  	if err := json.Unmarshal(payloadBytes, &responsePayload); err != nil {
   162  		return errors.Annotate(err, "unmarshalling response payload")
   163  	}
   164  
   165  	// Store the controller and account details.
   166  	controllerDetails := jujuclient.ControllerDetails{
   167  		APIEndpoints:   registrationParams.controllerAddrs,
   168  		ControllerUUID: responsePayload.ControllerUUID,
   169  		CACert:         responsePayload.CACert,
   170  	}
   171  	if err := store.AddController(registrationParams.controllerName, controllerDetails); err != nil {
   172  		return errors.Trace(err)
   173  	}
   174  	accountDetails := jujuclient.AccountDetails{
   175  		User:            registrationParams.userTag.Canonical(),
   176  		LastKnownAccess: string(permission.LoginAccess),
   177  	}
   178  	if err := store.UpdateAccount(registrationParams.controllerName, accountDetails); err != nil {
   179  		return errors.Trace(err)
   180  	}
   181  
   182  	// Log into the controller to verify the credentials, and
   183  	// list the models available.
   184  	models, err := c.listModelsFunc(store, registrationParams.controllerName, accountDetails.User)
   185  	if err != nil {
   186  		return errors.Trace(err)
   187  	}
   188  	for _, model := range models {
   189  		owner := names.NewUserTag(model.Owner)
   190  		if err := store.UpdateModel(
   191  			registrationParams.controllerName,
   192  			jujuclient.JoinOwnerModelName(owner, model.Name),
   193  			jujuclient.ModelDetails{model.UUID},
   194  		); err != nil {
   195  			return errors.Annotate(err, "storing model details")
   196  		}
   197  	}
   198  	if err := store.SetCurrentController(registrationParams.controllerName); err != nil {
   199  		return errors.Trace(err)
   200  	}
   201  
   202  	fmt.Fprintf(
   203  		ctx.Stderr, "\nWelcome, %s. You are now logged into %q.\n",
   204  		registrationParams.userTag.Id(), registrationParams.controllerName,
   205  	)
   206  	return c.maybeSetCurrentModel(ctx, store, registrationParams.controllerName, accountDetails.User, models)
   207  }
   208  
   209  func (c *registerCommand) listModels(store jujuclient.ClientStore, controllerName, userName string) ([]base.UserModel, error) {
   210  	api, err := c.NewAPIRoot(store, controllerName, "")
   211  	if err != nil {
   212  		return nil, errors.Trace(err)
   213  	}
   214  	defer api.Close()
   215  	mm := modelmanager.NewClient(api)
   216  	return mm.ListModels(userName)
   217  }
   218  
   219  func (c *registerCommand) maybeSetCurrentModel(ctx *cmd.Context, store jujuclient.ClientStore, controllerName, userName string, models []base.UserModel) error {
   220  	if len(models) == 0 {
   221  		fmt.Fprintf(ctx.Stderr, "\n%s\n\n", errNoModels.Error())
   222  		return nil
   223  	}
   224  
   225  	// If we get to here, there is at least one model.
   226  	if len(models) == 1 {
   227  		// There is exactly one model shared,
   228  		// so set it as the current model.
   229  		model := models[0]
   230  		owner := names.NewUserTag(model.Owner)
   231  		modelName := jujuclient.JoinOwnerModelName(owner, model.Name)
   232  		err := store.SetCurrentModel(controllerName, modelName)
   233  		if err != nil {
   234  			return errors.Trace(err)
   235  		}
   236  		fmt.Fprintf(ctx.Stderr, "\nCurrent model set to %q.\n\n", modelName)
   237  	} else {
   238  		fmt.Fprintf(ctx.Stderr, `
   239  There are %d models available. Use "juju switch" to select
   240  one of them:
   241  `, len(models))
   242  		user := names.NewUserTag(userName)
   243  		ownerModelNames := make(set.Strings)
   244  		otherModelNames := make(set.Strings)
   245  		for _, model := range models {
   246  			if model.Owner == userName {
   247  				ownerModelNames.Add(model.Name)
   248  				continue
   249  			}
   250  			owner := names.NewUserTag(model.Owner)
   251  			modelName := common.OwnerQualifiedModelName(model.Name, owner, user)
   252  			otherModelNames.Add(modelName)
   253  		}
   254  		for _, modelName := range ownerModelNames.SortedValues() {
   255  			fmt.Fprintf(ctx.Stderr, "  - juju switch %s\n", modelName)
   256  		}
   257  		for _, modelName := range otherModelNames.SortedValues() {
   258  			fmt.Fprintf(ctx.Stderr, "  - juju switch %s\n", modelName)
   259  		}
   260  		fmt.Fprintln(ctx.Stderr)
   261  	}
   262  	return nil
   263  }
   264  
   265  type registrationParams struct {
   266  	userTag         names.UserTag
   267  	controllerName  string
   268  	controllerAddrs []string
   269  	key             [32]byte
   270  	nonce           [24]byte
   271  	newPassword     string
   272  }
   273  
   274  // getParameters gets all of the parameters required for registering, prompting
   275  // the user as necessary.
   276  func (c *registerCommand) getParameters(ctx *cmd.Context, store jujuclient.ClientStore) (*registrationParams, error) {
   277  
   278  	// Decode key, username, controller addresses from the string supplied
   279  	// on the command line.
   280  	decodedData, err := base64.URLEncoding.DecodeString(c.EncodedData)
   281  	if err != nil {
   282  		return nil, errors.Trace(err)
   283  	}
   284  	var info jujuclient.RegistrationInfo
   285  	if _, err := asn1.Unmarshal(decodedData, &info); err != nil {
   286  		return nil, errors.Trace(err)
   287  	}
   288  
   289  	params := registrationParams{
   290  		controllerAddrs: info.Addrs,
   291  		userTag:         names.NewUserTag(info.User),
   292  	}
   293  	if len(info.SecretKey) != len(params.key) {
   294  		return nil, errors.NotValidf("secret key")
   295  	}
   296  	copy(params.key[:], info.SecretKey)
   297  
   298  	// Prompt the user for the controller name.
   299  	controllerName, err := c.promptControllerName(store, info.ControllerName, ctx.Stderr, ctx.Stdin)
   300  	if err != nil {
   301  		return nil, errors.Trace(err)
   302  	}
   303  	params.controllerName = controllerName
   304  
   305  	// Prompt the user for the new password to set.
   306  	newPassword, err := c.promptNewPassword(ctx.Stderr, ctx.Stdin)
   307  	if err != nil {
   308  		return nil, errors.Trace(err)
   309  	}
   310  	params.newPassword = newPassword
   311  
   312  	// Generate a random nonce for encrypting the request.
   313  	if _, err := rand.Read(params.nonce[:]); err != nil {
   314  		return nil, errors.Trace(err)
   315  	}
   316  
   317  	return &params, nil
   318  }
   319  
   320  func (c *registerCommand) secretKeyLogin(addrs []string, request params.SecretKeyLoginRequest) (*params.SecretKeyLoginResponse, error) {
   321  	apiContext, err := c.APIContext()
   322  	if err != nil {
   323  		return nil, errors.Annotate(err, "getting API context")
   324  	}
   325  
   326  	buf, err := json.Marshal(&request)
   327  	if err != nil {
   328  		return nil, errors.Annotate(err, "marshalling request")
   329  	}
   330  	r := bytes.NewReader(buf)
   331  
   332  	// Determine which address to use by attempting to open an API
   333  	// connection with each of the addresses. Note that we do not
   334  	// know the CA certificate yet, so we do not want to send any
   335  	// sensitive information. We make no attempt to log in until
   336  	// we can verify the server's identity.
   337  	opts := api.DefaultDialOpts()
   338  	opts.InsecureSkipVerify = true
   339  	conn, err := c.apiOpen(&api.Info{
   340  		Addrs:     addrs,
   341  		SkipLogin: true,
   342  		// NOTE(axw) CACert is required, but ignored if
   343  		// InsecureSkipVerify is set. We should try to
   344  		// bring together CACert and InsecureSkipVerify
   345  		// so they can be validated together.
   346  		CACert: "ignored",
   347  	}, opts)
   348  	if err != nil {
   349  		return nil, errors.Trace(err)
   350  	}
   351  	apiAddr := conn.Addr()
   352  	if err := conn.Close(); err != nil {
   353  		return nil, errors.Trace(err)
   354  	}
   355  
   356  	// Using the address we connected to above, perform the request.
   357  	// A success response will include a macaroon cookie that we can
   358  	// use to log in with.
   359  	urlString := fmt.Sprintf("https://%s/register", apiAddr)
   360  	httpReq, err := http.NewRequest("POST", urlString, r)
   361  	if err != nil {
   362  		return nil, errors.Trace(err)
   363  	}
   364  	httpReq.Header.Set("Content-Type", "application/json")
   365  	httpClient := utils.GetNonValidatingHTTPClient()
   366  	httpClient.Jar = apiContext.Jar
   367  	httpResp, err := httpClient.Do(httpReq)
   368  	if err != nil {
   369  		return nil, errors.Trace(err)
   370  	}
   371  	defer httpResp.Body.Close()
   372  
   373  	if httpResp.StatusCode != http.StatusOK {
   374  		var resp params.ErrorResult
   375  		if err := json.NewDecoder(httpResp.Body).Decode(&resp); err != nil {
   376  			return nil, errors.Trace(err)
   377  		}
   378  		return nil, resp.Error
   379  	}
   380  
   381  	var resp params.SecretKeyLoginResponse
   382  	if err := json.NewDecoder(httpResp.Body).Decode(&resp); err != nil {
   383  		return nil, errors.Trace(err)
   384  	}
   385  	return &resp, nil
   386  }
   387  
   388  func (c *registerCommand) promptNewPassword(stderr io.Writer, stdin io.Reader) (string, error) {
   389  	password, err := c.readPassword("Enter a new password: ", stderr, stdin)
   390  	if err != nil {
   391  		return "", errors.Trace(err)
   392  	}
   393  	if password == "" {
   394  		return "", errors.NewNotValid(nil, "you must specify a non-empty password")
   395  	}
   396  	passwordConfirmation, err := c.readPassword("Confirm password: ", stderr, stdin)
   397  	if err != nil {
   398  		return "", errors.Trace(err)
   399  	}
   400  	if password != passwordConfirmation {
   401  		return "", errors.Errorf("passwords do not match")
   402  	}
   403  	return password, nil
   404  }
   405  
   406  const errControllerConflicts = `WARNING: You already have a controller registered with the name %q. Please choose a different name for the new controller.
   407  
   408  `
   409  
   410  func (c *registerCommand) promptControllerName(store jujuclient.ClientStore, suggestedName string, stderr io.Writer, stdin io.Reader) (string, error) {
   411  	_, err := store.ControllerByName(suggestedName)
   412  	if err == nil {
   413  		fmt.Fprintf(stderr, errControllerConflicts, suggestedName)
   414  		suggestedName = ""
   415  	}
   416  	var setMsg string
   417  	setMsg = "Enter a name for this controller: "
   418  	if suggestedName != "" {
   419  		setMsg = fmt.Sprintf("Enter a name for this controller [%s]: ",
   420  			suggestedName)
   421  	}
   422  	fmt.Fprintf(stderr, setMsg)
   423  	defer stderr.Write([]byte{'\n'})
   424  	name, err := c.readLine(stdin)
   425  	if err != nil {
   426  		return "", errors.Trace(err)
   427  	}
   428  	name = strings.TrimSpace(name)
   429  	if name == "" && suggestedName == "" {
   430  		return "", errors.NewNotValid(nil, "you must specify a non-empty controller name")
   431  	}
   432  	if name == "" && suggestedName != "" {
   433  		return suggestedName, nil
   434  	}
   435  	return name, nil
   436  }
   437  
   438  func (c *registerCommand) readPassword(prompt string, stderr io.Writer, stdin io.Reader) (string, error) {
   439  	fmt.Fprintf(stderr, "%s", prompt)
   440  	defer stderr.Write([]byte{'\n'})
   441  	if f, ok := stdin.(*os.File); ok && terminal.IsTerminal(int(f.Fd())) {
   442  		password, err := terminal.ReadPassword(int(f.Fd()))
   443  		if err != nil {
   444  			return "", errors.Trace(err)
   445  		}
   446  		return string(password), nil
   447  	}
   448  	return c.readLine(stdin)
   449  }
   450  
   451  func (c *registerCommand) readLine(stdin io.Reader) (string, error) {
   452  	// Read one byte at a time to avoid reading beyond the delimiter.
   453  	line, err := bufio.NewReader(byteAtATimeReader{stdin}).ReadString('\n')
   454  	if err != nil {
   455  		return "", errors.Trace(err)
   456  	}
   457  	return line[:len(line)-1], nil
   458  }
   459  
   460  type byteAtATimeReader struct {
   461  	io.Reader
   462  }
   463  
   464  func (r byteAtATimeReader) Read(out []byte) (int, error) {
   465  	return r.Reader.Read(out[:1])
   466  }