~nskaggs/+junk/xenial-test

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
// Copyright 2016 Canonical Ltd.
// Licensed under the AGPLv3, see LICENCE file for details.

package user

import (
	"fmt"

	"github.com/juju/cmd"
	"github.com/juju/errors"
	"gopkg.in/juju/names.v2"
	"gopkg.in/macaroon.v1"

	"github.com/juju/juju/api/usermanager"
	"github.com/juju/juju/cmd/modelcmd"
	"github.com/juju/juju/juju"
	"github.com/juju/juju/jujuclient"
)

const loginDoc = `
After login, a token ("macaroon") will become active. It has an expiration
time of 24 hours. Upon expiration, no further Juju commands can be issued
and the user will be prompted to log in again.

Examples:

    juju login bob

See also: enable-user
          disable-user
          logout

`

// NewLoginCommand returns a new cmd.Command to handle "juju login".
func NewLoginCommand() cmd.Command {
	return modelcmd.WrapController(&loginCommand{
		newLoginAPI: func(args juju.NewAPIConnectionParams) (LoginAPI, error) {
			api, err := juju.NewAPIConnection(args)
			if err != nil {
				return nil, errors.Trace(err)
			}
			return usermanager.NewClient(api), nil
		},
	})
}

// loginCommand changes the password for a user.
type loginCommand struct {
	modelcmd.ControllerCommandBase
	newLoginAPI func(juju.NewAPIConnectionParams) (LoginAPI, error)
	User        string
}

// Info implements Command.Info.
func (c *loginCommand) Info() *cmd.Info {
	return &cmd.Info{
		Name:    "login",
		Args:    "[username]",
		Purpose: "Logs a user in to a controller.",
		Doc:     loginDoc,
	}
}

// Init implements Command.Init.
func (c *loginCommand) Init(args []string) error {
	var err error
	c.User, err = cmd.ZeroOrOneArgs(args)
	if err != nil {
		return errors.Trace(err)
	}
	return nil
}

// LoginAPI provides the API methods that the login command uses.
type LoginAPI interface {
	CreateLocalLoginMacaroon(names.UserTag) (*macaroon.Macaroon, error)
	Close() error
}

// Run implements Command.Run.
func (c *loginCommand) Run(ctx *cmd.Context) error {
	controllerName := c.ControllerName()
	store := c.ClientStore()

	user := c.User
	if user == "" {
		// TODO(rog) Try macaroon login first before
		// falling back to prompting for username.
		// The username has not been specified, so prompt for it.
		fmt.Fprint(ctx.Stderr, "username: ")
		var err error
		user, err = readLine(ctx.Stdin)
		if err != nil {
			return errors.Trace(err)
		}
		if user == "" {
			return errors.Errorf("you must specify a username")
		}
	}
	if !names.IsValidUserName(user) {
		return errors.NotValidf("user name %q", user)
	}
	userTag := names.NewUserTag(user)

	// Make sure that the client is not already logged in,
	// or if it is, that it is logged in as the specified
	// user.
	accountDetails, err := store.AccountDetails(controllerName)
	if err != nil && !errors.IsNotFound(err) {
		return errors.Trace(err)
	}
	if accountDetails != nil && accountDetails.User != userTag.Canonical() {
		return errors.New(`already logged in

Run "juju logout" first before attempting to log in as a different user.
`)
	}
	// Read password from the terminal, and attempt to log in using that.
	fmt.Fprint(ctx.Stderr, "password: ")
	password, err := readPassword(ctx.Stdin)
	fmt.Fprintln(ctx.Stderr)
	if err != nil {
		return errors.Trace(err)
	}
	accountDetails = &jujuclient.AccountDetails{
		User:     userTag.Canonical(),
		Password: password,
	}
	params, err := c.NewAPIConnectionParams(store, controllerName, "", accountDetails)
	if err != nil {
		return errors.Trace(err)
	}
	api, err := c.newLoginAPI(params)
	if err != nil {
		return errors.Annotate(err, "creating API connection")
	}
	defer api.Close()

	// Create a new local login macaroon, and update the account details
	// in the client store, removing the recorded password (if any) and
	// storing the macaroon.
	macaroon, err := api.CreateLocalLoginMacaroon(userTag)
	if err != nil {
		return errors.Annotate(err, "failed to create a temporary credential")
	}
	macaroonJSON, err := macaroon.MarshalJSON()
	if err != nil {
		return errors.Annotate(err, "marshalling temporary credential to JSON")
	}
	accountDetails.Password = ""
	accountDetails.Macaroon = string(macaroonJSON)
	if err := store.UpdateAccount(controllerName, *accountDetails); err != nil {
		return errors.Annotate(err, "failed to record temporary credential")
	}
	ctx.Infof("You are now logged in to %q as %q.", controllerName, userTag.Canonical())
	return nil
}