source: TI12-security/trunk/NDGSecurity/python/ndg_security_server/ndg/security/server/wsgi/authz/pep.py @ 7357

Subversion URL: http://proj.badc.rl.ac.uk/svn/ndg/TI12-security/trunk/NDGSecurity/python/ndg_security_server/ndg/security/server/wsgi/authz/pep.py@7357
Revision 7357, 13.1 KB checked in by pjkersha, 9 years ago (diff)

Incomplete - task 2: XACML-Security Integration

  • implemented caching of authorisation decision statements in the PEP to cut down on calls to authorisation service.
RevLine 
[7257]1'''NDG Security Policy Enforcement Point Module
[7164]2
[7257]3__author__ = "P J Kershaw"
4__date__ = "11/07/10"
5__copyright__ = "(C) 2010 Science and Technology Facilities Council"
6__license__ = "BSD - see LICENSE file in top-level directory"
7__contact__ = "Philip.Kershaw@stfc.ac.uk"
8__revision__ = '$Id:$'
[7164]9'''
[7257]10import logging
11log = logging.getLogger(__name__)
[7164]12
[7257]13import httplib
14from time import time
15
16import webob
17
18from ndg.saml.saml2.core import DecisionType
[7164]19from ndg.saml.saml2.binding.soap.client.authzdecisionquery import \
20                                            AuthzDecisionQuerySslSOAPBinding
[7257]21from ndg.security.server.wsgi.session import (SessionMiddlewareBase, 
22                                              SessionHandlerMiddleware)
[7357]23from ndg.security.common.credentialwallet import SAMLAuthzDecisionWallet
24from ndg.security.common.utils import str2Bool
[7164]25
[7168]26
[7287]27class SamlPepFilterConfigError(Exception):
[7164]28    """Error with SAML PEP configuration settings"""
29   
30   
[7287]31class SamlPepFilter(SessionMiddlewareBase):
[7164]32    '''Policy Enforcement Point for ESG with SAML based Interface
33   
[7168]34    @requires: ndg.security.server.wsgi.session.SessionHandlerMiddleware
35    instance upstream in the WSGI stack.
36   
37    @cvar AUTHZ_DECISION_QUERY_PARAMS_PREFIX: prefix for SAML authorisation
38    decision query options in config file
39    @type AUTHZ_DECISION_QUERY_PARAMS_PREFIX: string
40   
41    @cvar PARAM_NAMES: list of config option names
42    @type PARAM_NAMES: tuple
43   
[7164]44    @ivar __client: SAML authorisation decision query client
45    @type __client: ndg.saml.saml2.binding.soap.client.authzdecisionquery.AuthzDecisionQuerySslSOAPBinding
46    '''
47    AUTHZ_DECISION_QUERY_PARAMS_PREFIX = 'authzDecisionQuery.'
[7168]48    SESSION_KEY_PARAM_NAME = 'sessionKey'
[7357]49    CACHE_DECISIONS_PARAM_NAME = 'cacheDecisions'
[7164]50   
[7168]51    CREDENTIAL_WALLET_SESSION_KEYNAME = \
52        SessionHandlerMiddleware.CREDENTIAL_WALLET_SESSION_KEYNAME
53    USERNAME_SESSION_KEYNAME = \
54        SessionHandlerMiddleware.USERNAME_SESSION_KEYNAME
[7164]55   
[7168]56    PARAM_NAMES = (
57        'authzServiceURI',
[7357]58        'sessionKey',
59        'cacheDecisions'
[7168]60    )
61    __slots__ = (
62        '_app', '__client', '__session',
63    ) + tuple(["__%s" % i for i in PARAM_NAMES])
64    del i
65   
[7164]66    def __init__(self, app):
67        '''
68        Add reference to next WSGI middleware/app and create a SAML
69        authorisation decision query client interface
70        '''
71        self._app = app
72        self.__client = AuthzDecisionQuerySslSOAPBinding()
[7168]73        self.__session = None
[7164]74        self.__authzServiceURI = None
[7168]75        self.__sessionKey = None
[7357]76        self.__cacheDecisions = False
[7168]77
78    def _getClient(self):
79        return self.__client
80
81    def _setClient(self, value):
82        if not isinstance(value, basestring):
83            raise TypeError('Expecting string type for "client" attribute; '
84                            'got %r' % type(value))
85        self.__client = value
86
87    client = property(_getClient, _setClient, 
88                      doc="SAML authorisation decision query SOAP client")
89
90    def _getSession(self):
91        return self.__session
92
93    def _setSession(self, value):
94        self.__session = value
95
96    session = property(_getSession, _setSession, 
97                       doc="Beaker Security Session instance")
98
99    def _getAuthzServiceURI(self):
100        return self.__authzServiceURI
101
102    def _setAuthzServiceURI(self, value):
103        if not isinstance(value, basestring):
104            raise TypeError('Expecting string type for "authzServiceURI" '
105                            'attribute; got %r' % type(value))
106        self.__authzServiceURI = value
107
108    authzServiceURI = property(_getAuthzServiceURI, _setAuthzServiceURI, 
109                               doc="Authorisation Service URI")
110
111    def _getSessionKey(self):
112        return self.__sessionKey
113
114    def _setSessionKey(self, value):
115        if not isinstance(value, basestring):
116            raise TypeError('Expecting string type for "sessionKey" attribute; '
117                            'got %r' % type(value))
118        self.__sessionKey = value
119
120    sessionKey = property(_getSessionKey, _setSessionKey, 
121                          doc="environ key name for Beaker session object")
122
[7357]123    def _getCacheDecisions(self):
124        return self.__cacheDecisions
125
126    def _setCacheDecisions(self, value):
127        if isinstance(value, basestring):
128            self.__cacheDecisions = str2Bool(value)
129        elif isinstance(value, bool):
130            self.__cacheDecisions = value
131        else:
132            raise TypeError('Expecting bool/string type for "cacheDecisions" '
133                            'attribute; got %r' % type(value))
134       
135    cacheDecisions = property(_getCacheDecisions, _setCacheDecisions, 
136                              doc="Set to True to make the session cache "
137                                  "authorisation decisions returned from the "
138                                  "Authorisation Service")
139   
[7164]140    def initialise(self, prefix='', **kw):
141        '''Initialise object from keyword settings
142       
143        @type prefix: basestring
144        @param prefix: prefix for configuration items
[7168]145        @type kw: dict       
146        @param kw: configuration settings
[7164]147        dictionary
[7287]148        @raise SamlPepFilterConfigError: missing option setting(s)
[7164]149        '''
[7168]150        # Parse authorisation decision query options
[7164]151        queryPrefix = prefix + self.__class__.AUTHZ_DECISION_QUERY_PARAMS_PREFIX
[7168]152        self.client.parseKeywords(prefix=queryPrefix, **kw)
153           
154        # Parse other options
[7287]155        for name in SamlPepFilter.PARAM_NAMES:
[7168]156            paramName = prefix + name
157            value = kw.get(paramName)
158            if value is None:
[7357]159                raise SamlPepFilterConfigError('Missing option %r' % paramName)
160           
[7168]161            setattr(self, name, value)
162                   
[7164]163    @classmethod
164    def filter_app_factory(cls, app, global_conf, prefix='', **app_conf):
165        """Set-up using a Paste app factory pattern. 
166       
167        @type app: callable following WSGI interface
168        @param app: next middleware application in the chain     
169        @type global_conf: dict       
170        @param global_conf: PasteDeploy global configuration dictionary
171        @type prefix: basestring
172        @param prefix: prefix for configuration items
173        @type app_conf: dict       
174        @param app_conf: PasteDeploy application specific configuration
175        dictionary
176        """
177        app = cls(app)
178        app.initialise(prefix=prefix, **app_conf)
179       
180        return app
181               
182    def __call__(self, environ, start_response):
183        """Intercept request and call authorisation service to make an access
184        control decision
185       
186        @type environ: dict
187        @param environ: WSGI environment variables dictionary
188        @type start_response: function
189        @param start_response: standard WSGI start response function
190        @rtype: iterable
191        @return: response
192        """
[7168]193        # Get reference to session object - SessionHandler middleware must be in
194        # place upstream of this middleware in the WSGI stack
195        if self.sessionKey not in environ:
[7287]196            raise SamlPepFilterConfigError('No beaker session key "%s" found '
197                                           'in environ' % self.sessionKey)
[7168]198        self.session = environ[self.sessionKey]
199       
[7257]200        request = webob.Request(environ)
[7164]201       
[7357]202        # Check for cached decision
203        if self.cacheDecisions:
204            cachedAssertion = self._retrieveAuthzDecision(request.url)
205        else:
206            cachedAssertion = None   
207           
208        if cachedAssertion is not None:
209            assertions = (cachedAssertion,)
210        else: 
211            # No stored decision in cache, invoke the authorisation service   
212            self.client.resourceURI = request.url
213           
214            # Nb. user may not be logged in hence REMOTE_USER is not set
215            self.client.subjectID = request.remote_user or ''
216           
217            samlAuthzResponse = self.client.send(uri=self.__authzServiceURI)
218            assertions = samlAuthzResponse.assertions
219           
220            # Record the result in the user's session to enable later
221            # interrogation by any result handler Middleware
222            self.setSession(self.client.query, samlAuthzResponse)
[7168]223       
[7257]224       
225        # Set HTTP 403 Forbidden response if any of the decisions returned are
226        # deny or indeterminate status
227        failDecisions = (DecisionType.DENY, DecisionType.INDETERMINATE)
228       
[7357]229        # Review decision statement(s) in assertions and enforce the decision
230        assertion = None
231        for assertion in assertions:
[7257]232            for authzDecisionStatement in assertion.authzDecisionStatements:
233                if authzDecisionStatement.decision.value in failDecisions:
234                    response = webob.Response()
235                   
236                    if not self.client.subjectID:
237                        # Access failed and the user is not logged in
238                        response.status = httplib.UNAUTHORIZED
239                    else:
240                        # The user is logged in but not authorised
241                        response.status = httplib.FORBIDDEN
242                       
243                    response.body = 'Access denied to %r for user %r' % (
244                                                     self.client.resourceURI,
245                                                     self.client.subjectID)
246                    response.content_type = 'text/plain'
247                    log.info(response.body)
248                    return response(environ, start_response)
249
[7357]250        if assertion is None:
251            log.error("No assertions set in authorisation decision response "
252                      "from %r", self.authzServiceURI)
253           
254            response.body = ('An error occurred retrieving an access decision '
255                             'for %r for user %r' % (
256                                             self.client.resourceURI,
257                                             self.client.subjectID))
258            response.content_type = 'text/plain'
259            log.info(response.body)
260            return response(environ, start_response)     
261               
262        if self.cacheDecisions:
263            self._cacheAuthzDecision(assertion)
264           
[7257]265        # If got through to here then all is well, call next WSGI middleware/app
[7168]266        return self._app(environ, start_response)
[7164]267
[7357]268    def _retrieveAuthzDecision(self, resourceId):
269        """Return assertion containing authorisation decision for the given
270        resource ID.
271       
272        @param resourceId: search for decisions for this resource Id
273        @type resourceId: basestring
274        @return: assertion containing authorisation decision for the given
275        resource ID or None if no wallet has been set or no assertion was
276        found matching the input resource Id
277        @rtype: ndg.saml.saml2.core.Assertion / None type
278        """
279        # Get reference to wallet
280        walletKeyName = self.__class__.CREDENTIAL_WALLET_SESSION_KEYNAME
281        credWallet = self.session.get(walletKeyName)
282       
283        # Wallet has a dictionary of credential objects keyed by resource ID
284        credentials = getattr(credWallet, 'credentials', {})
285       
286        # Retrieve assertion from Credential object
287        assertion = getattr(credentials.get(resourceId), 'credential', None)
288        return assertion
289       
290       
291    def _cacheAuthzDecision(self, assertion):
292        """Cache an authorisation decision from a response retrieved from the
293        authorisation service.  This is invoked only if cacheDecisions boolean
294        is set to True
295       
296        @param assertion: SAML assertion containing authorisation decision
297        @type assertion: ndg.saml.saml2.core.Assertion
298        """
299        walletKeyName = self.__class__.CREDENTIAL_WALLET_SESSION_KEYNAME
300        credWallet = self.session.get(walletKeyName)
301        if credWallet is None:
302            credWallet = SAMLAuthzDecisionWallet()
303       
304        credWallet.addCredential(assertion)
305        self.session[walletKeyName] = credWallet
306        self.session.save()
307       
[7257]308    def setSession(self, request, response, save=True):
309        """Set PEP context information in the Beaker session using standard key
[7357]310        names.  This is a snapshot of the last request and the response
311        received.  It can be used by downstream middleware to provide contextual
312        information about authorisation decisions
[7257]313       
314        @param session: beaker session
315        @type session: beaker.session.SessionObject
316        @param request: authorisation decision query
317        @type request: ndg.saml.saml2.core.AuthzDecisionQuery
318        @param response: authorisation response
319        @type response: ndg.saml.saml2.core.Response
320        @param save: determines whether session is saved or not
321        @type save: bool
[7164]322        """
[7257]323        self.session[self.__class__.PEPCTX_SESSION_KEYNAME] = {
324            self.__class__.PEPCTX_REQUEST_SESSION_KEYNAME: request, 
325            self.__class__.PEPCTX_RESPONSE_SESSION_KEYNAME: response,
326            self.__class__.PEPCTX_TIMESTAMP_SESSION_KEYNAME: time()
327        }
328       
329        if save:
330            self.session.save()     
Note: See TracBrowser for help on using the repository browser.