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