-
Notifications
You must be signed in to change notification settings - Fork 7
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add login to websocket API #143
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -529,6 +529,32 @@ func (s *jemSuite) TestAddModel(c *gc.C) { | |
c.Assert(errgo.Cause(err), gc.Equals, params.ErrAlreadyExists) | ||
} | ||
|
||
func (s *jemSuite) TestModelFromUUID(c *gc.C) { | ||
uuid := "99999999-9999-9999-9999-999999999999" | ||
path := params.EntityPath{"bob", "x"} | ||
m := &mongodoc.Model{ | ||
Id: "ignored", | ||
Path: path, | ||
UUID: uuid, | ||
} | ||
err := s.store.AddModel(m) | ||
c.Assert(err, gc.IsNil) | ||
c.Assert(m, jc.DeepEquals, &mongodoc.Model{ | ||
Id: "bob/x", | ||
Path: path, | ||
UUID: uuid, | ||
}) | ||
|
||
m1, err := s.store.ModelFromUUID(uuid) | ||
c.Assert(err, gc.IsNil) | ||
c.Assert(m1, jc.DeepEquals, m) | ||
|
||
m2, err := s.store.ModelFromUUID("no-such-uuid") | ||
c.Assert(err, gc.ErrorMatches, `model "no-such-uuid" not found`) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Please also check the error cause (not found). There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. done |
||
c.Assert(errgo.Cause(err), gc.Equals, params.ErrNotFound) | ||
c.Assert(m2, gc.IsNil) | ||
} | ||
|
||
func (s *jemSuite) TestAddTemplate(c *gc.C) { | ||
path := params.EntityPath{"bob", "x"} | ||
tmpl := &mongodoc.Template{ | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -3,17 +3,51 @@ | |
package jujuapi | ||
|
||
import ( | ||
"reflect" | ||
|
||
"github.com/juju/juju/apiserver/common" | ||
"github.com/juju/juju/apiserver/observer" | ||
jujuparams "github.com/juju/juju/apiserver/params" | ||
"github.com/juju/juju/network" | ||
"github.com/juju/juju/rpc" | ||
"github.com/juju/juju/rpc/jsoncodec" | ||
"github.com/juju/juju/rpc/rpcreflect" | ||
"github.com/juju/loggo" | ||
"golang.org/x/net/websocket" | ||
"gopkg.in/errgo.v1" | ||
"gopkg.in/juju/names.v2" | ||
"gopkg.in/macaroon-bakery.v1/bakery" | ||
"gopkg.in/macaroon-bakery.v1/bakery/checkers" | ||
|
||
"github.com/CanonicalLtd/jem/internal/jem" | ||
"github.com/CanonicalLtd/jem/internal/mongodoc" | ||
"github.com/CanonicalLtd/jem/params" | ||
) | ||
|
||
// newWSServer creates a new websocket server suitible for handling the api for modelUUID. | ||
var logger = loggo.GetLogger("jem.internal.jujuapi") | ||
|
||
// mapError maps JEM errors to errors suitable for use with the juju API. | ||
func mapError(err error) error { | ||
if err == nil { | ||
return nil | ||
} | ||
logger.Debugf("error: %s\n details: %s", err.Error(), errgo.Details(err)) | ||
if _, ok := err.(*jujuparams.Error); ok { | ||
return err | ||
} | ||
msg := err.Error() | ||
code := "" | ||
switch errgo.Cause(err) { | ||
case params.ErrNotFound: | ||
code = jujuparams.CodeNotFound | ||
} | ||
return &jujuparams.Error{ | ||
Message: msg, | ||
Code: code, | ||
} | ||
} | ||
|
||
// newWSServer creates a new WebSocket server suitible for handling the API for modelUUID. | ||
func newWSServer(jem *jem.JEM, modelUUID string) websocket.Server { | ||
hnd := wsHandler{ | ||
jem: jem, | ||
|
@@ -24,42 +58,147 @@ func newWSServer(jem *jem.JEM, modelUUID string) websocket.Server { | |
} | ||
} | ||
|
||
// wsHandler is a handler for a particular websocket connection. | ||
// wsHandler is a handler for a particular WebSocket connection. | ||
type wsHandler struct { | ||
jem *jem.JEM | ||
modelUUID string | ||
jem *jem.JEM | ||
modelUUID string | ||
conn *rpc.Conn | ||
model *mongodoc.Model | ||
controller *mongodoc.Controller | ||
} | ||
|
||
// handle handles the connection. | ||
func (h *wsHandler) handle(wsConn *websocket.Conn) { | ||
codec := jsoncodec.NewWebsocket(wsConn) | ||
conn := rpc.NewConn(codec, observer.None()) | ||
h.conn = rpc.NewConn(codec, observer.None()) | ||
|
||
// TODO(mhilton) serve something useful on this connection. | ||
err := common.UnknownModelError(h.modelUUID) | ||
conn.ServeFinder(&errRoot{err}, serverError) | ||
conn.Start() | ||
var root rpc.MethodFinder | ||
root = adminRoot{admin{h}} | ||
err := h.resolveUUID() | ||
if err != nil { | ||
root = &errRoot{err} | ||
} | ||
h.conn.ServeFinder(root, mapError) | ||
h.conn.Start() | ||
select { | ||
case <-conn.Dead(): | ||
case <-h.conn.Dead(): | ||
} | ||
conn.Close() | ||
h.conn.Close() | ||
} | ||
|
||
func serverError(err error) error { | ||
if err := common.ServerError(err); err != nil { | ||
return err | ||
func (h *wsHandler) resolveUUID() error { | ||
var err error | ||
h.model, err = h.jem.ModelFromUUID(h.modelUUID) | ||
if err != nil { | ||
return errgo.Mask(err, errgo.Is(params.ErrNotFound)) | ||
} | ||
h.controller, err = h.jem.Controller(h.model.Controller) | ||
return errgo.Mask(err) | ||
} | ||
|
||
type admin struct { | ||
handler *wsHandler | ||
} | ||
|
||
func (a admin) Admin(id string) (admin, error) { | ||
if id != "" { | ||
// Safeguard id for possible future use. | ||
return admin{}, common.ErrBadId | ||
} | ||
return a, nil | ||
} | ||
|
||
// Login implements the Login method on the Admin facade. | ||
func (a admin) Login(req jujuparams.LoginRequest) (jujuparams.LoginResultV1, error) { | ||
// JAAS only supports macaroon login, ignore all the other fields. | ||
attr, err := a.handler.jem.Bakery.CheckAny(req.Macaroons, nil, checkers.TimeBefore) | ||
if err != nil { | ||
if verr, ok := err.(*bakery.VerificationError); ok { | ||
m, err := a.handler.jem.NewMacaroon() | ||
if err != nil { | ||
return jujuparams.LoginResultV1{}, errgo.Notef(err, "cannot create macaroon") | ||
} | ||
return jujuparams.LoginResultV1{ | ||
DischargeRequired: m, | ||
DischargeRequiredReason: verr.Error(), | ||
}, nil | ||
} | ||
return jujuparams.LoginResultV1{}, errgo.Mask(err) | ||
} | ||
a.handler.jem.Auth.Username = attr["username"] | ||
if err := a.handler.jem.CheckCanRead(a.handler.model); err != nil { | ||
return jujuparams.LoginResultV1{}, errgo.Mask(err, errgo.Is(params.ErrUnauthorized)) | ||
} | ||
|
||
// Login successful | ||
a.handler.jem.Auth = jem.Authorization{attr["username"]} | ||
|
||
// If the UUID is for a model send a redirect error. | ||
if a.handler.model.Id != a.handler.controller.Id { | ||
return jujuparams.LoginResultV1{}, &jujuparams.Error{ | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Just to understand the code, how do clients know where to redirect? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ah, should they call RedirectInfo right after the failure response? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. yes, That logic is in juju already. |
||
Code: jujuparams.CodeRedirect, | ||
Message: "redirect required", | ||
} | ||
} | ||
|
||
// TODO (mhilton) serve some new methods | ||
return jujuparams.LoginResultV1{ | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Juju returns more info in case of successful login, like facades and current user info. Is that part of your TODO above? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It is yes, especially the facades part. |
||
ModelTag: names.NewModelTag(a.handler.model.UUID).String(), | ||
ControllerTag: names.NewModelTag(a.handler.controller.UUID).String(), | ||
ServerVersion: "2.0.0", | ||
}, nil | ||
} | ||
|
||
// RedirectInfo implements the RedirectInfo method on the Admin facade. | ||
func (a admin) RedirectInfo() (jujuparams.RedirectInfoResult, error) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Add a docstring here. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. done, and on Login above |
||
if a.handler.jem.Auth.Username == "" { | ||
return jujuparams.RedirectInfoResult{}, params.ErrUnauthorized | ||
} | ||
if err := a.handler.jem.CheckCanRead(a.handler.model); err != nil { | ||
return jujuparams.RedirectInfoResult{}, errgo.Mask(err, errgo.Is(params.ErrUnauthorized)) | ||
} | ||
if a.handler.model.Id == a.handler.controller.Id { | ||
return jujuparams.RedirectInfoResult{}, errgo.New("not redirected") | ||
} | ||
nhps, err := network.ParseHostPorts(a.handler.controller.HostPorts...) | ||
if err != nil { | ||
return jujuparams.RedirectInfoResult{}, errgo.Mask(err) | ||
} | ||
hps := jujuparams.FromNetworkHostPorts(nhps) | ||
return jujuparams.RedirectInfoResult{ | ||
Servers: [][]jujuparams.HostPort{hps}, | ||
CACert: a.handler.controller.CACert, | ||
}, nil | ||
} | ||
|
||
// adminRoot is a rpc.MethodFinder that implements the admin interface. | ||
type adminRoot struct { | ||
admin | ||
} | ||
|
||
// FindMethod implements rpc.MethodFinder. | ||
func (r adminRoot) FindMethod(rootName string, version int, methodName string) (rpcreflect.MethodCaller, error) { | ||
if rootName != "Admin" { | ||
return nil, &rpcreflect.CallNotImplementedError{ | ||
RootMethod: rootName, | ||
Version: version, | ||
} | ||
} | ||
if version < 3 { | ||
return nil, &rpc.RequestError{ | ||
Code: jujuparams.CodeNotSupported, | ||
Message: "JAAS does not support login from old clients", | ||
} | ||
} | ||
return nil | ||
return rpcreflect.ValueOf(reflect.ValueOf(r.admin)).FindMethod("Admin", 0, methodName) | ||
} | ||
|
||
// errRoot implements the API that a client first sees | ||
// when connecting to the API. It exposes the same API as initialRoot, except | ||
// it returns the requested error when the client makes any request. | ||
// errRoot is a rpc.MethodFinder that always returns an error. | ||
type errRoot struct { | ||
err error | ||
} | ||
|
||
// FindMethod conforms to the same API as initialRoot, but we'll always return (nil, err) | ||
// FindMethod implements rpc.MethodFinder, but will always return (nil, err) | ||
func (r *errRoot) FindMethod(rootName string, version int, methodName string) (rpcreflect.MethodCaller, error) { | ||
return nil, r.err | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why the controller is public and a user is added if len(loc) != 0?
Could you please add a comment?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
done