Skip to content

Commit

Permalink
Merge pull request juju#6182 from axw/login-local-macaroon-part3
Browse files Browse the repository at this point in the history
apiserver: pass server address to auth context

When a client connects, we record the Host value from the HTTP request, and
pass it through to the auth context. This will be used in a following
patch, as the Location field of a third-party caveat for local users.
  • Loading branch information
jujubot authored Sep 8, 2016
2 parents dba1caf + 81cd14b commit 3066c25
Show file tree
Hide file tree
Showing 6 changed files with 93 additions and 38 deletions.
12 changes: 10 additions & 2 deletions apiserver/admin.go
Original file line number Diff line number Diff line change
Expand Up @@ -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{
Expand Down Expand Up @@ -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 {
Expand Down
15 changes: 9 additions & 6 deletions apiserver/apiserver.go
Original file line number Diff line number Diff line change
Expand Up @@ -402,7 +402,10 @@ func (srv *Server) endpoints() []apihttp.Endpoint {
add("/register",
&registerUserHandler{
httpCtxt,
srv.authCtxt.userAuth.CreateLocalLoginMacaroon,
// NOTE(axw) the string we pass in at the moment is
// unimportant, as it is not used in the branch. In
// the next branch, we won't need to do this.
srv.authCtxt.authenticator("").localUserAuth().CreateLocalLoginMacaroon,
},
)
add("/api", mainAPIHandler)
Expand Down Expand Up @@ -515,20 +518,20 @@ func (srv *Server) apiHandler(w http.ResponseWriter, req *http.Request) {
Handler: func(conn *websocket.Conn) {
modelUUID := req.URL.Query().Get(":modeluuid")
logger.Tracef("got a request for model %q", modelUUID)
if err := srv.serveConn(conn, modelUUID, apiObserver); err != nil {
if err := srv.serveConn(conn, modelUUID, apiObserver, req.Host); err != nil {
logger.Errorf("error serving RPCs: %v", err)
}
},
}
wsServer.ServeHTTP(w, req)
}

func (srv *Server) serveConn(wsConn *websocket.Conn, modelUUID string, apiObserver observer.Observer) error {
func (srv *Server) serveConn(wsConn *websocket.Conn, modelUUID string, apiObserver observer.Observer, host string) error {
codec := jsoncodec.NewWebsocket(wsConn)

conn := rpc.NewConn(codec, apiObserver)

h, err := srv.newAPIHandler(conn, modelUUID)
h, err := srv.newAPIHandler(conn, modelUUID, host)
if err != nil {
conn.ServeRoot(&errRoot{err}, serverError)
} else {
Expand All @@ -546,7 +549,7 @@ func (srv *Server) serveConn(wsConn *websocket.Conn, modelUUID string, apiObserv
return conn.Close()
}

func (srv *Server) newAPIHandler(conn *rpc.Conn, modelUUID string) (*apiHandler, error) {
func (srv *Server) newAPIHandler(conn *rpc.Conn, modelUUID, serverHost string) (*apiHandler, error) {
// Note that we don't overwrite modelUUID here because
// newAPIHandler treats an empty modelUUID as signifying
// the API version used.
Expand All @@ -561,7 +564,7 @@ func (srv *Server) newAPIHandler(conn *rpc.Conn, modelUUID string) (*apiHandler,
if err != nil {
return nil, errors.Trace(err)
}
return newAPIHandler(srv, st, conn, modelUUID)
return newAPIHandler(srv, st, conn, modelUUID, serverHost)
}

func (srv *Server) mongoPinger() error {
Expand Down
69 changes: 51 additions & 18 deletions apiserver/authcontext.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,12 @@ type authContext struct {

clock clock.Clock
agentAuth authentication.AgentAuthenticator
userAuth authentication.UserAuthenticator

// localUserBakeryService is the bakery.Service used by the controller
// for authenticating local users. In time, we may want to use this for
// both local and external users. Note that this service does not
// discharge the third-party caveats.
localUserBakeryService *expirableStorageBakeryService

// localUserInteractions maintains a set of in-progress local user
// authentication interactions.
Expand All @@ -48,28 +53,47 @@ func newAuthContext(st *state.State) (*authContext, error) {
clock: clock.WallClock,
localUserInteractions: authentication.NewInteractions(),
}

// Create a bakery service for local user authentication. This service
// persists keys into MongoDB in a TTL collection.
store, err := st.NewBakeryStorage()
if err != nil {
return nil, errors.Trace(err)
}
// We use a non-nil, but empty key, because we don't use the
// key, and don't want to incur the overhead of generating one
// each time we create a service.
bakeryService, key, err := newBakeryService(st, store, nil)
localUserBakeryService, localUserBakeryServiceKey, err := newBakeryService(
st, store, nil,
)
if err != nil {
return nil, errors.Trace(err)
}
ctxt.userAuth.Service = &expirableStorageBakeryService{bakeryService, key, store, nil}
// TODO(fwereade) 2016-07-21 there should be a clock parameter
ctxt.userAuth.Clock = clock.WallClock
ctxt.localUserBakeryService = &expirableStorageBakeryService{
localUserBakeryService, localUserBakeryServiceKey, store, nil,
}
return ctxt, nil
}

// authenticator returns an authenticator.EntityAuthenticator for the API
// connection associated with the specified API server host.
func (ctxt *authContext) authenticator(serverHost string) authenticator {
return authenticator{ctxt: ctxt, serverHost: serverHost}
}

// authenticator implements authenticator.EntityAuthenticator, delegating
// to the appropriate authenticator based on the tag kind.
type authenticator struct {
ctxt *authContext
serverHost string
}

// Authenticate implements authentication.EntityAuthenticator
// by choosing the right kind of authentication for the given
// tag.
func (ctxt *authContext) Authenticate(entityFinder authentication.EntityFinder, tag names.Tag, req params.LoginRequest) (state.Entity, error) {
auth, err := ctxt.authenticatorForTag(tag)
func (a authenticator) Authenticate(
entityFinder authentication.EntityFinder,
tag names.Tag,
req params.LoginRequest,
) (state.Entity, error) {
auth, err := a.authenticatorForTag(tag)
if err != nil {
return nil, errors.Trace(err)
}
Expand All @@ -78,9 +102,9 @@ func (ctxt *authContext) Authenticate(entityFinder authentication.EntityFinder,

// authenticatorForTag returns the authenticator appropriate
// to use for a login with the given possibly-nil tag.
func (ctxt *authContext) authenticatorForTag(tag names.Tag) (authentication.EntityAuthenticator, error) {
func (a authenticator) authenticatorForTag(tag names.Tag) (authentication.EntityAuthenticator, error) {
if tag == nil {
auth, err := ctxt.macaroonAuth()
auth, err := a.ctxt.externalMacaroonAuth()
if errors.Cause(err) == errMacaroonAuthNotConfigured {
// Make a friendlier error message.
err = errors.New("no credentials provided")
Expand All @@ -92,17 +116,26 @@ func (ctxt *authContext) authenticatorForTag(tag names.Tag) (authentication.Enti
}
switch tag.Kind() {
case names.UnitTagKind, names.MachineTagKind:
return &ctxt.agentAuth, nil
return &a.ctxt.agentAuth, nil
case names.UserTagKind:
return &ctxt.userAuth, nil
return a.localUserAuth(), nil
default:
return nil, errors.Annotatef(common.ErrBadRequest, "unexpected login entity tag")
}
}

// macaroonAuth returns an authenticator that can authenticate macaroon-based
// logins. If it fails once, it will always fail.
func (ctxt *authContext) macaroonAuth() (authentication.EntityAuthenticator, error) {
// localUserAuth returns an authenticator that can authenticate logins for
// local users with either passwords or macaroons.
func (a authenticator) localUserAuth() *authentication.UserAuthenticator {
return &authentication.UserAuthenticator{
Service: a.ctxt.localUserBakeryService,
Clock: a.ctxt.clock,
}
}

// externalMacaroonAuth returns an authenticator that can authenticate macaroon-based
// logins for external users. If it fails once, it will always fail.
func (ctxt *authContext) externalMacaroonAuth() (authentication.EntityAuthenticator, error) {
ctxt.macaroonAuthOnce.Do(func() {
ctxt._macaroonAuth, ctxt._macaroonAuthError = newExternalMacaroonAuth(ctxt.st)
})
Expand All @@ -116,7 +149,7 @@ var errMacaroonAuthNotConfigured = errors.New("macaroon authentication is not co

// newExternalMacaroonAuth returns an authenticator that can authenticate
// macaroon-based logins for external users. This is just a helper function
// for authCtxt.macaroonAuth.
// for authCtxt.externalMacaroonAuth.
func newExternalMacaroonAuth(st *state.State) (*authentication.ExternalMacaroonAuthenticator, error) {
controllerCfg, err := st.ControllerConfig()
if err != nil {
Expand Down
8 changes: 4 additions & 4 deletions apiserver/export_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,15 +32,15 @@ var (
)

func ServerMacaroon(srv *Server) (*macaroon.Macaroon, error) {
auth, err := srv.authCtxt.macaroonAuth()
auth, err := srv.authCtxt.externalMacaroonAuth()
if err != nil {
return nil, err
}
return auth.(*authentication.ExternalMacaroonAuthenticator).Macaroon, nil
}

func ServerBakeryService(srv *Server) (authentication.BakeryService, error) {
auth, err := srv.authCtxt.macaroonAuth()
auth, err := srv.authCtxt.externalMacaroonAuth()
if err != nil {
return nil, err
}
Expand All @@ -50,7 +50,7 @@ func ServerBakeryService(srv *Server) (authentication.BakeryService, error) {
// ServerAuthenticatorForTag calls the authenticatorForTag method
// of the server's authContext.
func ServerAuthenticatorForTag(srv *Server, tag names.Tag) (authentication.EntityAuthenticator, error) {
return srv.authCtxt.authenticatorForTag(tag)
return srv.authCtxt.authenticator("testing.invalid:1234").authenticatorForTag(tag)
}

func APIHandlerWithEntity(entity state.Entity) *apiHandler {
Expand Down Expand Up @@ -98,7 +98,7 @@ func TestingAPIHandler(c *gc.C, srvSt, st *state.State) (*apiHandler, *common.Re
state: srvSt,
tag: names.NewMachineTag("0"),
}
h, err := newAPIHandler(srv, st, nil, st.ModelUUID())
h, err := newAPIHandler(srv, st, nil, st.ModelUUID(), "testing.invalid:1234")
c.Assert(err, jc.ErrorIsNil)
return h, h.getResources()
}
Expand Down
5 changes: 3 additions & 2 deletions apiserver/httpcontext.go
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,8 @@ func (ctxt *httpContext) stateForRequestAuthenticated(r *http.Request) (*state.S
if err != nil {
return nil, nil, errors.NewUnauthorized(err, "")
}
entity, _, err := checkCreds(st, req, true, ctxt.srv.authCtxt)
authenticator := ctxt.srv.authCtxt.authenticator(r.Host)
entity, _, err := checkCreds(st, req, true, authenticator)
if err != nil {
if common.IsDischargeRequiredError(err) {
return nil, nil, errors.Trace(err)
Expand All @@ -71,7 +72,7 @@ func (ctxt *httpContext) stateForRequestAuthenticated(r *http.Request) (*state.S
// Handle the special case of a worker on a controller machine
// acting on behalf of a hosted model.
if isMachineTag(req.AuthTag) {
entity, err := checkControllerMachineCreds(ctxt.srv.state, req, ctxt.srv.authCtxt)
entity, err := checkControllerMachineCreds(ctxt.srv.state, req, authenticator)
if err != nil {
return nil, nil, errors.NewUnauthorized(err, "")
}
Expand Down
22 changes: 16 additions & 6 deletions apiserver/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,22 +48,28 @@ type apiHandler struct {
rpcConn *rpc.Conn
resources *common.Resources
entity state.Entity

// An empty modelUUID means that the user has logged in through the
// root of the API server rather than the /model/:model-uuid/api
// path, logins processed with v2 or later will only offer the
// user manager and model manager api endpoints from here.
modelUUID string

// serverHost is the host:port of the API server that the client
// connected to.
serverHost string
}

var _ = (*apiHandler)(nil)

// newAPIHandler returns a new apiHandler.
func newAPIHandler(srv *Server, st *state.State, rpcConn *rpc.Conn, modelUUID string) (*apiHandler, error) {
func newAPIHandler(srv *Server, st *state.State, rpcConn *rpc.Conn, modelUUID string, serverHost string) (*apiHandler, error) {
r := &apiHandler{
state: st,
resources: common.NewResources(),
rpcConn: rpcConn,
modelUUID: modelUUID,
state: st,
resources: common.NewResources(),
rpcConn: rpcConn,
modelUUID: modelUUID,
serverHost: serverHost,
}
if err := r.resources.RegisterNamed("machineID", common.StringResource(srv.tag.Id())); err != nil {
return nil, errors.Trace(err)
Expand All @@ -75,7 +81,11 @@ func newAPIHandler(srv *Server, st *state.State, rpcConn *rpc.Conn, modelUUID st
return nil, errors.Trace(err)
}
if err := r.resources.RegisterNamed("createLocalLoginMacaroon", common.ValueResource{
srv.authCtxt.userAuth.CreateLocalLoginMacaroon,
// NOTE(axw) the string we pass in at the moment is
// unimportant, as it is not used in the branch. In
// the next branch, we won't need to register this
// resource.
srv.authCtxt.authenticator("").localUserAuth().CreateLocalLoginMacaroon,
}); err != nil {
return nil, errors.Trace(err)
}
Expand Down

0 comments on commit 3066c25

Please sign in to comment.