source: TI12-security/trunk/python/ndg.security.server/ndg/security/server/wsgi/__init__.py @ 4863

Subversion URL: http://proj.badc.rl.ac.uk/svn/ndg/TI12-security/trunk/python/ndg.security.server/ndg/security/server/wsgi/__init__.py@4863
Revision 4863, 11.6 KB checked in by pjkersha, 11 years ago (diff)
  • added initialisation decorator for use with derived classes if ndg.security.server.wsgi.NDGSecurityMiddlewareBase.call
  • fixed SSO Client Middleware - client interface for using Single Sign On Service from Pylons or other app
  • Added capability to pass in args to class to be instantiated from instantiateClass factory function in classfactory module
  • Modified SSO wayf template to enable login from this page if the user is at their home site - saves an extra login step
  • Added Signin interface plugin for OpenID Relying Party middleware. The plugin is itself middleware so that it can include other middleware filters such StaticURLParser to include static content used in template.
Line 
1"""WSGI Middleware components
2
3NERC Data Grid Project"""
4__author__ = "P J Kershaw"
5__date__ = "27/05/08"
6__copyright__ = "(C) 2009 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$'
10import logging
11log = logging.getLogger(__name__)
12import httplib
13
14
15class NDGSecurityMiddlewareBase(object):
16    """Base class for NDG Security Middleware classes"""
17    propertyDefaults = {
18        'mountPath': '/',
19    }
20   
21    def __init__(self, app, app_conf, prefix='', **local_conf):
22        '''Set object attributes directly from app_conf and local_conf inputs
23        @type prefix: basestring
24        @param prefix: prefix for app_conf parameters e.g. 'ndgsecurity.' -
25        enables other global configuration parameters to be filtered out
26        '''
27        self._app = app
28        self._environ = {}
29        self._pathInfo = None
30        self._path = None
31       
32        opt = self.__class__.propertyDefaults.copy()
33       
34        # If no prefix is set, there is no way to distinguish options set for
35        # this app and those applying to other applications
36        if app_conf is not None and prefix:
37            # Update from application config dictionary - filter using prefix
38            self.__class__._filterOpts(opt, app_conf, prefix=prefix)
39                       
40        # Similarly, filter keyword input                 
41        self.__class__._filterOpts(opt, local_conf, prefix=prefix)
42       
43        # Set options as object attributes
44        for name, val in opt.items():
45            if not name.startswith('_'):
46                setattr(self, name, val)
47
48    def _initCall(self, environ):
49        """Call from derived class' __call__() to set environ and path
50        attributes"""
51        self.environ = environ
52        self.setPathInfo()
53        self.setPath()
54
55    @staticmethod
56    def initCall(__call__):
57        '''Decorator to __call__ to enable convenient attribute initialisation
58        '''
59        def __call__wrapper(self, environ, start_response):
60            self._initCall(environ)
61            return __call__(self, environ, start_response)
62
63        return __call__wrapper
64
65    def _setResponse(self, 
66                     environ, 
67                     start_response, 
68                     notFoundMsg=None,
69                     notFoundMsgContentType=None):
70        if self._app:
71            return self._app(environ, start_response)
72        else:
73            return self._setErrorResponse(environ, 
74                                          start_response, 
75                                          msg=notFoundMsg,
76                                          code=404,
77                                          contentType=notFoundMsgContentType)
78           
79    def _setErrorResponse(self, environ, start_response, msg=None, code=500,
80                          contentType=None):
81        '''Convenience method to set a simple error response
82       
83        @type environ: dict
84        @param environ: standard WSGI environ parameter
85        @type start_response: builtin_function_or_method
86        @param start_response: standard WSGI callable to set the HTTP header
87        @type msg: basestring
88        @param msg: optional error message
89        @type code: int
90        @param code: standard HTTP error response code
91        @type contentType: basestring
92        @param contentType: set 'Content-type' HTTP header field - defaults to
93        'text/plain'
94        '''
95        status = '%d %s' % (code, httplib.responses[code])
96        if msg is None:
97            response = status
98        else:
99            response = msg
100       
101        if contentType is None:
102            contentType = 'text/plain'
103               
104        start_response(status,
105                       [('Content-type', contentType),
106                        ('Content-Length', str(len(response)))])
107        return response
108       
109    # Utility functions to support Paste Deploy application and filter function
110    # signatures
111    @classmethod       
112    def filter_app_factory(cls, app, app_conf, **local_conf):
113        '''Function signature for Paste Deploy filter'''
114        return cls(app, app_conf, **local_conf)
115       
116    @classmethod
117    def app_factory(cls, app_conf, **local_conf):
118        '''Function Signature for Paste Deploy app'''
119        return cls(None, app_conf, **local_conf)
120   
121    @classmethod
122    def _filterOpts(cls, opt, newOpt, prefix='', propertyDefaults=None):
123        '''Convenience utility to filter input options set in __init__ via
124        app_conf or keywords
125       
126        @type opt: dict
127        @param opt: existing options set.  These will be updated by this
128        method based on the content of newOpt
129        @type newOpt: dict
130        @param newOpt: new options to update opt with
131        @type prefix: basestring
132        @param prefix: if set, remove the given prefix from the input options
133        @raise KeyError: if an option is set that is not in the classes
134        defOpt class variable
135        '''
136        if propertyDefaults is None:
137            propertyDefaults = cls.propertyDefaults
138           
139        badOpt = []
140        for k,v in newOpt.items():
141            if prefix and k.startswith(prefix):
142                subK = k.replace(prefix, '')                   
143                filtK = '_'.join(subK.split('.')) 
144            else:
145                #filtK = k
146                continue
147                   
148            if filtK not in propertyDefaults:
149                badOpt += [k]               
150            else:
151                opt[filtK] = v
152               
153        if len(badOpt) > 0:
154            raise TypeError("Invalid input option(s) set: %s" % 
155                            (", ".join(badOpt)))
156
157    def setMountPath(self, mountPath=None, environ=None):
158        if mountPath:
159            self._mountPath = mountPath
160        else:
161            if environ is None:
162                environ = self._environ
163           
164            self._mountPath = environ.get('SCRIPT_URL')
165            if self._mountPath is None:
166                raise AttributeError("SCRIPT_URL key not set in environ: "
167                                     "'mountPath' is set to None")
168           
169        if self._mountPath != '/':
170            self._mountPath = self._mountPath.rstrip('/')
171       
172    def _getMountPath(self):
173        return self._mountPath
174   
175    mountPath = property(fget=_getMountPath,
176                        fset=setMountPath,
177                        doc="URL path as assigned to SCRIPT_URL environ key")
178
179    def setPathInfo(self, pathInfo=None, environ=None):
180        if pathInfo:
181            self._pathInfo = pathInfo
182        else:
183            if environ is None:
184                environ = self._environ
185           
186            self._pathInfo = environ['PATH_INFO']
187           
188        if self._pathInfo != '/':
189            self._pathInfo = self._pathInfo.rstrip('/')
190       
191    def _getPathInfo(self):
192        return self._pathInfo
193   
194    pathInfo = property(fget=_getPathInfo,
195                        fset=setPathInfo,
196                        doc="URL path as assigned to PATH_INFO environ key")
197
198
199    def setPath(self, path=None):
200        if path:
201            self._path = path
202        else:
203            self._path = self.mountPath.rstrip('/') + self._pathInfo
204           
205        if self._path != '/':
206            self._path = self._path.rstrip('/')
207       
208    def _getPath(self):
209        return self._path
210   
211    path = property(fget=_getPath,
212                        fset=setPath,
213                        doc="Full URL path minus domain name - equivalent to "
214                            "self.mountPath PATH_INFO environ setting")
215
216    def _setEnviron(self, environ):
217        self._environ = environ
218       
219    def _getEnviron(self):
220        return self._environ
221   
222    environ = property(fget=_getEnviron,
223                       fset=_setEnviron,
224                       doc="Copy of WSGI environ dict")
225   
226   
227class NDGSecurityPathFilter(NDGSecurityMiddlewareBase):
228    """Specialization of NDG Security Middleware to enable filtering based on
229    PATH_INFO
230   
231    B{This class must be run under Apache mod_wsgi}
232
233    - Apache SSLOptions directive StdEnvVars option must be set
234    """
235    propertyDefaults = {
236        'errorResponseCode': 401,
237        'serverName': None,
238        'pathMatchList': ['/']
239    }
240    propertyDefaults.update(NDGSecurityMiddlewareBase.propertyDefaults)
241   
242    _pathMatch = lambda self: self._pathInfo in self.pathMatchList
243    pathMatch = property(fget=_pathMatch,
244                         doc="Check for input path match to list of paths"
245                             "to which this middleware is to be applied")
246   
247    sslKeyName = 'HTTPS'
248
249    _isSSLRequest = lambda self: self.environ.get(
250                                NDGSecurityPathFilter.sslKeyName) == '1'
251    isSSLRequest = property(fget=_isSSLRequest,
252                            doc="Approximation for is an SSL request boolean "
253                                "- depends on Apache config SSLOptions "
254                                "StdEnvVars option being set")
255   
256    def __init__(self, *arg, **kw):
257        super(NDGSecurityPathFilter, self).__init__(*arg, **kw)
258        self._pathMatchList = []
259       
260    def _getPathMatchList(self):
261        return self._pathMatchList
262   
263    def _setPathMatchList(self, pathList):
264        '''
265        @type pathList: list or tuple
266        @param pathList: list of URL paths to apply this middleware
267        to. Paths are relative to the point at which this middleware is mounted
268        as set in environ['PATH_INFO']
269        '''
270        # TODO: refactor to:
271        # * enable reading of path list from a database or some other
272        # configuration source.
273        # * enable some kind of pattern matching for paths
274       
275        if isinstance(pathList, basestring):
276            # Try parsing a space separated list of file paths
277             self._pathMatchList = pathList.split()
278           
279        elif not isinstance(pathList, (list, tuple)):
280            raise TypeError('Expecting a list or tuple for "pathMatchList"')
281        else:
282            self._pathMatchList = pathList
283           
284    pathMatchList = property(fget=_getPathMatchList,
285                             fset=_setPathMatchList,
286                             doc='List of URL paths to which to apply SSL '
287                                 'client authentication')
288       
289    def _getErrorResponseCode(self):
290        """Error response code getter
291        @rtype: int
292        @return: HTTP error code set by this middleware on client cert.
293        verification error
294        """
295        return self._errorResponseCode
296           
297    def _setErrorResponseCode(self, code):
298        """Error response code setter
299        @type code: int or basestring
300        @param code: error response code set if client cert. verification
301        fails"""
302        if isinstance(code, int):
303            self._errorResponseCode = code
304        elif isinstance(code, basestring):
305            self._errorResponseCode = int(code)
306        else:
307            raise TypeError('Expecting int or string type for '
308                            '"errorResponseCode" attribute')
309           
310        if self._errorResponseCode not in httplib.responses: 
311            raise ValueError("Error response code [%d] is not recognised "
312                             "standard HTTP response code" % 
313                             self._errorResponseCode) 
314           
315    errorResponseCode = property(fget=_getErrorResponseCode,
316                                 fset=_setErrorResponseCode,
317                                 doc="Response code raised if client "
318                                     "certificate verification fails")
Note: See TracBrowser for help on using the repository browser.