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

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