Permalink
...
Checking mergeability…
Don’t worry, you can still create the pull request.
Comparing changes
Open a pull request
- 1 commit
- 44 files changed
- 0 commit comments
- 1 contributor
Unified
Split
Showing
with
1,326 additions
and 586 deletions.
- +14 −0 api/authentication/package_test.go
- +63 −0 api/authentication/visitor.go
- +88 −0 api/authentication/visitor_test.go
- +5 −0 api/interface.go
- +16 −1 api/state.go
- +0 −25 api/state_test.go
- +0 −21 api/usermanager/client.go
- +10 −2 apiserver/admin.go
- +202 −7 apiserver/apiserver.go
- +123 −19 apiserver/authcontext.go
- +136 −0 apiserver/authentication/interactions.go
- +164 −0 apiserver/authentication/interactions_test.go
- +103 −65 apiserver/authentication/user.go
- +27 −36 apiserver/authentication/user_test.go
- +1 −0 apiserver/controller/controller_test.go
- +4 −4 apiserver/export_test.go
- +3 −2 apiserver/httpcontext.go
- +0 −8 apiserver/params/registration.go
- +39 −24 apiserver/registration.go
- +12 −6 apiserver/root.go
- +36 −25 apiserver/tools_test.go
- +10 −56 apiserver/usermanager/usermanager.go
- +4 −15 apiserver/usermanager/usermanager_test.go
- +32 −8 cmd/juju/commands/migrate.go
- +85 −13 cmd/juju/commands/migrate_test.go
- +12 −6 cmd/juju/controller/register.go
- +0 −8 cmd/juju/controller/register_test.go
- +13 −28 cmd/juju/user/change_password.go
- +4 −46 cmd/juju/user/change_password_test.go
- +5 −24 cmd/juju/user/login.go
- +6 −20 cmd/juju/user/login_test.go
- +1 −1 cmd/juju/user/logout.go
- +4 −8 cmd/juju/user/logout_test.go
- +7 −13 cmd/juju/user/user_test.go
- +12 −8 cmd/modelcmd/apicontext.go
- +62 −31 cmd/modelcmd/base.go
- +0 −17 cmd/modelcmd/base_test.go
- +0 −5 cmd/modelcmd/modelcommand.go
- +1 −2 featuretests/cmd_juju_login_test.go
- +10 −1 featuretests/cmd_juju_register_test.go
- +9 −19 juju/api.go
- +0 −5 jujuclient/interface.go
- +0 −4 jujuclient/validation.go
- +3 −3 resource/cmd/list_charm_resources.go
View
14
api/authentication/package_test.go
| @@ -0,0 +1,14 @@ | ||
| +// Copyright 2016 Canonical Ltd. | ||
| +// Licensed under the AGPLv3, see LICENCE file for details. | ||
| + | ||
| +package authentication_test | ||
| + | ||
| +import ( | ||
| + "testing" | ||
| + | ||
| + gc "gopkg.in/check.v1" | ||
| +) | ||
| + | ||
| +func TestPackage(t *testing.T) { | ||
| + gc.TestingT(t) | ||
| +} |
View
63
api/authentication/visitor.go
| @@ -0,0 +1,63 @@ | ||
| +// Copyright 2016 Canonical Ltd. | ||
| +// Licensed under the AGPLv3, see LICENCE file for details. | ||
| + | ||
| +package authentication | ||
| + | ||
| +import ( | ||
| + "encoding/json" | ||
| + "net/http" | ||
| + "net/url" | ||
| + | ||
| + "github.com/juju/errors" | ||
| + | ||
| + "gopkg.in/macaroon-bakery.v1/httpbakery" | ||
| +) | ||
| + | ||
| +const authMethod = "juju_userpass" | ||
| + | ||
| +// Visitor is a httpbakery.Visitor that will login directly | ||
| +// to the Juju controller using password authentication. This | ||
| +// only applies when logging in as a local user. | ||
| +type Visitor struct { | ||
| + username string | ||
| + getPassword func(string) (string, error) | ||
| +} | ||
| + | ||
| +// NewVisitor returns a new Visitor. | ||
| +func NewVisitor(username string, getPassword func(string) (string, error)) *Visitor { | ||
| + return &Visitor{ | ||
| + username: username, | ||
| + getPassword: getPassword, | ||
| + } | ||
| +} | ||
| + | ||
| +func (v *Visitor) VisitWebPage(client *httpbakery.Client, methodURLs map[string]*url.URL) error { | ||
| + methodURL := methodURLs[authMethod] | ||
| + if methodURL == nil { | ||
| + return httpbakery.ErrMethodNotSupported | ||
| + } | ||
| + | ||
| + password, err := v.getPassword(v.username) | ||
| + if err != nil { | ||
| + return err | ||
| + } | ||
| + | ||
| + // POST to the URL with username and password. | ||
| + resp, err := client.PostForm(methodURL.String(), url.Values{ | ||
| + "user": {v.username}, | ||
| + "password": {password}, | ||
| + }) | ||
| + if err != nil { | ||
| + return err | ||
| + } | ||
| + defer resp.Body.Close() | ||
| + | ||
| + if resp.StatusCode == http.StatusOK { | ||
| + return nil | ||
| + } | ||
| + var jsonError httpbakery.Error | ||
| + if err := json.NewDecoder(resp.Body).Decode(&jsonError); err != nil { | ||
| + return errors.Annotate(err, "unmarshalling error") | ||
| + } | ||
| + return &jsonError | ||
| +} |
View
88
api/authentication/visitor_test.go
| @@ -0,0 +1,88 @@ | ||
| +// Copyright 2016 Canonical Ltd. | ||
| +// Licensed under the AGPLv3, see LICENCE file for details. | ||
| + | ||
| +package authentication_test | ||
| + | ||
| +import ( | ||
| + "net/http" | ||
| + "net/http/cookiejar" | ||
| + "net/http/httptest" | ||
| + "net/url" | ||
| + | ||
| + "github.com/juju/juju/api/authentication" | ||
| + "github.com/juju/testing" | ||
| + jc "github.com/juju/testing/checkers" | ||
| + gc "gopkg.in/check.v1" | ||
| + "gopkg.in/macaroon-bakery.v1/httpbakery" | ||
| +) | ||
| + | ||
| +type VisitorSuite struct { | ||
| + testing.IsolationSuite | ||
| + | ||
| + jar *cookiejar.Jar | ||
| + client *httpbakery.Client | ||
| + server *httptest.Server | ||
| + handler http.Handler | ||
| +} | ||
| + | ||
| +var _ = gc.Suite(&VisitorSuite{}) | ||
| + | ||
| +func (s *VisitorSuite) SetUpTest(c *gc.C) { | ||
| + s.IsolationSuite.SetUpTest(c) | ||
| + var err error | ||
| + s.jar, err = cookiejar.New(nil) | ||
| + c.Assert(err, jc.ErrorIsNil) | ||
| + s.client = httpbakery.NewClient() | ||
| + s.client.Jar = s.jar | ||
| + s.handler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {}) | ||
| + s.server = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { | ||
| + s.handler.ServeHTTP(w, r) | ||
| + })) | ||
| + s.AddCleanup(func(c *gc.C) { s.server.Close() }) | ||
| +} | ||
| + | ||
| +func (s *VisitorSuite) TestVisitWebPage(c *gc.C) { | ||
| + v := authentication.NewVisitor("bob", func(username string) (string, error) { | ||
| + c.Assert(username, gc.Equals, "bob") | ||
| + return "hunter2", nil | ||
| + }) | ||
| + var formUser, formPassword string | ||
| + s.handler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { | ||
| + r.ParseForm() | ||
| + formUser = r.Form.Get("user") | ||
| + formPassword = r.Form.Get("password") | ||
| + }) | ||
| + err := v.VisitWebPage(s.client, map[string]*url.URL{ | ||
| + "juju_userpass": mustParseURL(s.server.URL), | ||
| + }) | ||
| + c.Assert(err, jc.ErrorIsNil) | ||
| + c.Assert(formUser, gc.Equals, "bob") | ||
| + c.Assert(formPassword, gc.Equals, "hunter2") | ||
| +} | ||
| + | ||
| +func (s *VisitorSuite) TestVisitWebPageMethodNotSupported(c *gc.C) { | ||
| + v := authentication.NewVisitor("bob", nil) | ||
| + err := v.VisitWebPage(s.client, map[string]*url.URL{}) | ||
| + c.Assert(err, gc.Equals, httpbakery.ErrMethodNotSupported) | ||
| +} | ||
| + | ||
| +func (s *VisitorSuite) TestVisitWebPageErrorResult(c *gc.C) { | ||
| + v := authentication.NewVisitor("bob", func(username string) (string, error) { | ||
| + return "hunter2", nil | ||
| + }) | ||
| + s.handler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { | ||
| + http.Error(w, `{"Message":"bleh"}`, http.StatusInternalServerError) | ||
| + }) | ||
| + err := v.VisitWebPage(s.client, map[string]*url.URL{ | ||
| + "juju_userpass": mustParseURL(s.server.URL), | ||
| + }) | ||
| + c.Assert(err, gc.ErrorMatches, "bleh") | ||
| +} | ||
| + | ||
| +func mustParseURL(s string) *url.URL { | ||
| + u, err := url.Parse(s) | ||
| + if err != nil { | ||
| + panic(err) | ||
| + } | ||
| + return u | ||
| +} |
View
5
api/interface.go
| @@ -4,6 +4,7 @@ | ||
| package api | ||
| import ( | ||
| + "net/url" | ||
| "time" | ||
| "github.com/juju/errors" | ||
| @@ -197,6 +198,10 @@ type Connection interface { | ||
| // ControllerAccess returns the access level of authorized user to the controller. | ||
| ControllerAccess() string | ||
| + // CookieURL returns the URL that HTTP cookies for the API will be | ||
| + // associated with. | ||
| + CookieURL() *url.URL | ||
| + | ||
| // These methods expose a bunch of worker-specific facades, and basically | ||
| // just should not exist; but removing them is too noisy for a single CL. | ||
| // Client in particular is intimately coupled with State -- and the others | ||
View
17
api/state.go
| @@ -5,6 +5,7 @@ package api | ||
| import ( | ||
| "net" | ||
| + "net/url" | ||
| "strconv" | ||
| "github.com/juju/errors" | ||
| @@ -40,7 +41,7 @@ func (st *state) Login(tag names.Tag, password, nonce string, macaroons []macaro | ||
| Nonce: nonce, | ||
| Macaroons: macaroons, | ||
| } | ||
| - if tag == nil { | ||
| + if password == "" { | ||
| // Add any macaroons from the cookie jar that might work for | ||
| // authenticating the login request. | ||
| request.Macaroons = append(request.Macaroons, | ||
| @@ -81,6 +82,13 @@ func (st *state) Login(tag names.Tag, password, nonce string, macaroons []macaro | ||
| MacaroonPath: "/", | ||
| }, | ||
| }); err != nil { | ||
| + cause := errors.Cause(err) | ||
| + if httpbakery.IsInteractionError(cause) { | ||
| + // Just inform the reason of the reason for the | ||
| + // failure, e.g. because the username/password | ||
| + // they presented was invalid. | ||
| + err = cause.(*httpbakery.InteractionError).Reason | ||
| + } | ||
| return errors.Trace(err) | ||
| } | ||
| // Add the macaroons that have been saved by HandleError to our login request. | ||
| @@ -188,6 +196,13 @@ func (st *state) ControllerAccess() string { | ||
| return st.controllerAccess | ||
| } | ||
| +// CookieURL returns the URL that HTTP cookies for the API will be | ||
| +// associated with. | ||
| +func (st *state) CookieURL() *url.URL { | ||
| + copy := *st.cookieURL | ||
| + return © | ||
| +} | ||
| + | ||
| // slideAddressToFront moves the address at the location (serverIndex, addrIndex) to be | ||
| // the first address of the first server. | ||
| func slideAddressToFront(servers [][]network.HostPort, serverIndex, addrIndex int) { | ||
View
25
api/state_test.go
| @@ -98,19 +98,6 @@ func (s *stateSuite) TestTags(c *gc.C) { | ||
| c.Check(controllerTag, gc.Equals, coretesting.ControllerTag) | ||
| } | ||
| -func (s *stateSuite) TestLoginMacaroon(c *gc.C) { | ||
| - apistate, tag, _ := s.OpenAPIWithoutLogin(c) | ||
| - defer apistate.Close() | ||
| - // Use a different API connection, because we can't get at UserManager without logging in. | ||
| - loggedInAPI := s.OpenControllerAPI(c) | ||
| - defer loggedInAPI.Close() | ||
| - mac, err := usermanager.NewClient(loggedInAPI).CreateLocalLoginMacaroon(tag.(names.UserTag)) | ||
| - c.Assert(err, jc.ErrorIsNil) | ||
| - err = apistate.Login(tag, "", "", []macaroon.Slice{{mac}}) | ||
| - c.Assert(err, jc.ErrorIsNil) | ||
| - c.Assert(apistate.AuthTag(), gc.Equals, tag) | ||
| -} | ||
| - | ||
| func (s *stateSuite) TestLoginSetsModelAccess(c *gc.C) { | ||
| // The default user has admin access. | ||
| c.Assert(s.APIState.ModelAccess(), gc.Equals, "admin") | ||
| @@ -156,18 +143,6 @@ func (s *stateSuite) TestLoginMacaroonInvalidId(c *gc.C) { | ||
| c.Assert(err, gc.ErrorMatches, "invalid entity name or password \\(unauthorized access\\)") | ||
| } | ||
| -func (s *stateSuite) TestLoginMacaroonInvalidUser(c *gc.C) { | ||
| - apistate, tag, _ := s.OpenAPIWithoutLogin(c) | ||
| - defer apistate.Close() | ||
| - // Use a different API connection, because we can't get at UserManager without logging in. | ||
| - loggedInAPI := s.OpenControllerAPI(c) | ||
| - defer loggedInAPI.Close() | ||
| - mac, err := usermanager.NewClient(loggedInAPI).CreateLocalLoginMacaroon(tag.(names.UserTag)) | ||
| - c.Assert(err, jc.ErrorIsNil) | ||
| - err = apistate.Login(names.NewUserTag("bob@local"), "", "", []macaroon.Slice{{mac}}) | ||
| - c.Assert(err, gc.ErrorMatches, "invalid entity name or password \\(unauthorized access\\)") | ||
| -} | ||
| - | ||
| func (s *stateSuite) TestLoginTracksFacadeVersions(c *gc.C) { | ||
| apistate, tag, password := s.OpenAPIWithoutLogin(c) | ||
| defer apistate.Close() | ||
View
21
api/usermanager/client.go
| @@ -7,8 +7,6 @@ import ( | ||
| "fmt" | ||
| "strings" | ||
| - "gopkg.in/macaroon.v1" | ||
| - | ||
| "github.com/juju/errors" | ||
| "github.com/juju/loggo" | ||
| "gopkg.in/juju/names.v2" | ||
| @@ -182,22 +180,3 @@ func (c *Client) SetPassword(username, password string) error { | ||
| } | ||
| return results.OneError() | ||
| } | ||
| - | ||
| -// CreateLocalLoginMacaroon creates a local login macaroon for the | ||
| -// authenticated user. | ||
| -func (c *Client) CreateLocalLoginMacaroon(tag names.UserTag) (*macaroon.Macaroon, error) { | ||
| - args := params.Entities{Entities: []params.Entity{{tag.String()}}} | ||
| - var results params.MacaroonResults | ||
| - if err := c.facade.FacadeCall("CreateLocalLoginMacaroon", args, &results); err != nil { | ||
| - return nil, errors.Trace(err) | ||
| - } | ||
| - if n := len(results.Results); n != 1 { | ||
| - logger.Errorf("expected 1 result, got %#v", results) | ||
| - return nil, errors.Errorf("expected 1 result, got %d", n) | ||
| - } | ||
| - result := results.Results[0] | ||
| - if result.Error != nil { | ||
| - return nil, errors.Trace(result.Error) | ||
| - } | ||
| - return result.Result, nil | ||
| -} | ||
View
12
apiserver/admin.go
| @@ -92,7 +92,7 @@ func (a *admin) login(req params.LoginRequest, loginVersion int) (params.LoginRe | ||
| controllerOnlyLogin := a.root.modelUUID == "" | ||
| controllerMachineLogin := false | ||
| - entity, lastConnection, err := doCheckCreds(a.root.state, req, isUser, a.srv.authCtxt) | ||
| + entity, lastConnection, err := a.checkCreds(req, isUser) | ||
| if err != nil { | ||
| if err, ok := errors.Cause(err).(*common.DischargeRequiredError); ok { | ||
| loginResult := params.LoginResult{ | ||
| @@ -241,8 +241,16 @@ func filterFacades(allowFacade func(name string) bool) []params.FacadeVersions { | ||
| return out | ||
| } | ||
| +func (a *admin) checkCreds(req params.LoginRequest, lookForModelUser bool) (state.Entity, *time.Time, error) { | ||
| + return doCheckCreds(a.root.state, req, lookForModelUser, a.authenticator()) | ||
| +} | ||
| + | ||
| func (a *admin) checkControllerMachineCreds(req params.LoginRequest) (state.Entity, error) { | ||
| - return checkControllerMachineCreds(a.srv.state, req, a.srv.authCtxt) | ||
| + return checkControllerMachineCreds(a.srv.state, req, a.authenticator()) | ||
| +} | ||
| + | ||
| +func (a *admin) authenticator() authentication.EntityAuthenticator { | ||
| + return a.srv.authCtxt.authenticator(a.root.serverHost) | ||
| } | ||
| func (a *admin) maintenanceInProgress() bool { | ||
Oops, something went wrong.