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

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

Incomplete - task 5: MyProxy? Logon HTTPS Interface

  • Finished epydoc ready for first release to PyPI.
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, prefix=PARAM_PREFIX, myProxyClientPrefix='',
212                    **app_conf):
213        """Parse dictionary of configuration items updating the relevant
214        attributes of this instance
215       
216        @type prefix: basestring
217        @param prefix: prefix for configuration items
218        @type myProxyClientPrefix: basestring
219        @param myProxyClientPrefix: explicit prefix for MyProxyClient class
220        specific configuration items
221        @type app_conf: dict       
222        @param app_conf: PasteDeploy application specific configuration
223        dictionary
224        """
225       
226        # Get MyProxyClient initialisation parameters
227        myProxyClientFullPrefix = prefix + myProxyClientPrefix
228                           
229        myProxyClientKw = dict([(k.replace(myProxyClientFullPrefix, ''), v) 
230                                 for k,v in app_conf.items() 
231                                 if k.startswith(myProxyClientFullPrefix)])
232       
233        self.myProxyClient = MyProxyClient(**myProxyClientKw)
234        clientEnvKeyOptName = prefix + \
235                            MyProxyClientMiddleware.CLIENT_ENV_KEYNAME_OPTNAME
236                   
237        self.clientEnvironKeyName = app_conf.get(clientEnvKeyOptName,
238                            MyProxyClientMiddleware.DEFAULT_CLIENT_ENV_KEYNAME)
239                   
240        logonFuncEnvKeyOptName = prefix + \
241                        MyProxyClientMiddleware.LOGON_FUNC_ENV_KEYNAME_OPTNAME
242
243        self.logonFuncEnvironKeyName = app_conf.get(logonFuncEnvKeyOptName,
244                        MyProxyClientMiddleware.DEFAULT_LOGON_FUNC_ENV_KEYNAME)
245
246    def _getLogonFuncEnvironKeyName(self):
247        """Get MyProxyClient logon function environ key name
248       
249        @rtype: basestring
250        @return: MyProxyClient logon function environ key name
251        """
252        return self.__logonFuncEnvironKeyName
253
254    def _setLogonFuncEnvironKeyName(self, value):
255        """Set MyProxyClient environ key name
256       
257        @type value: basestring
258        @param value: MyProxyClient logon function environ key name
259        """
260        if not isinstance(value, basestring):
261            raise TypeError('Expecting string type for '
262                            '"logonFuncEnvironKeyName"; got %r type' % 
263                            type(value))
264        self.__logonFuncEnvironKeyName = value
265
266    logonFuncEnvironKeyName = property(fget=_getLogonFuncEnvironKeyName, 
267                                       fset=_setLogonFuncEnvironKeyName, 
268                                       doc="key name in environ for the "
269                                           "MyProxy logon function")
270   
271    def _getMyProxyClient(self):
272        """Get MyProxyClient instance
273       
274        @rtype: myproxy.client.MyProxyClient
275        @return: MyProxyClient instance
276        """
277        return self.__myProxyClient
278
279    def _setMyProxyClient(self, value):
280        """Set MyProxyClient instance
281       
282        @type value: myproxy.client.MyProxyClient
283        @param value: MyProxyClient instance
284        """
285        if not isinstance(value, MyProxyClient):
286            raise TypeError('Expecting %r type for "myProxyClient" attribute '
287                            'got %r' % (MyProxyClient, type(value)))
288        self.__myProxyClient = value
289       
290    myProxyClient = property(fget=_getMyProxyClient,
291                             fset=_setMyProxyClient, 
292                             doc="MyProxyClient instance used to convert HTTPS"
293                                 " call into a call to a MyProxy server")
294
295    def __call__(self, environ, start_response):
296        '''Set MyProxyClient instance and MyProxy logon method in environ
297       
298        @type environ: dict
299        @param environ: WSGI environment variables dictionary
300        @type start_response: function
301        @param start_response: standard WSGI start response function
302        '''
303        log.debug("MyProxyClientMiddleware.__call__ ...")
304        environ[self.clientEnvironKeyName] = self.myProxyClient
305        environ[self.logonFuncEnvironKeyName] = self.myProxyLogon
306       
307        return self.app(environ, start_response)
308   
309    @property
310    def myProxyLogon(self):
311        """Return the MyProxy logon method wrapped as a HTTP Basic Auth
312        authenticate interface function
313       
314        @rtype: function
315        @return: MyProxy logon HTTP Basic Auth Callback
316        """
317        def _myProxylogon(environ, start_response, username, password):
318            """Wrap MyProxy logon method as a WSGI app
319            @type environ: dict
320            @param environ: WSGI environment variables dictionary
321            @type start_response: function
322            @param start_response: standard WSGI start response function
323            @type username: basestring
324            @param username: username credential to MyProxy logon
325            @type password: basestring
326            @param password: pass-phrase for MyProxy logon call
327            @raise HttpBasicAuthResponseException: invalid client request
328            @raise MyProxyClientMiddlewareError: socket error for backend
329            MyProxy server
330            """ 
331            requestMethod = environ.get('REQUEST_METHOD')             
332            if requestMethod != 'POST':
333                response = "HTTP Request method not recognised"
334                log.error("HTTP Request method %r not recognised", 
335                          requestMethod)
336                raise HttpBasicAuthResponseException(response, 
337                                                     httplib.METHOD_NOT_ALLOWED)
338           
339            request = Request(environ)
340            certReqKey = self.__class__.CERT_REQ_POST_PARAM_KEYNAME
341            pemCertReq = request.POST.get(certReqKey)
342            if pemCertReq is None:
343                response = "No %r form variable set" % certReqKey
344                log.error(response)
345                raise HttpBasicAuthResponseException(response, 
346                                                     httplib.BAD_REQUEST)
347            log.debug("cert req = %r", pemCertReq)
348           
349            # Expecting PEM encoded request
350            try:
351                certReq = crypto.load_certificate_request(crypto.FILETYPE_PEM,
352                                                          pemCertReq)
353            except crypto.Error, e:
354                log.error("Error loading input certificate request: %r", 
355                          pemCertReq)
356                raise HttpBasicAuthResponseException("Error loading input "
357                                                     "certificate request",
358                                                     httplib.BAD_REQUEST)
359               
360            # Convert to ASN1 format expect by logon client call
361            asn1CertReq = crypto.dump_certificate_request(crypto.FILETYPE_ASN1, 
362                                                          certReq)
363           
364            try:
365                credentials = self.myProxyClient.logon(username, 
366                                                       password,
367                                                       certReq=asn1CertReq)
368                status = self.getStatusMessage(httplib.OK)
369                response = '\n'.join(credentials)
370               
371                start_response(status,
372                               [('Content-length', str(len(response))),
373                                ('Content-type', 'text/plain')])
374                return [response]
375                       
376            except MyProxyClientError, e:
377                raise HttpBasicAuthResponseException(str(e),
378                                                     httplib.UNAUTHORIZED)
379            except socket.error, e:
380                raise MyProxyClientMiddlewareError("Socket error "
381                                        "with MyProxy server %r: %s" % 
382                                        (self.myProxyClient.hostname, e))
383            except Exception, e:
384                log.error("MyProxyClient.logon raised an unknown exception "
385                          "calling %r: %s", 
386                          self.myProxyClient.hostname,
387                          traceback.format_exc())
388                raise # Trigger 500 Internal Server Error
389           
390        return _myProxylogon
391   
392   
393class MyProxyGetTrustRootsMiddlewareError(Exception):
394    """MyProxyGetTrustRootsMiddleware exception class"""
395   
396   
397class MyProxyGetTrustRootsMiddleware(MyProxyClientMiddlewareBase):
398    """HTTP client interface for MyProxy server Get Trust Roots method
399   
400    It relies on a myproxy.server.wsgi.MyProxyClientMiddleware instance called
401    upstream in the WSGI stack to set up a MyProxyClient instance and make it
402    available in the environ to call its getTrustRoots method.
403   
404    @cvar PATH_OPTNAME: ini file option to set the URI path for this service
405    @type PATH_OPTNAME: string
406   
407    @cvar DEFAULT_PATH: default URI path setting
408    @type DEFAULT_PATH: string
409
410    @cvar PARAM_PREFIX: prefix for ini file option names
411    @type PARAM_PREFIX: string
412   
413    @ivar __path: URI path setting for this service
414    @type __path: basestring
415    """
416       
417    PATH_OPTNAME = 'path'     
418    DEFAULT_PATH = '/myproxy/get-trustroots'
419   
420    # Option prefixes
421    PARAM_PREFIX = 'myproxy.getTrustRoots.'
422   
423    __slots__ = (
424        '__path',
425    )
426   
427    def __init__(self, app):
428        '''Create attributes
429       
430        @type app: function
431        @param app: WSGI callable for next application in stack
432        '''
433        super(MyProxyGetTrustRootsMiddleware, self).__init__(app)
434        self.__path = None
435       
436    @classmethod
437    def filter_app_factory(cls, app, global_conf, prefix=PARAM_PREFIX, 
438                           **app_conf):
439        """Function following Paste filter app factory signature
440       
441        @type app: callable following WSGI interface
442        @param app: next middleware/application in the chain     
443        @type global_conf: dict       
444        @param global_conf: PasteDeploy global configuration dictionary
445        @type prefix: basestring
446        @param prefix: prefix for configuration items
447        @type app_conf: dict       
448        @param app_conf: PasteDeploy application specific configuration
449        dictionary
450       
451        @rtype: myproxy.server.wsgi.middleware.MyProxyGetTrustRootsMiddleware
452        @return: an instance of this middleware
453        """
454        app = cls(app)
455        app.parseConfig(prefix=prefix, **app_conf)
456        return app
457   
458    def parseConfig(self, prefix=PARAM_PREFIX, **app_conf):
459        """Parse dictionary of configuration items updating the relevant
460        attributes of this instance
461       
462        @type prefix: basestring
463        @param prefix: prefix for configuration items
464        @type app_conf: dict       
465        @param app_conf: PasteDeploy application specific configuration
466        dictionary
467        """
468        clientEnvKeyOptName = prefix + self.__class__.CLIENT_ENV_KEYNAME_OPTNAME
469                   
470        self.clientEnvironKeyName = app_conf.get(clientEnvKeyOptName,
471                                    self.__class__.DEFAULT_CLIENT_ENV_KEYNAME)
472       
473        pathOptName = prefix + self.__class__.PATH_OPTNAME
474        self.path = app_conf.get(pathOptName, self.__class__.DEFAULT_PATH)
475
476    def _getPath(self):
477        """Get URI path for get trust roots method
478        @rtype: basestring
479        @return: path for get trust roots method
480        """
481        return self.__path
482
483    def _setPath(self, value):
484        """Set URI path for get trust roots method
485        @type value: basestring
486        @param value: path for get trust roots method
487        """
488        if not isinstance(value, basestring):
489            raise TypeError('Expecting string type for "path"; got %r' % 
490                            type(value))
491       
492        self.__path = value
493
494    path = property(fget=_getPath, fset=_setPath, 
495                    doc="environ SCRIPT_NAME path which invokes the "
496                        "getTrustRoots method on this middleware")
497   
498    def __call__(self, environ, start_response):
499        '''Get MyProxyClient instance from environ and call MyProxy
500        getTrustRoots method returning the response.
501       
502        MyProxyClientMiddleware must be in place upstream in the WSGI stack
503       
504        @type environ: dict
505        @param environ: WSGI environment variables dictionary
506        @type start_response: function
507        @param start_response: standard WSGI start response function
508       
509        @rtype: list
510        @return: get trust roots response
511        '''
512        # Skip if path doesn't match
513        if environ['PATH_INFO'] != self.path:
514            return self.app(environ, start_response)
515       
516        log.debug("MyProxyGetTrustRootsMiddleware.__call__ ...")
517       
518        # Check method
519        requestMethod = environ.get('REQUEST_METHOD')             
520        if requestMethod != 'GET':
521            response = "HTTP Request method not recognised"
522            log.error("HTTP Request method %r not recognised", requestMethod)
523            status = self.__class__.getStatusMessage(httplib.BAD_REQUEST)
524            start_response(status,
525                           [('Content-type', 'text/plain'),
526                            ('Content-length', str(len(response)))])
527            return [response]
528       
529        myProxyClient = environ[self.clientEnvironKeyName]
530        if not isinstance(myProxyClient, MyProxyClient):
531            raise TypeError('Expecting %r type for "myProxyClient" environ[%r] '
532                            'attribute got %r' % (MyProxyClient, 
533                                                  self.clientEnvironKeyName,
534                                                  type(myProxyClient)))
535       
536        response = self._getTrustRoots(myProxyClient)
537        start_response(self.getStatusMessage(httplib.OK),
538                       [('Content-length', str(len(response))),
539                        ('Content-type', 'text/plain')])
540
541        return [response]
542   
543    @classmethod
544    def _getTrustRoots(cls, myProxyClient):
545        """Call getTrustRoots method on MyProxyClient instance retrieved from
546        environ and format and return a HTTP response
547       
548        @type myProxyClient: myproxy.client.MyProxyClient
549        @param myProxyClient: MyProxyClient instance on which to call
550        getTrustRoots method
551       
552        @rtype: basestring
553        @return: trust roots base64 encoded and concatenated together
554        @raise MyProxyGetTrustRootsMiddlewareError: socket error with backend
555        MyProxy server
556        @raise MyProxyClientError: error response received by MyProxyClient
557        instance
558        """
559        try:
560            trustRoots = myProxyClient.getTrustRoots()
561           
562            # Serialise dict response
563            response = "\n".join(["%s=%s" % (k, base64.b64encode(v))
564                                  for k,v in trustRoots.items()])
565           
566            return response
567                   
568        except MyProxyClientError, e:
569            log.error("MyProxyClient.getTrustRoots raised an "
570                      "MyProxyClientError exception calling %r: %s", 
571                      myProxyClient.hostname,
572                      traceback.format_exc())
573           
574        except socket.error, e:
575            raise MyProxyGetTrustRootsMiddlewareError("Socket error with "
576                                                      "MyProxy server %r: %s" % 
577                                                      (myProxyClient.hostname, 
578                                                       e))
579        except Exception, e:
580            log.error("MyProxyClient.getTrustRoots raised an unknown exception "
581                      "calling %r: %s", 
582                      myProxyClient.hostname,
583                      traceback.format_exc())
584            raise # Trigger 500 Internal Server Error
585       
Note: See TracBrowser for help on using the repository browser.