Changeset 6897 for TI12-security


Ignore:
Timestamp:
27/05/10 16:36:40 (9 years ago)
Author:
pjkersha
Message:

Fixed setting of authentication realm for HTTP Basic Auth middleware and improved interface to callback function by providing a exception type for the callback function to use to pass back message and HTTP status code.

Location:
TI12-security/trunk/MyProxyServerUtils/myproxy/server
Files:
9 edited

Legend:

Unmodified
Added
Removed
  • TI12-security/trunk/MyProxyServerUtils/myproxy/server/test/httpbasicauth.ini

    r6881 r6897  
    2323httpbasicauth.authnFuncEnvKeyName = HTTPBASICAUTH_FUNC 
    2424httpbasicauth.rePathMatchList = /auth 
     25httpbasicauth.realm = test-realm 
  • TI12-security/trunk/MyProxyServerUtils/myproxy/server/test/myproxywsgi.ini

    r6895 r6897  
    2020paste.app_factory = myproxy.server.wsgi.app:MyProxyLogonApp.app_factory 
    2121prefix = myproxy. 
     22myproxy.httpbasicauth.realm = myproxy-realm 
    2223myproxy.logonFuncEnvKeyName = MYPROXY_LOGON_FUNC 
    2324myproxy.rePathMatchList = /logon 
  • TI12-security/trunk/MyProxyServerUtils/myproxy/server/test/test_httpbasicauth.py

    r6881 r6897  
    1818 
    1919from myproxy.server.wsgi.httpbasicauth import (HttpBasicAuthMiddleware, 
    20                                                HttpBasicAuthUnauthorized) 
     20                                               HttpBasicAuthResponseException) 
    2121 
    2222 
     
    8585            if (username != self.__class__.USERNAME or 
    8686                password != self.__class__.PASSWORD): 
    87                 raise HttpBasicAuthUnauthorized() 
     87                raise HttpBasicAuthResponseException("Invalid credentials") 
    8888             
    8989        environ['HTTPBASICAUTH_FUNC'] = authenticate 
  • TI12-security/trunk/MyProxyServerUtils/myproxy/server/test/test_myproxywsgi.cfg

    r6895 r6897  
    1313# 
    1414[test01Logon] 
    15 username: pjk 
     15username = https://ceda.ac.uk/openid/Philip.Kershaw 
     16#username: pjk 
    1617#password = mypassword 
    1718uri = https://localhost:10443/logon 
  • TI12-security/trunk/MyProxyServerUtils/myproxy/server/test/test_myproxywsgi.py

    r6892 r6897  
    11#!/usr/bin/env python 
    2 """Unit tests for MyProxy WSGI Middleware classes and Application 
     2"""Unit tests for MyProxy WSGI Middleware classes and Application.  These are 
     3run using paste.fixture i.e. tests stubs to a web application server 
    34""" 
    45__author__ = "P J Kershaw" 
     
    9091         
    9192    def test01Logon(self): 
     93        # Test successful logon 
    9294        username = self.cfg.get('test01Logon', 'username') 
    9395        try:  
     
    105107        print response  
    106108        self.assert_(response) 
    107            
     109         
     110    def test02NoAuthorisationHeaderSet(self):    
     111        # Test failure with omission of HTTP Basic Auth header - a 401 result is 
     112        # expected. 
     113              
     114        # Create key pair and certificate request 
     115        keyPair, certReq = self._createRequestCreds() 
     116        response = self.app.post('/logon', certReq, status=401) 
     117        print response  
     118        self.assert_(response)   
     119         
     120    def test03NoCertificateRequestSent(self): 
     121        # Test with missing certificate request 
     122         
     123        username = self.cfg.get('test01Logon', 'username') 
     124        try:  
     125            password = self.cfg.get('test01Logon', 'password') 
     126        except NoOptionError: 
     127            password = getpass('test01Logon password: ') 
     128             
     129        base64String = base64.encodestring('%s:%s' % (username, password))[:-1] 
     130        authHeader =  "Basic %s" % base64String 
     131        headers = {'Authorization': authHeader} 
     132         
     133        # Bad POST'ed content 
     134        response = self.app.post('/logon', 'x', headers=headers, status=400) 
     135        print response  
     136        self.assert_(response) 
     137         
     138    def test04GET(self): 
     139        # Test HTTP GET request - should be rejected - POST is expected 
     140         
     141        username = self.cfg.get('test01Logon', 'username') 
     142        try:  
     143            password = self.cfg.get('test01Logon', 'password') 
     144        except NoOptionError: 
     145            password = getpass('test01Logon password: ') 
     146             
     147        base64String = base64.encodestring('%s:%s' % (username, password))[:-1] 
     148        authHeader =  "Basic %s" % base64String 
     149        headers = {'Authorization': authHeader} 
     150         
     151        response = self.app.get('/logon', headers=headers, status=405) 
     152        print response  
     153        self.assert_(response)                
    108154 
    109155if __name__ == "__main__": 
  • TI12-security/trunk/MyProxyServerUtils/myproxy/server/test/test_myproxywsgi_with_paster.py

    r6895 r6897  
    11#!/usr/bin/env python 
    22"""Unit tests for MyProxy WSGI Middleware classes and Application testing them 
    3 with PAster web application server 
     3with Paster web application server.  The server is started from __init__ method 
     4of the Test Case class and then called by the unit test methods.  The unit 
     5test methods themselves using a bash script myproxy-ws-logon.sh to query the  
     6MyProxy web application. 
    47""" 
    58__author__ = "P J Kershaw" 
     
    912__contact__ = "Philip.Kershaw@stfc.ac.uk" 
    1013__revision__ = '$Id$' 
    11 from os import path, waitpid 
     14from os import path 
    1215from getpass import getpass 
    13 from cStringIO import StringIO 
    1416from ConfigParser import SafeConfigParser, NoOptionError 
    1517import subprocess 
  • TI12-security/trunk/MyProxyServerUtils/myproxy/server/wsgi/app.py

    r6888 r6897  
    2828    """ 
    2929    PARAM_PREFIX = 'myproxy.logon.' 
     30    HTTPBASICAUTH_REALM_OPTNAME = 'httpbasicauth.realm' 
    3031     
    3132    @classmethod 
     
    4647        logonFuncEnvironKeyName = app_conf.get(logonFuncEnvKeyNameOptName, 
    4748                                MyProxyClientMiddleware.LOGON_FUNC_ENV_KEYNAME) 
    48              
    49         # Mirror callback function setting in HTTP Basic Auth middleware so 
    50         # that it correctly picks up the authentication function 
    51         app_conf[prefix + HttpBasicAuthMiddleware.AUTHN_FUNC_ENV_KEYNAME_OPTNAME 
    52                  ] = logonFuncEnvironKeyName 
    53                   
     49                        
    5450        app = MyProxyLogonApp() 
    5551        httpBasicAuthMWare = HttpBasicAuthMiddleware.filter_app_factory(app,  
     
    6763        httpBasicAuthMWare.authnFuncEnvironKeyName = app.logonFuncEnvironKeyName 
    6864         
     65        # Mirror callback function setting in HTTP Basic Auth middleware so 
     66        # that it correctly picks up the authentication function 
     67        realmOptName = prefix + cls.HTTPBASICAUTH_REALM_OPTNAME 
     68        httpBasicAuthMWare.realm = app_conf[realmOptName] 
     69         
    6970        return app 
    7071     
  • TI12-security/trunk/MyProxyServerUtils/myproxy/server/wsgi/httpbasicauth.py

    r6895 r6897  
    2323 
    2424 
    25 class HttpBasicAuthUnauthorized(HttpBasicAuthMiddlewareError):   
    26     """Raise from custom authentication interface in order to set HTTP  
    27     401 Unuathorized response""" 
     25class 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) 
    2843     
    2944     
     
    3348    AUTHN_FUNC_ENV_KEYNAME = ( 
    3449    'myproxy.server.wsgi.httpbasicauth.HttpBasicAuthMiddleware.authenticate') 
     50     
     51    # Config file option names 
    3552    AUTHN_FUNC_ENV_KEYNAME_OPTNAME = 'authnFuncEnvKeyName'        
     53    RE_PATH_MATCH_LIST_OPTNAME = 'rePathMatchList' 
     54    REALM_OPTNAME = 'realm' 
     55     
    3656    PARAM_PREFIX = 'http.auth.basic.' 
    37     HTTP_HDR_FIELDNAME = 'basic' 
     57     
     58    # HTTP header request and response field parameters 
     59    AUTHENTICATE_HDR_FIELDNAME = 'WWW-Authenticate' 
     60     
     61    # For testing header content in start_response_wrapper 
     62    AUTHENTICATE_HDR_FIELDNAME_LOWER = AUTHENTICATE_HDR_FIELDNAME.lower() 
     63     
     64    AUTHN_SCHEME_HDR_FIELDNAME = 'Basic' 
     65    AUTHN_SCHEME_HDR_FIELDNAME_LOWER = AUTHN_SCHEME_HDR_FIELDNAME.lower() 
     66     
    3867    FIELD_SEP = ':' 
    3968    AUTHZ_ENV_KEYNAME = 'HTTP_AUTHORIZATION' 
    4069     
    41     RE_PATH_MATCH_LIST_OPTNAME = 'rePathMatchList' 
    42      
    43     __slots__ = ('__rePathMatchList', '__authnFuncEnvironKeyName', '__app') 
     70    __slots__ = ( 
     71        '__rePathMatchList',  
     72        '__authnFuncEnvironKeyName',  
     73        '__realm', 
     74        '__app' 
     75    ) 
    4476     
    4577    def __init__(self, app): 
    4678        self.__rePathMatchList = None 
    4779        self.__authnFuncEnvironKeyName = None 
     80        self.__realm = None 
    4881        self.__app = app 
    4982 
     
    122155                                   "URI paths intercepted by this middleware") 
    123156 
     157    def _getRealm(self): 
     158        return self.__realm 
     159 
     160    def _setRealm(self, value): 
     161        if not isinstance(value, basestring): 
     162            raise TypeError('Expecting string type for ' 
     163                            '"realm"; got %r' % type(value)) 
     164         
     165        self.__realm = value 
     166 
     167    realm = property(fget=_getRealm, fset=_setRealm,  
     168                     doc="HTTP Authentication realm to set in responses") 
     169 
    124170    def _pathMatch(self, environ): 
    125171        """Apply a list of regular expression matching patterns to the contents 
     
    155201                        
    156202        method, encodedCreds = basicAuthHdr.split(None, 1) 
    157         if method.lower() != HttpBasicAuthMiddleware.HTTP_HDR_FIELDNAME: 
     203        if (method.lower() !=  
     204            HttpBasicAuthMiddleware.AUTHN_SCHEME_HDR_FIELDNAME_LOWER): 
    158205            log.debug("Auth method is %r not %r: skipping request", 
    159                       method, HttpBasicAuthMiddleware.HTTP_HDR_FIELDNAME) 
     206                      method,  
     207                      HttpBasicAuthMiddleware.AUTHN_SCHEME_HDR_FIELDNAME) 
    160208            return None, None 
    161209             
     
    166214    def __call__(self, environ, start_response): 
    167215        """Authenticate based HTTP header elements as specified by the HTTP 
    168         Basic Authentication spec.""" 
     216        Basic Authentication spec. 
     217         
     218        @param environ: WSGI environ  
     219        @type environ: dict-like type 
     220        @param start_response: WSGI start response function 
     221        @type start_response: function 
     222        @return: response 
     223        @rtype: iterable 
     224        """ 
    169225        log.debug("HttpBasicAuthNMiddleware.__call__ ...") 
    170226         
    171227        if not self._pathMatch(environ): 
    172228            return self.__app(environ, start_response) 
    173              
     229         
     230        def start_response_wrapper(status, headers):  
     231            """Ensure Authentication realm is included with 401 responses""" 
     232            statusCode = int(status.split()[0]) 
     233            if statusCode == httplib.UNAUTHORIZED: 
     234                authnRealmHdrFound = False 
     235                for name, val in headers: 
     236                    if (name.lower() ==  
     237                            self.__class__.AUTHENTICATE_HDR_FIELDNAME_LOWER): 
     238                        authnRealmHdrFound = True 
     239                        break 
     240                      
     241                if not authnRealmHdrFound: 
     242                    authnRealmHdr = (self.__class__.AUTHENTICATE_HDR_FIELDNAME, 
     243                                     "%s %s" % (                                    
     244                                     self.__class__.AUTHN_SCHEME_HDR_FIELDNAME, 
     245                                     self.realm)) 
     246                    headers.append(authnRealmHdr) 
     247                 
     248            return start_response(status, headers) 
     249         
    174250        username, password = self._parseCredentials(environ) 
    175251        if username is None: 
    176252            log.error('No username set in HTTP Authorization header') 
    177             return self.setErrorResponse(start_response,  
     253            return self.setErrorResponse(start_response_wrapper,  
    178254                                         msg="No username set\n") 
    179255         
     
    187263        # the next middleware is called in the chain 
    188264        try: 
    189             response = authenticateFunc(environ, start_response, username,  
     265            response = authenticateFunc(environ,  
     266                                        start_response_wrapper,  
     267                                        username,  
    190268                                        password) 
    191269            if response is None: 
    192                 return self.__app(environ, start_response) 
     270                return self.__app(environ, start_response_wrapper) 
    193271            else: 
    194272                return response 
    195273             
    196         except HttpBasicAuthUnauthorized: 
    197             log.error('Client authentication failed: %s',  
     274        except HttpBasicAuthResponseException, e: 
     275            log.error('Client authentication raised an exception: %s',  
    198276                      traceback.format_exc()) 
    199             return self.setErrorResponse(start_response)     
    200  
    201     @staticmethod 
    202     def setErrorResponse(start_response, msg=None,  
    203                          code=httplib.UNAUTHORIZED, contentType=None): 
     277            return self.setErrorResponse(start_response_wrapper, 
     278                                         msg=e.response, 
     279                                         code=e.code)     
     280 
     281    @classmethod 
     282    def setErrorResponse(cls, 
     283                         start_response,  
     284                         msg=None,  
     285                         code=httplib.UNAUTHORIZED, 
     286                         contentType=None): 
    204287        '''Convenience method to set a simple error response 
    205288         
     
    222305        if contentType is None: 
    223306            contentType = 'text/plain' 
    224                  
    225         start_response(status, 
    226                        [('Content-type', contentType), 
    227                         ('Content-Length', str(len(response)))]) 
     307                         
     308        headers = [ 
     309            ('Content-type', contentType),  
     310            ('Content-length', str(len(msg))) 
     311        ] 
     312        start_response(status, headers) 
    228313        return [response] 
  • TI12-security/trunk/MyProxyServerUtils/myproxy/server/wsgi/middleware.py

    r6893 r6897  
    1919from OpenSSL import crypto 
    2020from myproxy.client import MyProxyClient, MyProxyClientError 
    21         
     21from myproxy.server.wsgi.httpbasicauth import HttpBasicAuthResponseException 
     22   
    2223 
    2324class MyProxyClientMiddlewareError(Exception): 
     
    185186        def _myProxylogon(environ, start_response, username, password): 
    186187            """Wrap MyProxy logon method as a WSGI app 
    187             """                
    188             if environ.get('REQUEST_METHOD') == 'POST': 
    189                 wsgiInput = environ[ 
    190                                 MyProxyClientMiddleware.WSGI_INPUT_ENV_KEYNAME] 
    191                  
    192                 contentLength = int(environ.get('CONTENT_LENGTH', -1)) 
    193                 if contentLength == -1: 
    194                     raise MyProxyClientMiddlewareError('No "CONTENT_LENGTH" ' 
    195                                                        'setting found in ' 
    196                                                        'environ') 
    197                      
    198                 pemCertReq = wsgiInput.read(contentLength) 
    199                  
    200                 # Restore WSGI file object with duck typing(!) 
    201                 wsgiInput = StringIO() 
    202                 wsgiInput.write(pemCertReq) 
    203                 wsgiInput.seek(0) 
    204                  
    205                 # Expecting PEM encoded request 
    206                 certReq = crypto.load_certificate_request( 
    207                                                         crypto.FILETYPE_PEM, 
    208                                                         pemCertReq) 
    209                  
    210                 # Convert to ASN1 format expect by logon client call 
    211                 asn1CertReq = crypto.dump_certificate_request( 
    212                                                         crypto.FILETYPE_ASN1,  
    213                                                         certReq) 
    214             else: 
    215                 status = self.getStatusMessage(httplib.UNAUTHORIZED) 
    216                 response = ("HTTP request method %r not recognised for this " 
    217                             "request " % environ.get('REQUEST_METHOD',  
    218                                                      '<Not set>')) 
    219                 log.error(response) 
    220                 start_response(status, 
    221                                [('Content-length', str(len(response))), 
    222                                 ('Content-type', 'text/plain')]) 
    223                 return [response]   
    224                            
     188            """   
     189            requestMethod = environ.get('REQUEST_METHOD')              
     190            if requestMethod != 'POST': 
     191                response = "HTTP Request method not recognised" 
     192                log.error("HTTP Request method %r not recognised",  
     193                          requestMethod) 
     194                raise HttpBasicAuthResponseException(response,  
     195                                                     httplib.METHOD_NOT_ALLOWED)  
     196             
     197            wsgiInput = environ[MyProxyClientMiddleware.WSGI_INPUT_ENV_KEYNAME] 
     198                 
     199            contentLength = int(environ.get('CONTENT_LENGTH', -1)) 
     200            if contentLength == -1: 
     201                raise MyProxyClientMiddlewareError('No "CONTENT_LENGTH" ' 
     202                                                   'setting found in ' 
     203                                                   'environ') 
     204                 
     205            pemCertReq = wsgiInput.read(contentLength) 
     206             
     207            # Restore WSGI file object with duck typing(!) 
     208            wsgiInput = StringIO() 
     209            wsgiInput.write(pemCertReq) 
     210            wsgiInput.seek(0) 
     211             
     212            # Expecting PEM encoded request 
     213            try: 
     214                certReq = crypto.load_certificate_request(crypto.FILETYPE_PEM, 
     215                                                          pemCertReq) 
     216            except crypto.Error, e: 
     217                log.error("Error loading input certificate request: %r",  
     218                          pemCertReq) 
     219                raise HttpBasicAuthResponseException("Error loading input " 
     220                                                     "certificate request", 
     221                                                     httplib.BAD_REQUEST) 
     222                 
     223            # Convert to ASN1 format expect by logon client call 
     224            asn1CertReq = crypto.dump_certificate_request(crypto.FILETYPE_ASN1,  
     225                                                          certReq) 
     226  
    225227            try: 
    226228                credentials = self.myProxyClient.logon(username,  
     
    230232                response = '\n'.join(credentials) 
    231233                 
     234                start_response(status, 
     235                               [('Content-length', str(len(response))), 
     236                                ('Content-type', 'text/plain')]) 
     237                return [response] 
     238                        
    232239            except MyProxyClientError, e: 
    233                 status = self.getStatusMessage(httplib.UNAUTHORIZED) 
    234                 response = str(e) 
    235              
     240                raise HttpBasicAuthResponseException(str(e), 
     241                                                     httplib.UNAUTHORIZED) 
    236242            except socket.error, e: 
    237243                raise MyProxyClientMiddlewareError("Socket error " 
     
    243249                          self.myProxyClient.hostname, 
    244250                          traceback.format_exc()) 
    245                 raise 
    246              
    247             start_response(status, 
    248                            [('Content-length', str(len(response))), 
    249                             ('Content-type', 'text/plain')]) 
    250             return [response] 
     251                raise # Trigger 500 Internal Server Error 
     252             
     253 
    251254         
    252255        return _myProxylogon 
Note: See TracChangeset for help on using the changeset viewer.