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

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

Incomplete - task 2: XACML-Security Integration

  • migrating to ndg.saml and ndg.soap imports now that SAML WSGI middleware has moved to ndg.saml egg.
  • Property svn:keywords set to Id
Line 
1"""WSGI Policy Enforcement Point Package
2
3NERC DataGrid Project
4"""
5__author__ = "P J Kershaw"
6__date__ = "16/01/2009"
7__copyright__ = "(C) 2009 Science and Technology Facilities Council"
8__contact__ = "Philip.Kershaw@stfc.ac.uk"
9__revision__ = "$Id$"
10__license__ = "BSD - see LICENSE file in top-level directory"
11import logging
12log = logging.getLogger(__name__)
13
14import warnings
15from time import time
16from urlparse import urlunsplit
17from httplib import UNAUTHORIZED, FORBIDDEN
18
19from paste.cascade import Cascade
20from paste.urlparser import StaticURLParser
21from authkit.authenticate.multi import MultiHandler
22
23from ndg.security.common.utils.classfactory import importClass
24from ndg.security.common.X509 import X509Cert
25from ndg.saml.saml2.binding.soap.client.attributequery import \
26                                                AttributeQuerySslSOAPBinding
27
28from ndg.security.common.credentialwallet import SAMLCredentialWallet
29from ndg.security.server.wsgi import (NDGSecurityMiddlewareBase, 
30                                      NDGSecurityMiddlewareConfigError)
31
32from ndg.security.server.wsgi import (NDGSecurityMiddlewareBase, 
33                                      NDGSecurityMiddlewareConfigError)
34from ndg.security.server.wsgi.session import (SessionMiddlewareBase, 
35                                              SessionHandlerMiddleware)
36from ndg.security.server.wsgi.authz.result_handler import \
37    PEPResultHandlerMiddlewareBase
38from ndg.security.server.wsgi.authz.result_handler.basic import \
39    PEPResultHandlerMiddleware
40   
41from ndg.security.common.authz.pip import (PIPBase, PIPAttributeQuery,
42                                           PIPAttributeResponse)
43
44from ndg.security.common.authz import Subject
45from ndg.security.common.authz.msi import (Policy, PDP, Request, Response, 
46                                           Resource)
47
48
49class PEPFilterError(Exception):
50    """Base class for PEPFilter exception types"""
51   
52   
53class PEPFilterConfigError(PEPFilterError):
54    """Configuration related error for PEPFilter"""
55
56
57class PEPFilter(SessionMiddlewareBase):
58    """PEP (Policy Enforcement Point) WSGI Middleware.  The PEP enforces
59    access control decisions made by the PDP (Policy Decision Point).  In
60    this case, it follows the WSG middleware filter pattern and is configured
61    in a pipeline upstream of the application(s) which it protects.  if an
62    access denied decision is made, the PEP enforces this by returning a
63    403 Forbidden HTTP response without the application middleware executing
64   
65    SessionMiddlewareBase base class defines user session key and
66    isAuthenticated property
67    """
68    TRIGGER_HTTP_STATUS_CODE = str(FORBIDDEN)
69    MIDDLEWARE_ID = 'PEPFilter'
70    POLICY_PARAM_PREFIX = 'policy.'
71   
72    SESSION_KEYNAME = 'sessionKey'
73    POLICY_FILEPATH_PARAMNAME = 'filePath'
74   
75    def __init__(self, app, global_conf, prefix='', **local_conf):
76        """Initialise the PIP (Policy Information Point) and PDP (Policy
77        Decision Point).  The PDP makes access control decisions based on
78        a given policy.  The PIP manages the retrieval of user credentials on
79        behalf of the PDP
80       
81        @type app: callable following WSGI interface
82        @param app: next middleware application in the chain     
83        @type global_conf: dict       
84        @param global_conf: PasteDeploy global configuration dictionary
85        @type prefix: basestring
86        @param prefix: prefix for configuration items
87        @type local_conf: dict       
88        @param local_conf: PasteDeploy application specific configuration
89        dictionary
90       
91        """       
92        # Initialise the PDP reading in the policy
93        policyCfg = PEPFilter._filterKeywords(local_conf, 
94                                              PEPFilter.POLICY_PARAM_PREFIX)
95        self.policyFilePath = policyCfg[PEPFilter.POLICY_FILEPATH_PARAMNAME]
96        policy = Policy.Parse(policyCfg[PEPFilter.POLICY_FILEPATH_PARAMNAME])
97       
98        # Initialise the Policy Information Point to None.  This object is
99        # created and set later.  See AuthorizationMiddlewareBase.
100        self.pdp = PDP(policy, None)
101       
102        self.sessionKey = local_conf.get(PEPFilter.SESSION_KEYNAME, 
103                                         PEPFilter.propertyDefaults[
104                                                    PEPFilter.SESSION_KEYNAME])
105       
106        super(PEPFilter, self).__init__(app,
107                                        global_conf,
108                                        prefix=prefix,
109                                        **local_conf)
110
111    @NDGSecurityMiddlewareBase.initCall
112    def __call__(self, environ, start_response):
113        """
114        @type environ: dict
115        @param environ: WSGI environment variables dictionary
116        @type start_response: function
117        @param start_response: standard WSGI start response function
118        @rtype: iterable
119        @return: response
120        """
121        session = environ.get(self.sessionKey)
122        if session is None:
123            raise PEPFilterConfigError('No beaker session key "%s" found in '
124                                       'environ' % self.sessionKey)
125           
126        queryString = environ.get('QUERY_STRING', '')
127        resourceURI = urlunsplit(('', '', self.pathInfo, queryString, ''))
128       
129        # Check for a secured resource
130        matchingTargets = self._getMatchingTargets(resourceURI)
131        targetMatch = len(matchingTargets) > 0
132        if not targetMatch:
133            log.debug("PEPFilter.__call__: granting access - no matching URI "
134                      "path target was found in the policy for URI path [%s]", 
135                      resourceURI)
136            return self._app(environ, start_response)
137
138        log.debug("PEPFilter.__call__: found matching target(s):\n\n %s\n"
139                  "\nfrom policy file [%s] for URI Path=[%s]\n",
140                  '\n'.join(["RegEx=%s" % t for t in matchingTargets]), 
141                  self.policyFilePath,
142                  resourceURI)
143       
144        if not self.isAuthenticated:
145            log.info("PEPFilter.__call__: user is not authenticated - setting "
146                     "HTTP 401 response ...")
147           
148            # Set a 401 response for an authentication handler to capture
149            return self._setErrorResponse(code=UNAUTHORIZED)
150       
151        log.debug("PEPFilter.__call__: creating request to call PDP to check "
152                  "user authorisation ...")
153       
154        # Make a request object to pass to the PDP. 
155        request = Request()
156        request.subject[Subject.USERID_NS] = session['username']
157        request.resource[Resource.URI_NS] = resourceURI
158
159       
160        # Call the PDP
161        response = self.pdp.evaluate(request)       
162       
163        # Record the result in the user's session to enable later
164        # interrogation by the AuthZResultHandlerMiddleware
165        PEPFilter.setSession(session, request, response)
166       
167        if response.status == Response.DECISION_PERMIT:
168            log.info("PEPFilter.__call__: PDP granted access for URI path "
169                     "[%s] using policy [%s]", 
170                     resourceURI, 
171                     self.policyFilePath)
172           
173            return self._app(environ, start_response)
174        else:
175            log.info("PEPFilter.__call__: PDP returned a status of [%s] "
176                     "denying access for URI path [%s] using policy [%s]", 
177                     response.decisionValue2String[response.status],
178                     resourceURI,
179                     self.policyFilePath) 
180           
181            # Trigger AuthZResultHandlerMiddleware by setting a response
182            # with HTTP status code equal to the TRIGGER_HTTP_STATUS_CODE class
183            # attribute value
184            triggerStatusCode = int(PEPFilter.TRIGGER_HTTP_STATUS_CODE)
185            return self._setErrorResponse(code=triggerStatusCode)
186
187    @classmethod
188    def setSession(cls, session, request, response, save=True):
189        """Set PEP context information in the Beaker session using standard key
190        names
191       
192        @param session: beaker session
193        @type session: beaker.session.SessionObject
194        @param request: authorisation request
195        @type request: ndg.security.common.authz.msi.Request
196        @param response: authorisation response
197        @type response: ndg.security.common.authz.msi.Response
198        @param save: determines whether session is saved or not
199        @type save: bool
200        """
201        session[cls.PEPCTX_SESSION_KEYNAME] = {
202            cls.PEPCTX_REQUEST_SESSION_KEYNAME: request, 
203            cls.PEPCTX_RESPONSE_SESSION_KEYNAME: response,
204            cls.PEPCTX_TIMESTAMP_SESSION_KEYNAME: time()
205        }
206       
207        if save:
208            session.save()
209       
210    def _getMatchingTargets(self, resourceURI):
211        """This method may only be called following __call__ as __call__
212        updates the pathInfo property
213       
214        @type resourceURI: basestring
215        @param resourceURI: the URI of the requested resource
216        @rtype: list
217        @return: return list of policy target objects matching the current
218        path
219        """
220        matchingTargets = [target for target in self.pdp.policy.targets
221                           if target.regEx.match(resourceURI) is not None]
222        return matchingTargets
223
224    def multiHandlerInterceptFactory(self):
225        """Return a checker function for use with AuthKit's MultiHandler.
226        MultiHandler can be used to catch HTTP 403 Forbidden responses set by
227        an application and call middleware (AuthZResultMiddleware) to handle
228        the access denied message.
229        """
230       
231        def multiHandlerIntercept(environ, status, headers):
232            """AuthKit MultiHandler checker function to intercept
233            unauthorised response status codes from applications to be
234            protected.  This function's definition is embedded into a
235            factory method so that this function has visibility to the
236            PEPFilter object's attributes if required.
237           
238            @type environ: dict
239            @param environ: WSGI environment dictionary
240            @type status: basestring
241            @param status: HTTP response code set by application middleware
242            that this intercept function is to protect
243            @type headers: list
244            @param headers: HTTP response header content"""
245           
246            if status.startswith(PEPFilter.TRIGGER_HTTP_STATUS_CODE):
247                log.debug("PEPFilter: found [%s] status for URI path [%s]: "
248                          "invoking access denied response",
249                          PEPFilter.TRIGGER_HTTP_STATUS_CODE,
250                          environ['PATH_INFO'])
251                return True
252            else:
253                # No match - it's publicly accessible
254                log.debug("PEPFilter: the return status [%s] for this URI "
255                          "path [%s] didn't match the trigger status [%s]",
256                          status,
257                          environ['PATH_INFO'],
258                          PEPFilter.TRIGGER_HTTP_STATUS_CODE)
259                return False
260       
261        return multiHandlerIntercept
262       
263    @staticmethod
264    def _filterKeywords(conf, prefix):
265        filteredConf = {}
266        prefixLen = len(prefix)
267        for k, v in conf.items():
268            if k.startswith(prefix):
269                filteredConf[k[prefixLen:]] = conf.pop(k)
270               
271        return filteredConf
272
273    def _getPDP(self):
274        if self._pdp is None:
275            raise TypeError("PDP object has not been initialised")
276        return self._pdp
277   
278    def _setPDP(self, pdp):
279        if not isinstance(pdp, (PDP, None.__class__)):
280            raise TypeError("Expecting %s or None type for pdp; got %r" %
281                            (PDP.__class__.__name__, pdp))
282        self._pdp = pdp
283
284    pdp = property(fget=_getPDP,
285                   fset=_setPDP,
286                   doc="Policy Decision Point object makes access control "
287                       "decisions on behalf of the PEP")
288
289   
290class SamlPIPMiddlewareError(Exception):
291    """Base class for SAML based Policy Information Point WSGI middleware
292    exception types
293    """
294
295 
296class SamlPIPMiddlewareConfigError(SamlPIPMiddlewareError):
297    """Configuration related error for SAML Policy Information Point WSGI
298    middleware
299    """
300   
301
302class SamlPIPMiddleware(PIPBase, NDGSecurityMiddlewareBase):
303    '''Extend Policy Information Point to enable caching of SAML credentials in
304    a SAMLCredentialWallet object held in beaker.session
305    '''
306    ENVIRON_KEYNAME = 'ndg.security.server.wsgi.authz.SamlPIPMiddleware'
307       
308    propertyDefaults = {
309        'sessionKey': 'beaker.session.ndg.security',
310    }
311    propertyDefaults.update(NDGSecurityMiddlewareBase.propertyDefaults)
312 
313    CREDENTIAL_WALLET_SESSION_KEYNAME = \
314        SessionHandlerMiddleware.CREDENTIAL_WALLET_SESSION_KEYNAME
315    USERNAME_SESSION_KEYNAME = \
316        SessionHandlerMiddleware.USERNAME_SESSION_KEYNAME
317         
318    ATTRIBUTE_QUERY_ATTRNAME = 'attributeQuery'
319    LEN_ATTRIBUTE_QUERY_ATTRNAME = len(ATTRIBUTE_QUERY_ATTRNAME)
320         
321    def __init__(self, app, global_conf, prefix='', **local_conf):
322        '''
323        @type app: callable following WSGI interface
324        @param app: next middleware application in the chain     
325        @type global_conf: dict       
326        @param global_conf: PasteDeploy global configuration dictionary
327        @type prefix: basestring
328        @param prefix: prefix for configuration items
329        @type local_conf: dict       
330        @param local_conf: PasteDeploy application specific configuration
331        dictionary
332        '''
333        self.session = None
334        self.__attributeQueryBinding = AttributeQuerySslSOAPBinding()
335       
336        nameOffset = len(prefix)
337        for k in local_conf.keys():
338            if k.startswith(prefix):
339                val = local_conf.pop(k)
340                name = k[nameOffset:]
341                setattr(self, name, val)
342               
343        if not self.__attributeQueryBinding.issuerName:
344            issuerX509Cert = X509Cert.Read(
345                    self.__attributeQueryBinding.sslCtxProxy.sslCertFilePath)
346            self.__attributeQueryBinding.issuerName = str(issuerX509Cert.dn)
347               
348        NDGSecurityMiddlewareBase.__init__(self, app, {})
349           
350    def __setattr__(self, name, value):
351        """Enable setting of AttributeQuerySslSOAPBinding attributes from
352        names starting with attributeQuery.* / attributeQuery_*.  Addition for
353        setting these values from ini file
354        """
355
356        # Coerce into setting AttributeQuerySslSOAPBinding attributes -
357        # names must start with 'attributeQuery\W' e.g.
358        # attributeQuery.clockSkew or attributeQuery_issuerDN
359        if name.startswith(SamlPIPMiddleware.ATTRIBUTE_QUERY_ATTRNAME):
360            setattr(self.__attributeQueryBinding, 
361                    name[SamlPIPMiddleware.LEN_ATTRIBUTE_QUERY_ATTRNAME+1:], 
362                    value)
363        else:
364            super(SamlPIPMiddleware, self).__setattr__(name, value)   
365
366    @property
367    def attributeQueryBinding(self):
368        """SAML SOAP Attribute Query client binding object"""
369        return self.__attributeQueryBinding
370               
371    def __call__(self, environ, start_response):
372        """Take a copy of the session object so that it is in scope for
373        attributeQuery call and add this instance to the environ
374        so that the PEPFilter can retrieve it and pass on to the PDP
375       
376        @type environ: dict
377        @param environ: WSGI environment variables dictionary
378        @type start_response: function
379        @param start_response: standard WSGI start response function
380        @rtype: iterable
381        @return: response
382        """
383        self.session = environ.get(self.sessionKey)
384        if self.session is None:
385            raise SamlPIPMiddlewareConfigError('No beaker session key "%s" '
386                                               'found in environ' % 
387                                               self.sessionKey)
388        environ[SamlPIPMiddleware.ENVIRON_KEYNAME] = self
389       
390        return self._app(environ, start_response)
391   
392    def attributeQuery(self, attributeQuery):
393        """Query the Attribute Authority specified in the request to retrieve
394        the attributes if any corresponding to the subject
395       
396        @type attributeResponse: PIPAttributeQuery
397        @param attributeResponse:
398        @rtype: PIPAttributeResponse
399        @return: response containing the attributes retrieved from the
400        Attribute Authority"""
401        if not isinstance(attributeQuery, PIPAttributeQuery):
402            raise TypeError('Expecting %r type for input "attributeQuery"; '
403                            'got %r' % (PIPAttributeQuery, 
404                                        type(attributeQuery)))
405                           
406        attributeAuthorityURI = attributeQuery[
407                                        PIPAttributeQuery.ATTRIBUTEAUTHORITY_NS]
408       
409        log.debug("SamlPIPMiddleware: received attribute query: %r", 
410                  attributeQuery)
411               
412        # Check for a wallet in the current session - if not present, create
413        # one.  See ndg.security.server.wsgi.authn.SessionHandlerMiddleware
414        # for session keys.  The 'credentialWallet' key is deleted along with
415        # any other security keys when the user logs out
416        credentialWalletKeyName = \
417                            SamlPIPMiddleware.CREDENTIAL_WALLET_SESSION_KEYNAME
418        usernameKeyName = SamlPIPMiddleware.USERNAME_SESSION_KEYNAME
419           
420        if not credentialWalletKeyName in self.session:
421            log.debug("SamlPIPMiddleware.attributeQuery: adding a "
422                      "Credential Wallet to user session [%s] ...",
423                      self.session[usernameKeyName])
424           
425            credentialWallet = SAMLCredentialWallet()
426            credentialWallet.userId = self.session[usernameKeyName]
427           
428            self.session[credentialWalletKeyName] = credentialWallet
429            self.session.save()
430        else:   
431            # Take reference to wallet for efficiency
432            credentialWallet = self.session[credentialWalletKeyName]   
433       
434        # Check for existing credentials cached in wallet           
435        credentialItem = credentialWallet.credentialsKeyedByURI.get(
436                                                    attributeAuthorityURI)
437        if credentialItem is None:
438            # No assertion is cached - make a fresh SAML Attribute Query
439            self.attributeQueryBinding.subjectID = credentialWallet.userId
440            response = self.attributeQueryBinding.send(
441                                                    uri=attributeAuthorityURI)
442            for assertion in response.assertions:
443                credentialWallet.addCredential(assertion,
444                                   attributeAuthorityURI=attributeAuthorityURI,
445                                   verifyCredential=False)
446           
447            log.debug("SamlPIPMiddleware.attributeQuery: updating Credential "
448                      "Wallet with retrieved SAML Attribute Assertion "
449                      "for user session [%s]", self.session[usernameKeyName])
450        else:
451            log.debug("SamlPIPMiddleware.attributeQuery: retrieved existing "
452                      "SAML Attribute Assertion cached in Credential Wallet "
453                      "for user session [%s]", self.session[usernameKeyName])
454
455        attributeResponse = PIPAttributeResponse()
456        attributeResponse[Subject.ROLES_NS] = []
457       
458        # Unpack assertion attribute values and add to the response object
459        for credentialItem in credentialWallet.credentials.values():
460            for statement in credentialItem.credential.attributeStatements:
461                for attribute in statement.attributes:
462                    attributeResponse[Subject.ROLES_NS] += [
463                        attributeValue.value
464                        for attributeValue in attribute.attributeValues
465                        if attributeValue.value not in attributeResponse[
466                                                            Subject.ROLES_NS]
467                    ]
468       
469        log.debug("SamlPIPMiddleware.attributeQuery response: %r", 
470                  attributeResponse)
471       
472        return attributeResponse
473
474
475class AuthorizationMiddlewareError(Exception):
476    """Base class for AuthorizationMiddlewareBase exceptions"""
477   
478   
479class AuthorizationMiddlewareConfigError(Exception):
480    """AuthorizationMiddlewareBase configuration related exceptions"""
481 
482   
483class AuthorizationMiddlewareBase(NDGSecurityMiddlewareBase):
484    '''Virtual class - A base Handler to call Policy Enforcement Point
485    middleware to intercept requests and enforce access control decisions. 
486   
487    Extend THIS class adding the new type to any WSGI middleware chain ahead of
488    the application(s) which it is to protect.  To make an implementation for
489    this virtual class, set PIP_MIDDLEWARE_CLASS in the derived type to a
490    valid Policy Information Point Class.  Use in conjunction with
491    ndg.security.server.wsgi.authn.AuthenticationMiddleware
492    '''
493    PEP_PARAM_PREFIX = 'pep.filter.'
494    PIP_PARAM_PREFIX = 'pip.'
495    PEP_RESULT_HANDLER_PARAMNAME = "pepResultHandler"
496    PEP_RESULT_HANDLER_PARAM_PREFIX = PEP_RESULT_HANDLER_PARAMNAME + '.'
497    PEP_RESULT_HANDLER_STATIC_CONTENT_DIR_PARAMNAME = 'staticContentDir'
498   
499    class PIP_MIDDLEWARE_CLASS(object):
500        """Policy Information Point WSGI middleware abstract base,
501        implementations should retrieve user credentials to enable the PDP to
502        make access control decisions
503        """
504        def __init__(self, app, global_conf, prefix='', **local_conf): 
505            raise NotImplementedError(' '.join(
506                AuthorizationMiddlewareBase.PIP_MIDDLEWARE_CLASS.__doc__.split())
507            )
508   
509    def __init__(self, app, global_conf, prefix='', **app_conf):
510        """Set-up Policy Enforcement Point to enforce access control decisions
511        based on the URI path requested and/or the HTTP response code set by
512        application(s) to be protected.  An AuthKit MultiHandler is setup to
513        handle the latter.  PEPResultHandlerMiddleware handles the output
514        set following an access denied decision
515        @type app: callable following WSGI interface
516        @param app: next middleware application in the chain     
517        @type global_conf: dict       
518        @param global_conf: PasteDeploy global configuration dictionary
519        @type prefix: basestring
520        @param prefix: prefix for configuration items
521        @type app_conf: dict       
522        @param app_conf: PasteDeploy application specific configuration
523        dictionary
524        """
525        cls = AuthorizationMiddlewareBase
526       
527        # Allow for static content for use with PEP result handler middleware       
528        pepResultHandlerParamPrefix = prefix + \
529                                            cls.PEP_RESULT_HANDLER_PARAM_PREFIX
530        pepResultHandlerStaticContentDirParamName = \
531            pepResultHandlerParamPrefix + \
532            cls.PEP_RESULT_HANDLER_STATIC_CONTENT_DIR_PARAMNAME
533       
534        pepResultHandlerStaticContentDir = app_conf.get(
535                                    pepResultHandlerStaticContentDirParamName)
536        if pepResultHandlerStaticContentDir is not None:   
537            staticApp = StaticURLParser(pepResultHandlerStaticContentDir)
538            app = Cascade([app, staticApp], catch=(404,))
539
540        authzPrefix = prefix + cls.PEP_PARAM_PREFIX
541        pepFilter = PEPFilter(app,
542                              global_conf,
543                              prefix=authzPrefix,
544                              **app_conf)
545        pepInterceptFunc = pepFilter.multiHandlerInterceptFactory()
546       
547        # Slot in the Policy Information Point in the WSGI stack at this point
548        # so that it can take a copy of the beaker session object from environ
549        # ahead of the PDP's request to it for an Attribute Certificate
550        pipPrefix = cls.PIP_PARAM_PREFIX
551        pipFilter = self.__class__.PIP_MIDDLEWARE_CLASS(pepFilter,
552                                                        global_conf,
553                                                        prefix=pipPrefix,
554                                                        **app_conf)
555        pepFilter.pdp.pip = pipFilter
556       
557        app = MultiHandler(pipFilter)
558
559        pepResultHandlerClassName = app_conf.pop(
560                                        prefix+cls.PEP_RESULT_HANDLER_PARAMNAME, 
561                                        None)
562        if pepResultHandlerClassName is None:
563            pepResultHandler = PEPResultHandlerMiddleware
564        else:
565            pepResultHandler = importClass(pepResultHandlerClassName,
566                                    objectType=PEPResultHandlerMiddlewareBase)
567                               
568        app.add_method(PEPFilter.MIDDLEWARE_ID,
569                       pepResultHandler.filter_app_factory,
570                       global_conf,
571                       prefix=pepResultHandlerParamPrefix,
572                       **app_conf)
573       
574        app.add_checker(PEPFilter.MIDDLEWARE_ID, pepInterceptFunc)
575
576        super(AuthorizationMiddlewareBase, self).__init__(app, {})
577
578
579class SAMLAuthorizationMiddleware(AuthorizationMiddlewareBase):
580    """Implementation of AuthorizationMiddlewareBase using the SAML Policy
581    Information Point interface.  This retrieves attributes over the SOAP/SAML
582    Attribute Authority interface
583    (ndg.security.server.wsgi.saml.attributeinterface.SOAPAttributeInterfaceMiddleware) and caches
584    SAML Assertions in a
585    ndg.security.common.credentialWallet.SAMLCredentialWallet
586    """     
587    PIP_MIDDLEWARE_CLASS = SamlPIPMiddleware
Note: See TracBrowser for help on using the repository browser.