github.com/niedbalski/juju@v0.0.0-20190215020005-8ff100488e47/apiserver/stateauthenticator/auth.go (about) 1 // Copyright 2018 Canonical Ltd. 2 // Licensed under the AGPLv3, see LICENCE file for details. 3 4 package stateauthenticator 5 6 import ( 7 "encoding/base64" 8 "net/http" 9 "strings" 10 "time" 11 12 "github.com/juju/clock" 13 "github.com/juju/errors" 14 "gopkg.in/juju/names.v2" 15 "gopkg.in/macaroon-bakery.v2-unstable/httpbakery" 16 "gopkg.in/macaroon.v2-unstable" 17 18 "github.com/juju/juju/apiserver/apiserverhttp" 19 "github.com/juju/juju/apiserver/authentication" 20 "github.com/juju/juju/apiserver/common" 21 "github.com/juju/juju/apiserver/httpcontext" 22 "github.com/juju/juju/apiserver/params" 23 "github.com/juju/juju/state" 24 ) 25 26 // Authenticator is an implementation of httpcontext.Authenticator, 27 // using *state.State for authentication. 28 // 29 // This Authenticator only works with requests that have been handled 30 // by one of the httpcontext.*ModelHandler handlers. 31 type Authenticator struct { 32 statePool *state.StatePool 33 authContext *authContext 34 } 35 36 // NewAuthenticator returns a new Authenticator using the given StatePool. 37 func NewAuthenticator(statePool *state.StatePool, clock clock.Clock) (*Authenticator, error) { 38 authContext, err := newAuthContext(statePool.SystemState(), clock) 39 if err != nil { 40 return nil, errors.Trace(err) 41 } 42 return &Authenticator{ 43 statePool: statePool, 44 authContext: authContext, 45 }, nil 46 } 47 48 // Maintain periodically expires local login interactions. 49 func (a *Authenticator) Maintain(done <-chan struct{}) { 50 for { 51 select { 52 case <-done: 53 return 54 case <-a.authContext.clock.After(authentication.LocalLoginInteractionTimeout): 55 now := a.authContext.clock.Now() 56 a.authContext.localUserInteractions.Expire(now) 57 } 58 } 59 } 60 61 // CreateLocalLoginMacaroon is part of the 62 // httpcontext.LocalMacaroonAuthenticator interface. 63 func (a *Authenticator) CreateLocalLoginMacaroon(tag names.UserTag) (*macaroon.Macaroon, error) { 64 return a.authContext.CreateLocalLoginMacaroon(tag) 65 } 66 67 // AddHandlers adds the handlers to the given mux for handling local 68 // macaroon logins. 69 func (a *Authenticator) AddHandlers(mux *apiserverhttp.Mux) { 70 h := &localLoginHandlers{ 71 authCtxt: a.authContext, 72 finder: a.statePool.SystemState(), 73 } 74 h.AddHandlers(mux) 75 } 76 77 // Authenticate is part of the httpcontext.Authenticator interface. 78 func (a *Authenticator) Authenticate(req *http.Request) (httpcontext.AuthInfo, error) { 79 modelUUID := httpcontext.RequestModelUUID(req) 80 if modelUUID == "" { 81 return httpcontext.AuthInfo{}, errors.New("model UUID not found") 82 } 83 loginRequest, err := LoginRequest(req) 84 if err != nil { 85 return httpcontext.AuthInfo{}, errors.Trace(err) 86 } 87 return a.AuthenticateLoginRequest(req.Host, modelUUID, loginRequest) 88 } 89 90 // AuthenticateLoginRequest authenticates a LoginRequest. 91 // 92 // TODO(axw) we shouldn't be using params types here. 93 func (a *Authenticator) AuthenticateLoginRequest( 94 serverHost string, 95 modelUUID string, 96 req params.LoginRequest, 97 ) (httpcontext.AuthInfo, error) { 98 var authTag names.Tag 99 if req.AuthTag != "" { 100 tag, err := names.ParseTag(req.AuthTag) 101 if err != nil { 102 return httpcontext.AuthInfo{}, errors.Trace(err) 103 } 104 authTag = tag 105 } 106 107 st, err := a.statePool.Get(modelUUID) 108 if err != nil { 109 return httpcontext.AuthInfo{}, errors.Trace(err) 110 } 111 defer st.Release() 112 113 authenticator := a.authContext.authenticator(serverHost) 114 authInfo, err := a.checkCreds(st.State, req, authTag, true, authenticator) 115 if err != nil { 116 if common.IsDischargeRequiredError(err) || errors.IsNotProvisioned(err) { 117 // TODO(axw) move out of common? 118 return httpcontext.AuthInfo{}, errors.Trace(err) 119 } 120 if _, ok := authTag.(names.MachineTag); ok && !st.IsController() { 121 // Controller agents are allowed to log into any model. 122 var err2 error 123 authInfo, err2 = a.checkCreds( 124 a.statePool.SystemState(), 125 req, authTag, false, authenticator, 126 ) 127 if err2 == nil && authInfo.Controller { 128 err = nil 129 } 130 } 131 if err != nil { 132 return httpcontext.AuthInfo{}, errors.NewUnauthorized(err, "") 133 } 134 } 135 return authInfo, nil 136 } 137 138 func (a *Authenticator) checkCreds( 139 st *state.State, 140 req params.LoginRequest, 141 authTag names.Tag, 142 userLogin bool, 143 authenticator authentication.EntityAuthenticator, 144 ) (httpcontext.AuthInfo, error) { 145 var entityFinder authentication.EntityFinder = st 146 if userLogin { 147 // When looking up model users, use a custom 148 // entity finder that looks up both the local user (if the user 149 // tag is in the local domain) and the model user. 150 entityFinder = modelUserEntityFinder{st} 151 } 152 entity, err := authenticator.Authenticate(entityFinder, authTag, req) 153 if err != nil { 154 return httpcontext.AuthInfo{}, errors.Trace(err) 155 } 156 157 authInfo := httpcontext.AuthInfo{Entity: entity} 158 type withIsManager interface { 159 IsManager() bool 160 } 161 if entity, ok := entity.(withIsManager); ok { 162 authInfo.Controller = entity.IsManager() 163 } 164 165 type withLastLogin interface { 166 LastLogin() (time.Time, error) 167 UpdateLastLogin() error 168 } 169 if entity, ok := entity.(withLastLogin); ok { 170 lastLogin, err := entity.LastLogin() 171 if err == nil { 172 authInfo.LastConnection = lastLogin 173 } else if !state.IsNeverLoggedInError(err) { 174 return httpcontext.AuthInfo{}, errors.Trace(err) 175 } 176 // TODO log or return error returned by 177 // UpdateLastLogin? Old code didn't do 178 // anything with it. 179 entity.UpdateLastLogin() 180 } 181 return authInfo, nil 182 } 183 184 // LoginRequest extracts basic auth login details from an http.Request. 185 // 186 // TODO(axw) we shouldn't be using params types here. 187 func LoginRequest(req *http.Request) (params.LoginRequest, error) { 188 authHeader := req.Header.Get("Authorization") 189 macaroons := httpbakery.RequestMacaroons(req) 190 if authHeader == "" { 191 return params.LoginRequest{Macaroons: macaroons}, nil 192 } 193 parts := strings.Fields(authHeader) 194 if len(parts) != 2 || parts[0] != "Basic" { 195 // Invalid header format or no header provided. 196 return params.LoginRequest{}, errors.NotValidf("request format") 197 } 198 199 // Challenge is a base64-encoded "tag:pass" string. 200 // See RFC 2617, Section 2. 201 challenge, err := base64.StdEncoding.DecodeString(parts[1]) 202 if err != nil { 203 return params.LoginRequest{}, errors.NotValidf("request format") 204 } 205 tagPass := strings.SplitN(string(challenge), ":", 2) 206 if len(tagPass) != 2 { 207 return params.LoginRequest{}, errors.NotValidf("request format") 208 } 209 210 // Ensure that a sensible tag was passed. 211 if _, err := names.ParseTag(tagPass[0]); err != nil { 212 return params.LoginRequest{}, errors.Trace(err) 213 } 214 return params.LoginRequest{ 215 AuthTag: tagPass[0], 216 Credentials: tagPass[1], 217 Macaroons: httpbakery.RequestMacaroons(req), 218 Nonce: req.Header.Get(params.MachineNonceHeader), 219 }, nil 220 }