Package myproxy :: Package ws :: Package server :: Package wsgi :: Module httpbasicauth
[hide private]

Source Code for Module myproxy.ws.server.wsgi.httpbasicauth

  1  """WSGI middleware implementing HTTP Basic Auth to support HTTPS proxy app to  
  2  MyProxy server. 
  3  """ 
  4  __author__ = "P J Kershaw" 
  5  __date__ = "21/05/10" 
  6  __copyright__ = "(C) 2010 Science and Technology Facilities Council" 
  7  __license__ = "BSD - see LICENSE file in top-level directory" 
  8  __contact__ = "Philip.Kershaw@stfc.ac.uk" 
  9  __revision__ = "$Id$" 
 10  import logging 
 11  log = logging.getLogger(__name__) 
 12  import traceback 
 13  import re 
 14  import httplib 
 15  import base64 
16 17 -class HttpBasicAuthMiddlewareError(Exception):
18 """Base exception type for HttpBasicAuthMiddleware"""
19
20 21 -class HttpBasicAuthMiddlewareConfigError(HttpBasicAuthMiddlewareError):
22 """Configuration error with HTTP Basic Auth middleware"""
23
24 25 -class HttpBasicAuthResponseException(HttpBasicAuthMiddlewareError):
26 """Exception class for use by the authentication function callback to 27 signal HTTP codes and messages back to HttpBasicAuthMiddleware. The code 28 can conceivably a non-error HTTP code such as 200 29 """
30 - def __init__(self, *arg, **kw):
31 """Extend Exception type to accommodate an extra HTTP response code 32 argument 33 """ 34 self.response = arg[0] 35 if len(arg) == 2: 36 argList = list(arg) 37 self.code = argList.pop() 38 arg = tuple(argList) 39 else: 40 self.code = httplib.UNAUTHORIZED 41 42 HttpBasicAuthMiddlewareError.__init__(self, *arg, **kw)
43
44 45 -class HttpBasicAuthMiddleware(object):
46 '''HTTP Basic Authentication Middleware 47 48 @cvar AUTHN_FUNC_ENV_KEYNAME: key name for referencing Authentication 49 callback function in environ. Upstream middleware must set this. 50 @type AUTHN_FUNC_ENV_KEYNAME: string 51 @cvar AUTHN_FUNC_ENV_KEYNAME_OPTNAME: in file option name for setting the 52 Authentication callback environ key 53 @type AUTHN_FUNC_ENV_KEYNAME_OPTNAME: string 54 @cvar REALM_OPTNAME: ini file option name for setting the HTTP Basic Auth 55 authentication realm 56 @type REALM_OPTNAME: string 57 @cvar PARAM_PREFIX: prefix for ini file options 58 @type PARAM_PREFIX: string 59 @cvar AUTHENTICATE_HDR_FIELDNAME: HTTP header field name 'WWW-Authenticate' 60 @type AUTHENTICATE_HDR_FIELDNAME: string 61 @cvar AUTHENTICATE_HDR_FIELDNAME_LOWER: lowercase version of 62 AUTHENTICATE_HDR_FIELDNAME class variable included for convenience with 63 string matching 64 @type AUTHENTICATE_HDR_FIELDNAME_LOWER: string 65 @cvar AUTHN_SCHEME_HDR_FIELDNAME: HTTP Authentication scheme identifier 66 @type AUTHN_SCHEME_HDR_FIELDNAME: string 67 @cvar FIELD_SEP: field separator for username/password header string 68 @type FIELD_SEP: string 69 @cvar AUTHZ_ENV_KEYNAME: WSGI environ key name for HTTP Basic Auth header 70 content 71 @type AUTHZ_ENV_KEYNAME: string 72 @cvar AUTHN_HDR_FORMAT: HTTP Basic Auth format string following RFC2617 73 @type AUTHN_HDR_FORMAT: string 74 75 @ivar __rePathMatchList: list of regular expression patterns used to match 76 incoming requests and enforce HTTP Basic Auth against 77 @type __rePathMatchList: list 78 @ivar __authnFuncEnvironKeyName: __authnFuncEnvironKeyName 79 @type __authnFuncEnvironKeyName: string 80 @ivar __realm: HTTP Basic Auth authentication realm 81 @type __realm: string 82 @ivar __app: next WSGI app/middleware in call chain 83 @type __app: function 84 ''' 85 AUTHN_FUNC_ENV_KEYNAME = ( 86 'myproxy.server.wsgi.httpbasicauth.HttpBasicAuthMiddleware.authenticate') 87 88 # Config file option names 89 AUTHN_FUNC_ENV_KEYNAME_OPTNAME = 'authnFuncEnvKeyName' 90 RE_PATH_MATCH_LIST_OPTNAME = 'rePathMatchList' 91 REALM_OPTNAME = 'realm' 92 93 PARAM_PREFIX = 'http.auth.basic.' 94 95 # HTTP header request and response field parameters 96 AUTHENTICATE_HDR_FIELDNAME = 'WWW-Authenticate' 97 98 # For testing header content in start_response_wrapper 99 AUTHENTICATE_HDR_FIELDNAME_LOWER = AUTHENTICATE_HDR_FIELDNAME.lower() 100 101 AUTHN_SCHEME_HDR_FIELDNAME = 'Basic' 102 AUTHN_SCHEME_HDR_FIELDNAME_LOWER = AUTHN_SCHEME_HDR_FIELDNAME.lower() 103 104 FIELD_SEP = ':' 105 AUTHZ_ENV_KEYNAME = 'HTTP_AUTHORIZATION' 106 107 AUTHN_HDR_FORMAT = '%s ' + REALM_OPTNAME + '="%s"' 108 109 __slots__ = ( 110 '__rePathMatchList', 111 '__authnFuncEnvironKeyName', 112 '__realm', 113 '__app' 114 ) 115
116 - def __init__(self, app):
117 """Create instance variables 118 @param app: next middleware/app in WSGI stack 119 @type app: function 120 """ 121 self.__rePathMatchList = None 122 self.__authnFuncEnvironKeyName = None 123 self.__realm = None 124 self.__app = app
125 126 @classmethod
127 - def filter_app_factory(cls, app, global_conf, prefix=PARAM_PREFIX, 128 **local_conf):
129 """Function following Paste filter app factory signature 130 131 @type app: callable following WSGI interface 132 @param app: next middleware/application in the chain 133 @type global_conf: dict 134 @param global_conf: PasteDeploy global configuration dictionary 135 @type prefix: basestring 136 @param prefix: prefix for configuration items 137 @type local_conf: dict 138 @param local_conf: PasteDeploy application specific configuration 139 dictionary 140 @rtype: myproxy.server.wsgi.httpbasicauth.HttpBasicAuthMiddleware 141 @return: an instance of this middleware 142 """ 143 httpBasicAuthFilter = cls(app) 144 httpBasicAuthFilter.parseConfig(prefix=prefix, **local_conf) 145 146 return httpBasicAuthFilter
147
148 - def parseConfig(self, prefix='', **app_conf):
149 """Parse dictionary of configuration items updating the relevant 150 attributes of this instance 151 152 @type prefix: basestring 153 @param prefix: prefix for configuration items 154 @type app_conf: dict 155 @param app_conf: PasteDeploy application specific configuration 156 dictionary 157 """ 158 rePathMatchListOptName = prefix + \ 159 HttpBasicAuthMiddleware.RE_PATH_MATCH_LIST_OPTNAME 160 rePathMatchListVal = app_conf.pop(rePathMatchListOptName, '') 161 162 self.rePathMatchList = [re.compile(i) 163 for i in rePathMatchListVal.split()] 164 165 paramName = prefix + \ 166 HttpBasicAuthMiddleware.AUTHN_FUNC_ENV_KEYNAME_OPTNAME 167 168 self.authnFuncEnvironKeyName = app_conf.get(paramName, 169 HttpBasicAuthMiddleware.AUTHN_FUNC_ENV_KEYNAME)
170
172 """Get authentication callback function environ key name 173 174 @rtype: basestring 175 @return: callback function environ key name 176 """ 177 return self.__authnFuncEnvironKeyName
178
179 - def _setAuthnFuncEnvironKeyName(self, value):
180 """Set authentication callback environ key name 181 182 @type value: basestring 183 @param value: callback function environ key name 184 """ 185 if not isinstance(value, basestring): 186 raise TypeError('Expecting string type for ' 187 '"authnFuncEnvironKeyName"; got %r type' % 188 type(value)) 189 190 self.__authnFuncEnvironKeyName = value
191 192 authnFuncEnvironKeyName = property(fget=_getAuthnFuncEnvironKeyName, 193 fset=_setAuthnFuncEnvironKeyName, 194 doc="key name in environ for the " 195 "custom authentication function " 196 "used by this class") 197
198 - def _getRePathMatchList(self):
199 """Get regular expression path match list 200 201 @rtype: tuple or list 202 @return: list of regular expressions used to match incoming request 203 paths and apply HTTP Basic Auth to 204 """ 205 return self.__rePathMatchList
206
207 - def _setRePathMatchList(self, value):
208 """Set regular expression path match list 209 210 @type value: tuple or list 211 @param value: list of regular expressions used to match incoming request 212 paths and apply HTTP Basic Auth to 213 """ 214 if not isinstance(value, (list, tuple)): 215 raise TypeError('Expecting list or tuple type for ' 216 '"rePathMatchList"; got %r' % type(value)) 217 218 self.__rePathMatchList = value
219 220 rePathMatchList = property(fget=_getRePathMatchList, 221 fset=_setRePathMatchList, 222 doc="List of regular expressions determine the " 223 "URI paths intercepted by this middleware") 224
225 - def _getRealm(self):
226 """Get realm 227 228 @rtype: basestring 229 @return: HTTP Authentication realm to set in responses 230 """ 231 return self.__realm
232
233 - def _setRealm(self, value):
234 """Set realm 235 236 @type value: basestring 237 @param value: HTTP Authentication realm to set in responses 238 """ 239 if not isinstance(value, basestring): 240 raise TypeError('Expecting string type for ' 241 '"realm"; got %r' % type(value)) 242 243 self.__realm = value
244 245 realm = property(fget=_getRealm, fset=_setRealm, 246 doc="HTTP Authentication realm to set in responses") 247
248 - def _pathMatch(self, environ):
249 """Apply a list of regular expression matching patterns to the contents 250 of environ['PATH_INFO'], if any match, return True. This method is 251 used to determine whether to apply SSL client authentication 252 253 @param environ: WSGI environment variables dictionary 254 @type environ: dict 255 @return: True if request path matches the regular expression list set, 256 False otherwise 257 @rtype: bool 258 """ 259 path = environ['PATH_INFO'] 260 for regEx in self.rePathMatchList: 261 if regEx.match(path): 262 return True 263 264 return False
265
266 - def _parseCredentials(self, environ):
267 """Extract username and password from HTTP_AUTHORIZATION environ key 268 269 @param environ: WSGI environ dict 270 @type environ: dict 271 272 @rtype: tuple 273 @return: username and password. If the key is not set or the auth 274 method is not basic return a two element tuple with elements both set 275 to None 276 """ 277 basicAuthHdr = environ.get(HttpBasicAuthMiddleware.AUTHZ_ENV_KEYNAME) 278 if basicAuthHdr is None: 279 log.debug("No %r setting in environ: skipping HTTP Basic Auth", 280 HttpBasicAuthMiddleware.AUTHZ_ENV_KEYNAME) 281 return None, None 282 283 method, encodedCreds = basicAuthHdr.split(None, 1) 284 if (method.lower() != 285 HttpBasicAuthMiddleware.AUTHN_SCHEME_HDR_FIELDNAME_LOWER): 286 log.debug("Auth method is %r not %r: skipping request", 287 method, 288 HttpBasicAuthMiddleware.AUTHN_SCHEME_HDR_FIELDNAME) 289 return None, None 290 291 creds = base64.decodestring(encodedCreds) 292 username, password = creds.rsplit(HttpBasicAuthMiddleware.FIELD_SEP, 1) 293 return username, password
294
295 - def __call__(self, environ, start_response):
296 """Authenticate based HTTP header elements as specified by the HTTP 297 Basic Authentication spec. 298 299 @param environ: WSGI environ 300 @type environ: dict-like type 301 @param start_response: WSGI start response function 302 @type start_response: function 303 @return: response 304 @rtype: iterable 305 @raise HttpBasicAuthMiddlewareConfigError: no authentication callback 306 found in environ 307 """ 308 log.debug("HttpBasicAuthNMiddleware.__call__ ...") 309 310 if not self._pathMatch(environ): 311 return self.__app(environ, start_response) 312 313 def start_response_wrapper(status, headers): 314 """Ensure Authentication realm is included with 401 responses""" 315 statusCode = int(status.split()[0]) 316 if statusCode == httplib.UNAUTHORIZED: 317 authnRealmHdrFound = False 318 for name, val in headers: 319 if (name.lower() == 320 self.__class__.AUTHENTICATE_HDR_FIELDNAME_LOWER): 321 authnRealmHdrFound = True 322 break 323 324 if not authnRealmHdrFound: 325 # Nb. realm requires double quotes according to RFC 326 authnRealmHdr = (self.__class__.AUTHENTICATE_HDR_FIELDNAME, 327 self.__class__.AUTHN_HDR_FORMAT % ( 328 self.__class__.AUTHN_SCHEME_HDR_FIELDNAME, 329 self.realm)) 330 headers.append(authnRealmHdr) 331 332 return start_response(status, headers)
333 334 username, password = self._parseCredentials(environ) 335 if username is None: 336 log.error('No username set in HTTP Authorization header') 337 return self.setErrorResponse(start_response_wrapper, 338 msg="No username set\n") 339 340 authenticateFunc = environ.get(self.authnFuncEnvironKeyName) 341 if authenticateFunc is None: 342 # HTTP 500 default is right for this error 343 raise HttpBasicAuthMiddlewareConfigError("No authentication " 344 "function set in environ") 345 346 # Call authentication middleware/application. If no response is set, 347 # the next middleware is called in the chain 348 try: 349 response = authenticateFunc(environ, 350 start_response_wrapper, 351 username, 352 password) 353 if response is None: 354 return self.__app(environ, start_response_wrapper) 355 else: 356 return response 357 358 except HttpBasicAuthResponseException, e: 359 log.error('Client authentication raised an exception: %s', 360 traceback.format_exc()) 361 return self.setErrorResponse(start_response_wrapper, 362 msg=e.response, 363 code=e.code)
364 365 @classmethod
366 - def setErrorResponse(cls, 367 start_response, 368 msg=None, 369 code=httplib.UNAUTHORIZED, 370 contentType=None):
371 '''Convenience method to set a simple error response 372 373 @type start_response: function 374 @param start_response: standard WSGI callable to set the HTTP header 375 @type msg: basestring 376 @param msg: optional error message 377 @type code: int 378 @param code: standard HTTP error response code 379 @type contentType: basestring 380 @param contentType: set 'Content-type' HTTP header field - defaults to 381 'text/plain' 382 @rtype: list 383 @return: HTTP error response 384 ''' 385 status = '%d %s' % (code, httplib.responses[code]) 386 if msg is None: 387 response = "%s\n" % status 388 else: 389 response = msg 390 391 if contentType is None: 392 contentType = 'text/plain' 393 394 headers = [ 395 ('Content-type', contentType), 396 ('Content-length', str(len(msg))) 397 ] 398 start_response(status, headers) 399 return [response]
400