github.com/niedbalski/juju@v0.0.0-20190215020005-8ff100488e47/apiserver/httpcontext/auth.go (about) 1 // Copyright 2018 Canonical Ltd. 2 // Licensed under the AGPLv3, see LICENCE file for details. 3 4 package httpcontext 5 6 import ( 7 "context" 8 "encoding/json" 9 "fmt" 10 "net/http" 11 "time" 12 13 "github.com/juju/errors" 14 "github.com/juju/loggo" 15 "gopkg.in/juju/names.v2" 16 "gopkg.in/macaroon.v2-unstable" 17 18 "github.com/juju/juju/apiserver/common" 19 "github.com/juju/juju/apiserver/params" 20 ) 21 22 var logger = loggo.GetLogger("juju.apiserver.httpcontext") 23 24 // LocalMacaroonAuthenticator extends Authenticator with a method of 25 // creating a local login macaroon. The authenticator is expected to 26 // honour the resulting macaroon. 27 type LocalMacaroonAuthenticator interface { 28 Authenticator 29 30 // CreateLocalLoginMacaroon creates a macaroon that may be 31 // provided to a user as proof that they have logged in with 32 // a valid username and password. This macaroon may then be 33 // used to obtain a discharge macaroon so that the user can 34 // log in without presenting their password for a set amount 35 // of time. 36 CreateLocalLoginMacaroon(names.UserTag) (*macaroon.Macaroon, error) 37 } 38 39 // Authenticator provides an interface for authenticating a request. 40 // 41 // TODO(axw) contract should include macaroon discharge error. 42 // 43 // If this returns an error, the handler should return StatusUnauthorized. 44 type Authenticator interface { 45 // Authenticate authenticates the given request, returning the 46 // auth info. 47 // 48 // If the request does not contain any authentication details, 49 // then an error satisfying errors.IsNotFound will be returned. 50 Authenticate(req *http.Request) (AuthInfo, error) 51 52 // AuthenticateLoginRequest authenticates a LoginRequest. 53 // 54 // TODO(axw) we shouldn't be using params types here. 55 AuthenticateLoginRequest( 56 serverHost string, 57 modelUUID string, 58 req params.LoginRequest, 59 ) (AuthInfo, error) 60 } 61 62 // Authorizer is a function type for authorizing a request. 63 // 64 // If this returns an error, the handler should return StatusForbidden. 65 type Authorizer interface { 66 Authorize(AuthInfo) error 67 } 68 69 // AuthorizerFunc is a function type implementing Authorizer. 70 type AuthorizerFunc func(AuthInfo) error 71 72 // Authorize is part of the Authorizer interface. 73 func (f AuthorizerFunc) Authorize(info AuthInfo) error { 74 return f(info) 75 } 76 77 // Entity represents a user, machine, or unit that might be 78 // authenticated. 79 type Entity interface { 80 Tag() names.Tag 81 } 82 83 // AuthInfo is returned by Authenticator and RequestAuthInfo. 84 type AuthInfo struct { 85 // Entity is the user/machine/unit/etc that has authenticated. 86 Entity Entity 87 88 // LastConnection returns the time of the last connection for 89 // the authenticated entity. If it's the zero value, then the 90 // entity has not previously logged in. 91 LastConnection time.Time 92 93 // Controller reports whether or not the authenticated 94 // entity is a controller agent. 95 Controller bool 96 } 97 98 // BasicAuthHandler is an http.Handler that authenticates requests that 99 // it handles with a specified Authenticator. The auth details can later 100 // be retrieved using the top-level RequestAuthInfo function in this package. 101 type BasicAuthHandler struct { 102 http.Handler 103 104 // Authenticator is the Authenticator used for authenticating 105 // the HTTP requests handled by this handler. 106 Authenticator Authenticator 107 108 // Authorizer, if non-nil, will be called with the auth info 109 // returned by the Authenticator, to validate it for the route. 110 Authorizer Authorizer 111 } 112 113 // sendStatusAndJSON sends an HTTP status code and 114 // a JSON-encoded response to a client. 115 func sendStatusAndJSON(w http.ResponseWriter, statusCode int, response interface{}) error { 116 body, err := json.Marshal(response) 117 if err != nil { 118 return errors.Errorf("cannot marshal JSON result %#v: %v", response, err) 119 } 120 121 if statusCode == http.StatusUnauthorized { 122 w.Header().Set("WWW-Authenticate", `Basic realm="juju"`) 123 } 124 w.Header().Set("Content-Type", params.ContentTypeJSON) 125 w.Header().Set("Content-Length", fmt.Sprint(len(body))) 126 w.WriteHeader(statusCode) 127 if _, err := w.Write(body); err != nil { 128 return errors.Annotate(err, "cannot write response") 129 } 130 return nil 131 } 132 133 // sendError sends a JSON-encoded error response 134 // for errors encountered during processing. 135 func sendError(w http.ResponseWriter, errToSend error) error { 136 paramsErr, statusCode := common.ServerErrorAndStatus(errToSend) 137 logger.Debugf("sending error: %d %v", statusCode, paramsErr) 138 return errors.Trace(sendStatusAndJSON(w, statusCode, ¶ms.ErrorResult{ 139 Error: paramsErr, 140 })) 141 } 142 143 // ServeHTTP is part of the http.Handler interface. 144 func (h *BasicAuthHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) { 145 authInfo, err := h.Authenticator.Authenticate(req) 146 if err != nil { 147 w.Header().Set("WWW-Authenticate", `Basic realm="juju"`) 148 if common.IsDischargeRequiredError(err) { 149 sendErr := sendError(w, err) 150 if sendErr != nil { 151 logger.Errorf("%v", sendErr) 152 } 153 return 154 } 155 http.Error(w, 156 fmt.Sprintf("authentication failed: %s", err), 157 http.StatusUnauthorized, 158 ) 159 return 160 } 161 if h.Authorizer != nil { 162 if err := h.Authorizer.Authorize(authInfo); err != nil { 163 http.Error(w, 164 fmt.Sprintf("authorization failed: %s", err), 165 http.StatusForbidden, 166 ) 167 return 168 } 169 } 170 ctx := context.WithValue(req.Context(), authInfoKey{}, authInfo) 171 req = req.WithContext(ctx) 172 h.Handler.ServeHTTP(w, req) 173 } 174 175 type authInfoKey struct{} 176 177 // RequestAuthInfo returns the AuthInfo associated with the request, 178 // if any, and a boolean indicating whether or not the request was 179 // authenticated. 180 func RequestAuthInfo(req *http.Request) (AuthInfo, bool) { 181 authInfo, ok := req.Context().Value(authInfoKey{}).(AuthInfo) 182 return authInfo, ok 183 }