source: TI12-security/trunk/MyProxyWebService/myproxy/server/wsgi/middleware.py @ 6957

Subversion URL: http://proj.badc.rl.ac.uk/svn/ndg/TI12-security/trunk/MyProxyWebService/myproxy/server/wsgi/middleware.py@6957
Revision 6957, 23.2 KB checked in by pjkersha, 9 years ago (diff)

Incomplete - task 5: MyProxy? Logon HTTPS Interface

  • fixed paths to be absolute for PasteDeploy? loadapp ini file in unit tests.
Line 
1"""MyProxy WSGI middleware - places a MyProxy client instance in environ for
2other downstream middleware or apps to access and use
3 
4NERC DataGrid Project
5"""
6__author__ = "P J Kershaw"
7__date__ = "24/05/10"
8__copyright__ = "(C) 2010 Science and Technology Facilities Council"
9__license__ = "BSD - see LICENSE file in top-level directory"
10__contact__ = "Philip.Kershaw@stfc.ac.uk"
11__revision__ = "$Id: $"
12import logging
13log = logging.getLogger(__name__)
14import traceback
15import socket
16import httplib
17import base64
18
19from webob import Request
20from OpenSSL import crypto
21
22from myproxy.client import MyProxyClient, MyProxyClientError
23from myproxy.server.wsgi.httpbasicauth import HttpBasicAuthResponseException
24 
25
26class MyProxyClientMiddlewareError(Exception):
27    """Runtime error with MyProxyClientMiddleware"""
28
29       
30class MyProxyClientMiddlewareConfigError(MyProxyClientMiddlewareError):
31    """Configuration error with MyProxyClientMiddleware"""
32
33
34class MyProxyClientMiddlewareBase(object):
35    """Base class for common functionality
36   
37    @cvar CLIENT_ENV_KEYNAME_OPTNAME: ini file option which sets the key name
38    in the WSGI environ for referring to the MyProxy client instance shared
39    between MyProxy* middleware/apps
40    @type CLIENT_ENV_KEYNAME_OPTNAME: string
41   
42    @cvar DEFAULT_CLIENT_ENV_KEYNAME: default value for key name set in the
43    WSGI environ dict which refers to the MyProxy client instance shared
44    between MyProxy* middleware/apps
45    @type DEFAULT_CLIENT_ENV_KEYNAME: string
46   
47    @ivar __app: WSGI callable for next middleware or app in the WSGI stack
48    @type __app: function
49   
50    @ivar __clientEnvironKeyName: key name set in the WSGI environ dict which
51    refers to the MyProxy client instance shared between MyProxy* middleware/
52    apps
53    @type __clientEnvironKeyName: string
54    """
55    __slots__ = (
56        '__app', 
57        '__clientEnvironKeyName',
58    )
59   
60    CLIENT_ENV_KEYNAME_OPTNAME = 'clientEnvKeyName'
61    DEFAULT_CLIENT_ENV_KEYNAME = ('myproxy.server.wsgi.middleware.'
62                                  'MyProxyClientMiddleware.myProxyClient')
63       
64    def __init__(self, app):
65        """Create WSGI app and MyProxy client attributes
66        @type app: function
67        @param app: WSGI callable for next middleware or app in the WSGI stack
68        """
69        self.__app = app
70        self.__clientEnvironKeyName = None
71   
72    def _getClientEnvironKeyName(self):
73        """Get MyProxyClient environ key name
74       
75        @rtype: basestring
76        @return: MyProxyClient environ key name
77        """
78        return self.__clientEnvironKeyName
79
80    def _setClientEnvironKeyName(self, value):
81        """Set MyProxyClient environ key name
82       
83        @type value: basestring
84        @param value: MyProxyClient environ key name
85        """
86        if not isinstance(value, basestring):
87            raise TypeError('Expecting string type for "clientEnvironKeyName"; '
88                            'got %r type' % type(value))
89        self.__clientEnvironKeyName = value
90
91    clientEnvironKeyName = property(fget=_getClientEnvironKeyName, 
92                                    fset=_setClientEnvironKeyName, 
93                                    doc="key name in environ for the "
94                                        "MyProxyClient instance") 
95   
96    @property
97    def app(self):
98        """Get Property method for reference to next WSGI application in call
99        stack
100        @rtype: function
101        @return: WSGI application
102        """
103        return self.__app
104   
105    @staticmethod
106    def getStatusMessage(statusCode):
107        '''Make a standard status message for use with start_response
108        @type statusCode: int
109        @param statusCode: HTTP status code
110        @rtype: string
111        @return: status code with standard message
112        @raise KeyError: for invalid status code
113        '''
114        return '%d %s' % (statusCode, httplib.responses[statusCode])
115       
116   
117class MyProxyClientMiddleware(MyProxyClientMiddlewareBase):
118    '''Create a MyProxy client and make it available to other middleware in the
119    WSGI stack
120   
121    @cvar LOGON_FUNC_ENV_KEYNAME_OPTNAME: ini file option name to set the key
122    name in WSGI environ dict to assign to the Logon function created by this
123    middleware
124    @type LOGON_FUNC_ENV_KEYNAME_OPTNAME: string
125   
126    @cvar DEFAULT_LOGON_FUNC_ENV_KEYNAME: default value for the key name in
127    WSGI environ dict to assign to the Logon function created by this
128    middleware
129    @type DEFAULT_LOGON_FUNC_ENV_KEYNAME: string
130   
131    @cvar CERT_REQ_POST_PARAM_KEYNAME: HTTP POST field name for the
132    certificate request posted in logon calls
133    @type CERT_REQ_POST_PARAM_KEYNAME: string
134   
135    @cvar PARAM_PREFIX: prefix for ini file option names
136    @type PARAM_PREFIX: string
137   
138    @cvar MYPROXY_CLIENT_PARAM_PREFIX: default value for ini file sub-prefix
139    used for MyProxyClient initialisation settings such as MyProxy server
140    hostname, CA cert directory etc.  The prefix is such that option names
141    will look like this e.g.
142    <PARAM_PREFIX><MYPROXY_CLIENT_PARAM_PREFIX>hostname
143    ...
144    @type MYPROXY_CLIENT_PARAM_PREFIX: string
145   
146    @ivar __myProxyClient: MyProxy client interface object to enable this
147    middleware to communicate with a backend MyProxy server using the MyProxy
148    protocol
149    @type __myProxyClient: myproxy.client.MyProxyClient
150   
151    @ivar __logonFuncEnvironKeyName:
152    @type __logonFuncEnvironKeyName: string
153    '''
154    # Options for ini file
155    LOGON_FUNC_ENV_KEYNAME_OPTNAME = 'logonFuncEnvKeyName'     
156   
157    # Default environ key names
158    DEFAULT_LOGON_FUNC_ENV_KEYNAME = ('myproxy.server.wsgi.middleware.'
159                                      'MyProxyClientMiddleware.logon')
160   
161    CERT_REQ_POST_PARAM_KEYNAME = 'certificate_request'
162   
163    # Option prefixes
164    PARAM_PREFIX = 'myproxy.'
165    MYPROXY_CLIENT_PARAM_PREFIX = 'client.'
166   
167    __slots__ = (
168        '__myProxyClient', 
169        '__logonFuncEnvironKeyName',
170    )
171   
172    def __init__(self, app):
173        '''Create attributes
174       
175        @type app: function
176        @param app: WSGI callable for next application in stack
177        '''
178        super(MyProxyClientMiddleware, self).__init__(app)
179        self.__myProxyClient = None
180        self.__logonFuncEnvironKeyName = None
181
182    @classmethod
183    def filter_app_factory(cls, app, global_conf, 
184                           prefix=PARAM_PREFIX, 
185                           myProxyClientPrefix=MYPROXY_CLIENT_PARAM_PREFIX, 
186                           **app_conf):
187        """Function following Paste filter app factory signature
188       
189        @type app: callable following WSGI interface
190        @param app: next middleware/application in the chain     
191        @type global_conf: dict       
192        @param global_conf: PasteDeploy global configuration dictionary
193        @type prefix: basestring
194        @param prefix: prefix for configuration items
195        @type myProxyClientPrefix: ini file sub-prefix used for MyProxyClient
196        initialisation settings such as MyProxy server  hostname, CA cert.
197        directory etc. 
198        @param myProxyClientPrefix: basestring
199        @type app_conf: dict       
200        @param app_conf: PasteDeploy application specific configuration
201        dictionary
202       
203        @rtype: myproxy.server.wsgi.middleware.MyProxyClientMiddleware
204        @return: an instance of this application
205        """
206        app = cls(app)
207        app.parseConfig(prefix=prefix, myProxyClientPrefix=myProxyClientPrefix,
208                        **app_conf)
209        return app
210   
211    def parseConfig(self, 
212                    prefix=PARAM_PREFIX, 
213                    myProxyClientPrefix=MYPROXY_CLIENT_PARAM_PREFIX,
214                    **app_conf):
215        """Parse dictionary of configuration items updating the relevant
216        attributes of this instance
217       
218        @type prefix: basestring
219        @param prefix: prefix for configuration items
220        @type myProxyClientPrefix: basestring
221        @param myProxyClientPrefix: explicit prefix for MyProxyClient class
222        specific configuration items
223        @type app_conf: dict       
224        @param app_conf: PasteDeploy application specific configuration
225        dictionary
226        """
227       
228        # Get MyProxyClient initialisation parameters
229        myProxyClientFullPrefix = prefix + myProxyClientPrefix
230                           
231        myProxyClientKw = dict([(k.replace(myProxyClientFullPrefix, ''), v) 
232                                 for k,v in app_conf.items() 
233                                 if k.startswith(myProxyClientFullPrefix)])
234       
235        self.myProxyClient = MyProxyClient(**myProxyClientKw)
236        clientEnvKeyOptName = prefix + \
237                            MyProxyClientMiddleware.CLIENT_ENV_KEYNAME_OPTNAME
238                   
239        self.clientEnvironKeyName = app_conf.get(clientEnvKeyOptName,
240                            MyProxyClientMiddleware.DEFAULT_CLIENT_ENV_KEYNAME)
241                   
242        logonFuncEnvKeyOptName = prefix + \
243                        MyProxyClientMiddleware.LOGON_FUNC_ENV_KEYNAME_OPTNAME
244
245        self.logonFuncEnvironKeyName = app_conf.get(logonFuncEnvKeyOptName,
246                        MyProxyClientMiddleware.DEFAULT_LOGON_FUNC_ENV_KEYNAME)
247
248    def _getLogonFuncEnvironKeyName(self):
249        """Get MyProxyClient logon function environ key name
250       
251        @rtype: basestring
252        @return: MyProxyClient logon function environ key name
253        """
254        return self.__logonFuncEnvironKeyName
255
256    def _setLogonFuncEnvironKeyName(self, value):
257        """Set MyProxyClient environ key name
258       
259        @type value: basestring
260        @param value: MyProxyClient logon function environ key name
261        """
262        if not isinstance(value, basestring):
263            raise TypeError('Expecting string type for '
264                            '"logonFuncEnvironKeyName"; got %r type' % 
265                            type(value))
266        self.__logonFuncEnvironKeyName = value
267
268    logonFuncEnvironKeyName = property(fget=_getLogonFuncEnvironKeyName, 
269                                       fset=_setLogonFuncEnvironKeyName, 
270                                       doc="key name in environ for the "
271                                           "MyProxy logon function")
272   
273    def _getMyProxyClient(self):
274        """Get MyProxyClient instance
275       
276        @rtype: myproxy.client.MyProxyClient
277        @return: MyProxyClient instance
278        """
279        return self.__myProxyClient
280
281    def _setMyProxyClient(self, value):
282        """Set MyProxyClient instance
283       
284        @type value: myproxy.client.MyProxyClient
285        @param value: MyProxyClient instance
286        """
287        if not isinstance(value, MyProxyClient):
288            raise TypeError('Expecting %r type for "myProxyClient" attribute '
289                            'got %r' % (MyProxyClient, type(value)))
290        self.__myProxyClient = value
291       
292    myProxyClient = property(fget=_getMyProxyClient,
293                             fset=_setMyProxyClient, 
294                             doc="MyProxyClient instance used to convert HTTPS"
295                                 " call into a call to a MyProxy server")
296
297    def __call__(self, environ, start_response):
298        '''Set MyProxyClient instance and MyProxy logon method in environ
299       
300        @type environ: dict
301        @param environ: WSGI environment variables dictionary
302        @type start_response: function
303        @param start_response: standard WSGI start response function
304        '''
305        log.debug("MyProxyClientMiddleware.__call__ ...")
306        environ[self.clientEnvironKeyName] = self.myProxyClient
307        environ[self.logonFuncEnvironKeyName] = self.myProxyLogon
308       
309        return self.app(environ, start_response)
310   
311    @property
312    def myProxyLogon(self):
313        """Return the MyProxy logon method wrapped as a HTTP Basic Auth
314        authenticate interface function
315       
316        @rtype: function
317        @return: MyProxy logon HTTP Basic Auth Callback
318        """
319        def _myProxylogon(environ, start_response, username, password):
320            """Wrap MyProxy logon method as a WSGI app
321            @type environ: dict
322            @param environ: WSGI environment variables dictionary
323            @type start_response: function
324            @param start_response: standard WSGI start response function
325            @type username: basestring
326            @param username: username credential to MyProxy logon
327            @type password: basestring
328            @param password: pass-phrase for MyProxy logon call
329            @raise HttpBasicAuthResponseException: invalid client request
330            @raise MyProxyClientMiddlewareError: socket error for backend
331            MyProxy server
332            """ 
333            requestMethod = environ.get('REQUEST_METHOD')             
334            if requestMethod != 'POST':
335                response = "HTTP Request method not recognised"
336                log.error("HTTP Request method %r not recognised", 
337                          requestMethod)
338                raise HttpBasicAuthResponseException(response, 
339                                                     httplib.METHOD_NOT_ALLOWED)
340           
341            request = Request(environ)
342            certReqKey = self.__class__.CERT_REQ_POST_PARAM_KEYNAME
343            pemCertReq = request.POST.get(certReqKey)
344            if pemCertReq is None:
345                response = "No %r form variable set" % certReqKey
346                log.error(response)
347                raise HttpBasicAuthResponseException(response, 
348                                                     httplib.BAD_REQUEST)
349            log.debug("cert req = %r", pemCertReq)
350           
351            # Expecting PEM encoded request
352            try:
353                certReq = crypto.load_certificate_request(crypto.FILETYPE_PEM,
354                                                          pemCertReq)
355            except crypto.Error, e:
356                log.error("Error loading input certificate request: %r", 
357                          pemCertReq)
358                raise HttpBasicAuthResponseException("Error loading input "
359                                                     "certificate request",
360                                                     httplib.BAD_REQUEST)
361               
362            # Convert to ASN1 format expect by logon client call
363            asn1CertReq = crypto.dump_certificate_request(crypto.FILETYPE_ASN1, 
364                                                          certReq)
365           
366            try:
367                credentials = self.myProxyClient.logon(username, 
368                                                       password,
369                                                       certReq=asn1CertReq)
370                status = self.getStatusMessage(httplib.OK)
371                response = '\n'.join(credentials)
372               
373                start_response(status,
374                               [('Content-length', str(len(response))),
375                                ('Content-type', 'text/plain')])
376                return [response]
377                       
378            except MyProxyClientError, e:
379                raise HttpBasicAuthResponseException(str(e),
380                                                     httplib.UNAUTHORIZED)
381            except socket.error, e:
382                raise MyProxyClientMiddlewareError("Socket error "
383                                        "with MyProxy server %r: %s" % 
384                                        (self.myProxyClient.hostname, e))
385            except Exception, e:
386                log.error("MyProxyClient.logon raised an unknown exception "
387                          "calling %r: %s", 
388                          self.myProxyClient.hostname,
389                          traceback.format_exc())
390                raise # Trigger 500 Internal Server Error
391           
392        return _myProxylogon
393   
394   
395class MyProxyGetTrustRootsMiddlewareError(Exception):
396    """MyProxyGetTrustRootsMiddleware exception class"""
397   
398   
399class MyProxyGetTrustRootsMiddleware(MyProxyClientMiddlewareBase):
400    """HTTP client interface for MyProxy server Get Trust Roots method
401   
402    It relies on a myproxy.server.wsgi.MyProxyClientMiddleware instance called
403    upstream in the WSGI stack to set up a MyProxyClient instance and make it
404    available in the environ to call its getTrustRoots method.
405   
406    @cvar PATH_OPTNAME: ini file option to set the URI path for this service
407    @type PATH_OPTNAME: string
408   
409    @cvar DEFAULT_PATH: default URI path setting
410    @type DEFAULT_PATH: string
411
412    @cvar PARAM_PREFIX: prefix for ini file option names
413    @type PARAM_PREFIX: string
414   
415    @ivar __path: URI path setting for this service
416    @type __path: basestring
417    """
418       
419    PATH_OPTNAME = 'path'     
420    DEFAULT_PATH = '/myproxy/get-trustroots'
421   
422    # Option prefixes
423    PARAM_PREFIX = 'myproxy.getTrustRoots.'
424   
425    __slots__ = (
426        '__path',
427    )
428   
429    def __init__(self, app):
430        '''Create attributes
431       
432        @type app: function
433        @param app: WSGI callable for next application in stack
434        '''
435        super(MyProxyGetTrustRootsMiddleware, self).__init__(app)
436        self.__path = None
437       
438    @classmethod
439    def filter_app_factory(cls, app, global_conf, prefix=PARAM_PREFIX, 
440                           **app_conf):
441        """Function following Paste filter app factory signature
442       
443        @type app: callable following WSGI interface
444        @param app: next middleware/application in the chain     
445        @type global_conf: dict       
446        @param global_conf: PasteDeploy global configuration dictionary
447        @type prefix: basestring
448        @param prefix: prefix for configuration items
449        @type app_conf: dict       
450        @param app_conf: PasteDeploy application specific configuration
451        dictionary
452       
453        @rtype: myproxy.server.wsgi.middleware.MyProxyGetTrustRootsMiddleware
454        @return: an instance of this middleware
455        """
456        app = cls(app)
457        app.parseConfig(prefix=prefix, **app_conf)
458        return app
459   
460    def parseConfig(self, prefix=PARAM_PREFIX, **app_conf):
461        """Parse dictionary of configuration items updating the relevant
462        attributes of this instance
463       
464        @type prefix: basestring
465        @param prefix: prefix for configuration items
466        @type app_conf: dict       
467        @param app_conf: PasteDeploy application specific configuration
468        dictionary
469        """
470        clientEnvKeyOptName = prefix + self.__class__.CLIENT_ENV_KEYNAME_OPTNAME
471                   
472        self.clientEnvironKeyName = app_conf.get(clientEnvKeyOptName,
473                                    self.__class__.DEFAULT_CLIENT_ENV_KEYNAME)
474       
475        pathOptName = prefix + self.__class__.PATH_OPTNAME
476        self.path = app_conf.get(pathOptName, self.__class__.DEFAULT_PATH)
477
478    def _getPath(self):
479        """Get URI path for get trust roots method
480        @rtype: basestring
481        @return: path for get trust roots method
482        """
483        return self.__path
484
485    def _setPath(self, value):
486        """Set URI path for get trust roots method
487        @type value: basestring
488        @param value: path for get trust roots method
489        """
490        if not isinstance(value, basestring):
491            raise TypeError('Expecting string type for "path"; got %r' % 
492                            type(value))
493       
494        self.__path = value
495
496    path = property(fget=_getPath, fset=_setPath, 
497                    doc="environ SCRIPT_NAME path which invokes the "
498                        "getTrustRoots method on this middleware")
499   
500    def __call__(self, environ, start_response):
501        '''Get MyProxyClient instance from environ and call MyProxy
502        getTrustRoots method returning the response.
503       
504        MyProxyClientMiddleware must be in place upstream in the WSGI stack
505       
506        @type environ: dict
507        @param environ: WSGI environment variables dictionary
508        @type start_response: function
509        @param start_response: standard WSGI start response function
510       
511        @rtype: list
512        @return: get trust roots response
513        '''
514        # Skip if path doesn't match
515        if environ['PATH_INFO'] != self.path:
516            return self.app(environ, start_response)
517       
518        log.debug("MyProxyGetTrustRootsMiddleware.__call__ ...")
519       
520        # Check method
521        requestMethod = environ.get('REQUEST_METHOD')             
522        if requestMethod != 'GET':
523            response = "HTTP Request method not recognised"
524            log.error("HTTP Request method %r not recognised", requestMethod)
525            status = self.__class__.getStatusMessage(httplib.BAD_REQUEST)
526            start_response(status,
527                           [('Content-type', 'text/plain'),
528                            ('Content-length', str(len(response)))])
529            return [response]
530       
531        myProxyClient = environ[self.clientEnvironKeyName]
532        if not isinstance(myProxyClient, MyProxyClient):
533            raise TypeError('Expecting %r type for "myProxyClient" environ[%r] '
534                            'attribute got %r' % (MyProxyClient, 
535                                                  self.clientEnvironKeyName,
536                                                  type(myProxyClient)))
537       
538        response = self._getTrustRoots(myProxyClient)
539        start_response(self.getStatusMessage(httplib.OK),
540                       [('Content-length', str(len(response))),
541                        ('Content-type', 'text/plain')])
542
543        return [response]
544   
545    @classmethod
546    def _getTrustRoots(cls, myProxyClient):
547        """Call getTrustRoots method on MyProxyClient instance retrieved from
548        environ and format and return a HTTP response
549       
550        @type myProxyClient: myproxy.client.MyProxyClient
551        @param myProxyClient: MyProxyClient instance on which to call
552        getTrustRoots method
553       
554        @rtype: basestring
555        @return: trust roots base64 encoded and concatenated together
556        @raise MyProxyGetTrustRootsMiddlewareError: socket error with backend
557        MyProxy server
558        @raise MyProxyClientError: error response received by MyProxyClient
559        instance
560        """
561        try:
562            trustRoots = myProxyClient.getTrustRoots()
563           
564            # Serialise dict response
565            response = "\n".join(["%s=%s" % (k, base64.b64encode(v))
566                                  for k,v in trustRoots.items()])
567           
568            return response
569                   
570        except MyProxyClientError, e:
571            log.error("MyProxyClient.getTrustRoots raised an "
572                      "MyProxyClientError exception calling %r: %s", 
573                      myProxyClient.hostname,
574                      traceback.format_exc())
575           
576        except socket.error, e:
577            raise MyProxyGetTrustRootsMiddlewareError("Socket error with "
578                                                      "MyProxy server %r: %s" % 
579                                                      (myProxyClient.hostname, 
580                                                       e))
581        except Exception, e:
582            log.error("MyProxyClient.getTrustRoots raised an unknown exception "
583                      "calling %r: %s", 
584                      myProxyClient.hostname,
585                      traceback.format_exc())
586            raise # Trigger 500 Internal Server Error
587       
Note: See TracBrowser for help on using the repository browser.