source: TI12-security/trunk/NDGSecurity/python/ndg_security_common/ndg/security/common/saml_utils/bindings.py @ 6560

Subversion URL: http://proj.badc.rl.ac.uk/svn/ndg/TI12-security/trunk/NDGSecurity/python/ndg_security_common/ndg/security/common/saml_utils/bindings.py@6560
Revision 6560, 30.8 KB checked in by pjkersha, 10 years ago (diff)

Refactoring SAML SOAP bindings module to include AuthzDecisionQuery?

RevLine 
[5681]1"""SAML 2.0 bindings module implements SOAP binding for attribute query
2
3NERC DataGrid Project
4"""
5__author__ = "P J Kershaw"
6__date__ = "02/09/09"
7__copyright__ = "(C) 2009 Science and Technology Facilities Council"
8__license__ = "BSD - see LICENSE file in top-level directory"
9__contact__ = "Philip.Kershaw@stfc.ac.uk"
[6069]10__revision__ = '$Id: $'
[5741]11import logging
12log = logging.getLogger(__name__)
13
[6044]14import re
[6050]15from os import path
[6044]16from datetime import datetime, timedelta
[6050]17from uuid import uuid4
18from ConfigParser import ConfigParser
[6044]19
[5681]20from M2Crypto.m2urllib2 import HTTPSHandler
21
[6044]22from saml.common import SAMLObject
23from saml.utils import SAMLDateTime
[6050]24from saml.saml2.core import (Attribute, AttributeQuery, StatusCode, Response,
25                             Issuer, Subject, SAMLVersion, NameID)
[5681]26from saml.xml.etree import AttributeQueryElementTree, ResponseElementTree
27
[6050]28from ndg.security.common.saml_utils.esg import EsgSamlNamespaces
[6560]29from ndg.security.common.utils import TypedList, str2Bool
[6050]30from ndg.security.common.utils.configfileparsers import (
31                                                    CaseSensitiveConfigParser)
32from ndg.security.common.utils.etree import QName   
33from ndg.security.common.X509 import X500DN
34from ndg.security.common.soap import SOAPEnvelopeBase
35from ndg.security.common.soap.etree import SOAPEnvelope
36from ndg.security.common.soap.client import (UrlLib2SOAPClient, 
37                                             UrlLib2SOAPRequest)
38
[6044]39# Prevent whole module breaking if this is not available - it's only needed for
40# AttributeQuerySslSOAPBinding
41try:
42    from ndg.security.common.utils.m2crypto import SSLContextProxy
43    _sslContextProxySupport = True
44   
45except ImportError:
46    _sslContextProxySupport = False
47
[5681]48
49class SOAPBindingError(Exception):
50    '''Base exception type for client SAML SOAP Binding for Attribute Query'''
51
52
53class SOAPBindingInvalidResponse(SOAPBindingError):
54    '''Raise if the response is invalid'''
55   
56   
57_isIterable = lambda obj: getattr(obj, '__iter__', False) 
58   
[6033]59
[6034]60class SOAPBinding(object):
[6044]61    '''Client SAML SOAP Binding'''
[5681]62   
63    isIterable = staticmethod(_isIterable)
[6044]64    __slots__ = (
[6064]65        "__client",
66        "__requestEnvelopeClass",
67        "__serialise",
68        "__deserialise"
[6044]69    )
[5681]70   
71    def __init__(self, 
72                 requestEnvelopeClass=SOAPEnvelope,
73                 responseEnvelopeClass=SOAPEnvelope,
[6050]74                 serialise=AttributeQueryElementTree.toXML,
75                 deserialise=ResponseElementTree.fromXML,
[5681]76                 handlers=(HTTPSHandler,)):
[6050]77        '''Create SAML SOAP Client - Nb. serialisation functions assume
78        AttributeQuery/Response'''
[5681]79        self.__client = None
[6050]80        self.serialise = serialise
81        self.deserialise = deserialise
82       
[5681]83        self.client = UrlLib2SOAPClient()
84       
85        # ElementTree based envelope class
86        self.requestEnvelopeClass = requestEnvelopeClass
87        self.client.responseEnvelopeClass = responseEnvelopeClass
88
89        if not SOAPBinding.isIterable(handlers):
90            raise TypeError('Expecting iterable for "handlers" keyword; got %r'
91                            % type(handlers))
92           
93        for handler in handlers:
94            self.client.openerDirector.add_handler(handler())
95
96    def _getSerialise(self):
97        return self.__serialise
98
99    def _setSerialise(self, value):
100        if not callable(value):
101            raise TypeError('Expecting callable for "serialise"; got %r' % 
102                            value)
103        self.__serialise = value
104
105    serialise = property(_getSerialise, _setSerialise, 
106                         doc="callable to serialise request into XML type")
107
108    def _getDeserialise(self):
109        return self.__deserialise
110
111    def _setDeserialise(self, value):
112        if not callable(value):
113            raise TypeError('Expecting callable for "deserialise"; got %r' % 
114                            value)
115        self.__deserialise = value
116
117    deserialise = property(_getDeserialise, 
118                           _setDeserialise, 
119                           doc="callable to de-serialise response from XML "
120                               "type")
121
122    def _getRequestEnvelopeClass(self):
123        return self.__requestEnvelopeClass
124
125    def _setRequestEnvelopeClass(self, value):
126        if not issubclass(value, SOAPEnvelopeBase):
127            raise TypeError('Expecting %r for "requestEnvelopeClass"; got %r'% 
128                            (SOAPEnvelopeBase, value))
129       
130        self.__requestEnvelopeClass = value
131
132    requestEnvelopeClass = property(_getRequestEnvelopeClass, 
133                                    _setRequestEnvelopeClass, 
134                                    doc="SOAP Envelope Request Class")
135
136    def _getClient(self):
137        return self.__client
138
139    def _setClient(self, value):     
140        if not isinstance(value, UrlLib2SOAPClient):
141            raise TypeError('Expecting %r for "client"; got %r'% 
142                            (UrlLib2SOAPClient, type(value)))
143        self.__client = value
144
145    client = property(_getClient, _setClient, 
146                      doc="SOAP Client object")   
[6044]147
148    def send(self, samlObj, uri=None, request=None):
149        '''Make an request/query to a remote SAML service
[5681]150       
[6044]151        @type samlObj: saml.common.SAMLObject
152        @param samlObj: SAML query/request object
[5681]153        @type uri: basestring
154        @param uri: uri of service.  May be omitted if set from request.url
155        @type request: ndg.security.common.soap.UrlLib2SOAPRequest
156        @param request: SOAP request object to which query will be attached
157        defaults to ndg.security.common.soap.client.UrlLib2SOAPRequest
158        '''
[6044]159        if not isinstance(samlObj, SAMLObject):
[5681]160            raise TypeError('Expecting %r for input attribute query; got %r'
[6044]161                            % (SAMLObject, type(samlObj)))
[5681]162           
163        if request is None:
164            request = UrlLib2SOAPRequest()           
165            request.envelope = self.requestEnvelopeClass()
166            request.envelope.create()
167           
168        if uri is not None:
169            request.url = uri
170       
[6044]171        samlElem = self.serialise(samlObj)
[5681]172
173        # Attach query to SOAP body
[6044]174        request.envelope.body.elem.append(samlElem)
[5741]175           
[5681]176        response = self.client.send(request)
177       
178        if len(response.envelope.body.elem) != 1:
179            raise SOAPBindingInvalidResponse("Expecting single child element "
180                                             "is SOAP body")
181           
182        if QName.getLocalPart(response.envelope.body.elem[0].tag)!='Response':
183            raise SOAPBindingInvalidResponse('Expecting "Response" element in '
184                                             'SOAP body')
185           
186        response = self.deserialise(response.envelope.body.elem[0])
187       
188        return response
[6560]189
190    @classmethod
191    def fromConfig(cls, cfg, **kw):
192        '''Alternative constructor makes object from config file settings
193        @type cfg: basestring /ConfigParser derived type
194        @param cfg: configuration file path or ConfigParser type object
195        @rtype: ndg.security.common.credentialWallet.AttributeQuery
196        @return: new instance of this class
197        '''
198        obj = cls()
199        obj.parseConfig(cfg, **kw)
[6044]200       
[6560]201        return obj
202
203    def parseConfig(self, cfg, prefix='', section='DEFAULT'):
204        '''Read config file settings
205        @type cfg: basestring /ConfigParser derived type
206        @param cfg: configuration file path or ConfigParser type object
207        @type prefix: basestring
208        @param prefix: prefix for option names e.g. "attributeQuery."
209        @type section: baestring
210        @param section: configuration file section from which to extract
211        parameters.
212        ''' 
213        if isinstance(cfg, basestring):
214            cfgFilePath = path.expandvars(cfg)
215            _cfg = CaseSensitiveConfigParser()
216            _cfg.read(cfgFilePath)
217           
218        elif isinstance(cfg, ConfigParser):
219            _cfg = cfg   
220        else:
221            raise AttributeError('Expecting basestring or ConfigParser type '
222                                 'for "cfg" attribute; got %r type' % type(cfg))
223       
224        prefixLen = len(prefix)
225        for optName, val in _cfg.items(section):
226            if prefix:
227                # Filter attributes based on prefix
228                if optName.startswith(prefix):
229                    setattr(self, optName[prefixLen:], val)
230            else:
231                # No prefix set - attempt to set all attributes   
232                setattr(self, optName, val)
233       
[6044]234    def __getstate__(self):
[6064]235        '''Enable pickling for use with beaker.session'''
236        _dict = {}
237        for attrName in SOAPBinding.__slots__:
238            # Ugly hack to allow for derived classes setting private member
239            # variables
240            if attrName.startswith('__'):
241                attrName = "_SOAPBinding" + attrName
242               
243            _dict[attrName] = getattr(self, attrName)
244           
245        return _dict
[6044]246       
247    def __setstate__(self, attrDict):
248        '''Specific implementation needed with __slots__'''
249        for attr, val in attrDict.items():
250            setattr(self, attr, val)
251           
[5681]252
[6560]253class SubjectQueryResponseError(SOAPBindingInvalidResponse):
254    """SAML Response error from Subject Query"""
[6044]255    def __init__(self, *arg, **kw):
256        SOAPBindingInvalidResponse.__init__(self, *arg, **kw)
257        self.__response = None
258   
259    def _getResponse(self):
260        '''Gets the response corresponding to this error
261       
262        @return the response
263        '''
264        return self.__response
265
266    def _setResponse(self, value):
267        '''Sets the response corresponding to this error.
268       
269        @param value: the response
270        '''
271        if not isinstance(value, Response):
272            raise TypeError('"response" must be a %r, got %r' % (Response,
273                                                                 type(value)))
274        self.__response = value
275       
276    response = property(fget=_getResponse, fset=_setResponse, 
277                        doc="SAML Response associated with this exception")
278
279
[6560]280class AttributeQueryResponseError(SubjectQueryResponseError):
281    """SAML Response error from Attribute Query"""
282   
283   
284class SubjectQuerySOAPBinding(SOAPBinding): 
285    """SAML Subject Query SOAP Binding
286   
287    Nb. Assumes X.509 subject type for query issuer
288    """
289    SUBJECT_ID_OPTNAME = 'subjectID'
290    ISSUER_NAME_OPTNAME = 'issuerName'
291    CLOCK_SKEW_OPTNAME = 'clockSkewTolerance'
292    VERIFY_TIME_CONDITIONS_OPTNAME = 'verifyTimeConditions'
293   
294    CONFIG_FILE_OPTNAMES = (
295        SUBJECT_ID_OPTNAME,
296        ISSUER_NAME_OPTNAME,                 
297        CLOCK_SKEW_OPTNAME,
298        VERIFY_TIME_CONDITIONS_OPTNAME           
299    )
300   
301    __PRIVATE_ATTR_PREFIX = "__"
302    __slots__ = tuple([__PRIVATE_ATTR_PREFIX + i
303                       for i in CONFIG_FILE_OPTNAMES])
304    del i
305   
306    def __init__(self, **kw):
307        '''Create SOAP Client for SAML Attribute Query'''
308        self.__issuerName = None
309        self.__clockSkewTolerance = timedelta(seconds=0.)
310        self.__verifyTimeConditions = True
311       
312        super(SubjectQuerySOAPBinding, self).__init__(**kw)
313
314    def _getVerifyTimeConditions(self):
315        return self.__verifyTimeConditions
316
317    def _setVerifyTimeConditions(self, value):
318        if isinstance(value, bool):
319            self.__verifyTimeConditions = value
320           
321        if isinstance(value, basestring):
322            self.__verifyTimeConditions = str2Bool(value)
323        else:
324            raise TypeError('Expecting bool or string type for '
325                            '"verifyTimeConditions"; got %r instead' % 
326                            type(value))
327
328    verifyTimeConditions = property(_getVerifyTimeConditions, 
329                                    _setVerifyTimeConditions, 
330                                    doc='Set to True to verify any time '
331                                        'Conditions set in the returned '
332                                        'response assertions')
333       
334    def _getSubjectID(self):
335        return self.__subjectID
336
337    def _setSubjectID(self, value):
338        if not isinstance(value, basestring):
339            raise TypeError('Expecting string type for "subjectID"; got %r '
340                            'instead' % type(value))
341        self.__subjectID = value
342
343    subjectID = property(_getSubjectID, _setSubjectID, 
344                         doc="ID to be sent as query subject") 
345
346    def _getIssuerName(self):
347        return self.__issuerName
348
349    def _setIssuerName(self, value):
350        if not isinstance(value, basestring):
351            raise TypeError('Expecting string type for "issuerName"; '
352                            'got %r instead' % type(value))
353           
354        self.__issuerName = value
355
356    issuerName = property(_getIssuerName, _setIssuerName, 
357                        doc="Distinguished Name of issuer of SAML Attribute "
358                            "Query to Attribute Authority")
359
360    def _getClockSkewTolerance(self):
361        return self.__clockSkewTolerance
362
363    def _setClockSkewTolerance(self, value):
364        if isinstance(value, (float, int, long)):
365            self.__clockSkewTolerance = timedelta(seconds=value)
366           
367        elif isinstance(value, basestring):
368            self.__clockSkewTolerance = timedelta(seconds=float(value))
369        else:
370            raise TypeError('Expecting float, int, long or string type for '
371                            '"clockSkewTolerance"; got %r' % type(value))
372
373    clockSkewTolerance = property(fget=_getClockSkewTolerance, 
374                                  fset=_setClockSkewTolerance, 
375                                  doc="Allow a tolerance in seconds for SAML "
376                                      "Query issueInstant parameter check and "
377                                      "assertion condition notBefore and "
378                                      "notOnOrAfter times to allow for clock "
379                                      "skew") 
380
381    def _createQuery(self, queryClass=SubjectQuery):
382        """ Create a SAML SubjectQuery derived type instance
383        @param queryClass: query type to create - must be
384        saml.saml2.core.SubjectQuery type
385        @type queryClass: type
386        @return: query instance
387        @rtype: saml.saml2.core.SubjectQuery
388        """
389        if not isinstance(queryClass, SubjectQuery):
390            raise TypeError('Query class %r is not a SubjectQuery derived type'
391                            % queryClass)
392           
393        query = queryClass()
394        query.version = SAMLVersion(SAMLVersion.VERSION_20)
395        query.id = str(uuid4())
396        query.issueInstant = datetime.utcnow()
397       
398        if self.issuerName is None:
399            raise AttributeError('No issuer DN has been set for SAML Query')
400       
401        query.issuer = Issuer()
402        query.issuer.format = Issuer.X509_SUBJECT
403        query.issuer.value = self.issuerName
404                       
405        query.subject = Subject() 
406        query.subject.nameID = NameID()
407        query.subject.nameID.format = EsgSamlNamespaces.NAMEID_FORMAT
408        query.subject.nameID.value = self.subjectID
409           
410        return query
411
412    def send(self, **kw):
413        '''Make an attribute query to a remote SAML service
414       
415        @type uri: basestring
416        @param uri: uri of service.  May be omitted if set from request.url
417        @type request: ndg.security.common.soap.UrlLib2SOAPRequest
418        @param request: SOAP request object to which query will be attached
419        defaults to ndg.security.common.soap.client.UrlLib2SOAPRequest
420        '''
421        query = self._createQuery()
422           
423        response = super(SubjectQuerySOAPBinding, self).send(query, **kw)
424
425        # Perform validation
426        if response.status.statusCode.value != StatusCode.SUCCESS_URI:
427            msg = ('Return status code flagged an error.  The message is: %r' %
428                   response.status.statusMessage.value)
429            samlRespError = SubjectQueryResponseError(msg)
430            samlRespError.response = response
431            raise samlRespError
432       
433        # Check Query ID matches the query ID the service received
434        if response.inResponseTo != query.id:
435            msg = ('Response in-response-to ID %r, doesn\'t match the original '
436                   'query ID, %r' % (response.inResponseTo, query.id))
437           
438            samlRespError = SubjectQueryResponseError(msg)
439            samlRespError.response = response
440            raise samlRespError
441       
442        utcNow = datetime.utcnow() + self.clockSkewTolerance
443        if response.issueInstant > utcNow:
444            msg = ('SAML Attribute Response issueInstant [%s] is after '
445                   'the current clock time [%s]' % 
446                   (query.issueInstant, SAMLDateTime.toString(utcNow)))
447           
448            samlRespError = SubjectQueryResponseError(msg)                 
449            samlRespError.response = response
450            raise samlRespError
451       
452        for assertion in response.assertions:
453            if self.verifyTimeConditions and assertion.conditions is not None:
454                if utcNow < assertion.conditions.notBefore:           
455                    msg = ('The current clock time [%s] is before the SAML '
456                           'Attribute Response assertion conditions not before '
457                           'time [%s]' % 
458                           (SAMLDateTime.toString(utcNow),
459                            assertion.conditions.notBefore))
460                             
461                    samlRespError = SubjectQueryResponseError(msg)
462                    samlRespError.response = response
463                    raise samlRespError
464                 
465                if utcNow >= assertion.conditions.notOnOrAfter:           
466                    msg = ('The current clock time [%s] is on or after the '
467                           'SAML Attribute Response assertion conditions not '
468                           'on or after time [%s]' % 
469                           (SAMLDateTime.toString(utcNow),
470                            response.assertion.conditions.notOnOrAfter))
471                   
472                    samlRespError = SubjectQueryResponseError(msg) 
473                    samlRespError.response = response
474                    raise samlRespError   
475           
476        return response
477
478
[6044]479class AttributeQuerySOAPBinding(SOAPBinding): 
[6052]480    """SAML Attribute Query SOAP Binding
481   
482    Nb. Assumes X.509 subject type for query issuer
483    """
[6050]484    SUBJECT_ID_OPTNAME = 'subjectID'
[6062]485    ISSUER_NAME_OPTNAME = 'issuerName'
[6560]486    CLOCK_SKEW_OPTNAME = 'clockSkewTolerance'
[6044]487   
488    CONFIG_FILE_OPTNAMES = (
[6050]489        SUBJECT_ID_OPTNAME,
[6062]490        ISSUER_NAME_OPTNAME,                 
[6044]491        CLOCK_SKEW_OPTNAME           
492    )
493   
494    QUERY_ATTRIBUTES_ATTRNAME = 'queryAttributes'
495    LEN_QUERY_ATTRIBUTES_ATTRNAME = len(QUERY_ATTRIBUTES_ATTRNAME)
496    QUERY_ATTRIBUTES_PAT = re.compile(',\s*')
497   
[6064]498    __PRIVATE_ATTR_PREFIX = "__"
499    __slots__ = tuple([__PRIVATE_ATTR_PREFIX + i
500                       for i in \
501                       CONFIG_FILE_OPTNAMES + (QUERY_ATTRIBUTES_ATTRNAME,)])
[6044]502    del i
503   
504    def __init__(self, **kw):
505        '''Create SOAP Client for SAML Attribute Query'''
[6062]506        self.__issuerName = None
[6044]507        self.__queryAttributes = TypedList(Attribute)
[6560]508        self.__clockSkewTolerance = timedelta(seconds=0.)
[6044]509               
510        super(AttributeQuerySOAPBinding, self).__init__(**kw)
511
512    @classmethod
513    def fromConfig(cls, cfg, **kw):
514        '''Alternative constructor makes object from config file settings
515        @type cfg: basestring /ConfigParser derived type
516        @param cfg: configuration file path or ConfigParser type object
517        @rtype: ndg.security.common.credentialWallet.AttributeQuery
518        @return: new instance of this class
519        '''
520        obj = cls()
521        obj.parseConfig(cfg, **kw)
522       
523        return obj
524
525    def parseConfig(self, cfg, prefix='', section='DEFAULT'):
526        '''Read config file settings
527        @type cfg: basestring /ConfigParser derived type
528        @param cfg: configuration file path or ConfigParser type object
529        @type prefix: basestring
530        @param prefix: prefix for option names e.g. "attributeQuery."
531        @type section: baestring
532        @param section: configuration file section from which to extract
533        parameters.
534        ''' 
535        if isinstance(cfg, basestring):
536            cfgFilePath = path.expandvars(cfg)
537            _cfg = CaseSensitiveConfigParser()
538            _cfg.read(cfgFilePath)
539           
540        elif isinstance(cfg, ConfigParser):
541            _cfg = cfg   
542        else:
543            raise AttributeError('Expecting basestring or ConfigParser type '
[6050]544                                 'for "cfg" attribute; got %r type' % type(cfg))
[6044]545       
546        prefixLen = len(prefix)
547        for optName, val in _cfg.items(section):
[6050]548            if prefix:
549                # Filter attributes based on prefix
550                if optName.startswith(prefix):
551                    setattr(self, optName[prefixLen:], val)
552            else:
553                # No prefix set - attempt to set all attributes   
554                setattr(self, optName, val)
[6044]555           
556    def __setattr__(self, name, value):
557        """Enable setting of SAML query attribute objects via a comma separated
558        string suitable for use reading from an ini file. 
559        """
560        try:
561            super(AttributeQuerySOAPBinding, self).__setattr__(name, value)
562           
563        except AttributeError:
564            if name.startswith(
565                        AttributeQuerySOAPBinding.QUERY_ATTRIBUTES_ATTRNAME):
566                # Special handler for parsing string format settings
567                if not isinstance(value, basestring):
568                    raise TypeError('Expecting string format for special '
569                                    '%r attribute; got %r instead' %
570                                    (name, type(value)))
571                   
572                pat = AttributeQuerySOAPBinding.QUERY_ATTRIBUTES_PAT
573                attribute = Attribute()
574               
575                (attribute.name, 
576                 attribute.friendlyName, 
[6067]577                 attribute.nameFormat) = pat.split(value)
[6044]578                 
579                self.queryAttributes.append(attribute)
580            else:
581                raise
[6050]582
583    def _getSubjectID(self):
584        return self.__subjectID
585
586    def _setSubjectID(self, value):
587        if not isinstance(value, basestring):
588            raise TypeError('Expecting string type for "subjectID"; got %r '
589                            'instead' % type(value))
590        self.__subjectID = value
591
592    subjectID = property(_getSubjectID, _setSubjectID, 
593                         doc="ID to be sent as query subject") 
594             
[6044]595    def _getQueryAttributes(self):
596        """Returns a *COPY* of the attributes to avoid overwriting the
597        member variable content
598        """
599        return self.__queryAttributes
600
601    def _setQueryAttributes(self, value):
602        if not isinstance(value, TypedList) and value.elementType != Attribute:
603            raise TypeError('Expecting TypedList(Attribute) type for '
604                            '"queryAttributes"; got %r instead' % type(value)) 
605       
606        self.__queryAttributes = value
607   
608    queryAttributes = property(_getQueryAttributes, 
609                               _setQueryAttributes, 
610                               doc="List of attributes to query from the "
611                                   "Attribute Authority")
612
[6062]613    def _getIssuerName(self):
614        return self.__issuerName
[6044]615
[6062]616    def _setIssuerName(self, value):
617        if not isinstance(value, basestring):
618            raise TypeError('Expecting string type for "issuerName"; '
619                            'got %r instead' % type(value))
[6044]620           
[6062]621        self.__issuerName = value
[6044]622
[6062]623    issuerName = property(_getIssuerName, _setIssuerName, 
[6044]624                        doc="Distinguished Name of issuer of SAML Attribute "
625                            "Query to Attribute Authority")
626
[6560]627    def _getClockSkewTolerance(self):
628        return self.__clockSkewTolerance
[6044]629
[6560]630    def _setClockSkewTolerance(self, value):
[6044]631        if isinstance(value, (float, int, long)):
[6560]632            self.__clockSkewTolerance = timedelta(seconds=value)
[6044]633           
634        elif isinstance(value, basestring):
[6560]635            self.__clockSkewTolerance = timedelta(seconds=float(value))
[6044]636        else:
637            raise TypeError('Expecting float, int, long or string type for '
[6560]638                            '"clockSkewTolerance"; got %r' % type(value))
[6044]639
[6560]640    clockSkewTolerance = property(fget=_getClockSkewTolerance, 
641                         fset=_setClockSkewTolerance, 
[6044]642                         doc="Allow a clock skew in seconds for SAML Attribute"
643                             " Query issueInstant parameter check") 
[6050]644
645    def _createQuery(self):
646        """ Create a SAML attribute query"""
647        attributeQuery = AttributeQuery()
648        attributeQuery.version = SAMLVersion(SAMLVersion.VERSION_20)
649        attributeQuery.id = str(uuid4())
650        attributeQuery.issueInstant = datetime.utcnow()
651       
[6062]652        if self.issuerName is None:
[6052]653            raise AttributeError('No issuer DN has been set for SAML Attribute '
654                                 'Query')
655       
[6050]656        attributeQuery.issuer = Issuer()
657        attributeQuery.issuer.format = Issuer.X509_SUBJECT
[6062]658        attributeQuery.issuer.value = self.issuerName
[6050]659                       
660        attributeQuery.subject = Subject() 
661        attributeQuery.subject.nameID = NameID()
662        attributeQuery.subject.nameID.format = EsgSamlNamespaces.NAMEID_FORMAT
663        attributeQuery.subject.nameID.value = self.subjectID
664                 
665        # Add list of attributes to query                     
666        for attribute in self.queryAttributes:
667            attributeQuery.attributes.append(attribute)
668           
669        return attributeQuery
670
671    def send(self, **kw):
[6044]672        '''Make an attribute query to a remote SAML service
673       
674        @type uri: basestring
675        @param uri: uri of service.  May be omitted if set from request.url
676        @type request: ndg.security.common.soap.UrlLib2SOAPRequest
677        @param request: SOAP request object to which query will be attached
678        defaults to ndg.security.common.soap.client.UrlLib2SOAPRequest
679        '''
[6050]680        attributeQuery = self._createQuery()
[6044]681           
682        response = super(AttributeQuerySOAPBinding, self).send(attributeQuery, 
683                                                               **kw)
684
685        # Perform validation
686        if response.status.statusCode.value != StatusCode.SUCCESS_URI:
687            msg = ('Return status code flagged an error.  The message is: %r' %
688                   response.status.statusMessage.value)
689            samlRespError = AttributeQueryResponseError(msg)
690            samlRespError.response = response
691            raise samlRespError
692       
693        # Check Query ID matches the query ID the service received
694        if response.inResponseTo != attributeQuery.id:
695            msg = ('Response in-response-to ID %r, doesn\'t match the original '
696                   'query ID, %r' % (response.inResponseTo, attributeQuery.id))
697           
698            samlRespError = AttributeQueryResponseError(msg)
699            samlRespError.response = response
700            raise samlRespError
701       
[6560]702        utcNow = datetime.utcnow() + self.clockSkewTolerance
[6044]703        if response.issueInstant > utcNow:
704            msg = ('SAML Attribute Response issueInstant [%s] is after '
705                   'the current clock time [%s]' % 
706                   (attributeQuery.issueInstant, SAMLDateTime.toString(utcNow)))
707           
708            samlRespError = AttributeQueryResponseError(msg)                 
709            samlRespError.response = response
710            raise samlRespError
711       
712        for assertion in response.assertions:
[6560]713            if assertion.conditions is not None:
714                if utcNow < assertion.conditions.notBefore:           
715                    msg = ('The current clock time [%s] is before the SAML '
716                           'Attribute Response assertion conditions not before '
717                           'time [%s]' % 
718                           (SAMLDateTime.toString(utcNow),
719                            assertion.conditions.notBefore))
720                             
721                    samlRespError = AttributeQueryResponseError(msg)
722                    samlRespError.response = response
723                    raise samlRespError
724                 
725                if utcNow >= assertion.conditions.notOnOrAfter:           
726                    msg = ('The current clock time [%s] is on or after the '
727                           'SAML Attribute Response assertion conditions not '
728                           'on or after time [%s]' % 
729                           (SAMLDateTime.toString(utcNow),
730                            response.assertion.conditions.notOnOrAfter))
731                   
732                    samlRespError = AttributeQueryResponseError(msg) 
733                    samlRespError.response = response
734                    raise samlRespError   
[6050]735           
736        return response
[6044]737
738   
739class AttributeQuerySslSOAPBinding(AttributeQuerySOAPBinding):
740    """Specialisation of AttributeQuerySOAPbinding taking in the setting of
741    SSL parameters for mutual authentication
742    """
743    SSL_CONTEXT_PROXY_SUPPORT = _sslContextProxySupport
[6064]744    __slots__ = ('__sslCtxProxy',)
[6044]745   
746    def __init__(self, **kw):
747        if not AttributeQuerySslSOAPBinding.SSL_CONTEXT_PROXY_SUPPORT:
748            raise ImportError("ndg.security.common.utils.m2crypto import "
749                              "failed - missing M2Crypto package?")
750       
751        # Miss out default HTTPSHandler and set in send() instead
752        if 'handlers' in kw:
753            raise TypeError("__init__() got an unexpected keyword argument "
754                            "'handlers'")
755           
756        super(AttributeQuerySslSOAPBinding, self).__init__(handlers=(), **kw)
757        self.__sslCtxProxy = SSLContextProxy()
758
759    def send(self, **kw):
760        """Override base class implementation to pass explicit SSL Context
761        """
762        httpsHandler = HTTPSHandler(ssl_context=self.sslCtxProxy.createCtx())
763        self.client.openerDirector.add_handler(httpsHandler)
764        return super(AttributeQuerySslSOAPBinding, self).send(**kw)
765       
766    @property
767    def sslCtxProxy(self):
768        """SSL Context Proxy object used for setting up an SSL Context for
769        queries
770        """
771        return self.__sslCtxProxy
772           
773    def __setattr__(self, name, value):
774        """Enable setting of SSLContextProxy attributes as if they were
775        attributes of this class.  This is intended as a convenience for
776        making settings parameters read from a config file
777        """
778        try:
[6050]779            super(AttributeQuerySslSOAPBinding, self).__setattr__(name, value)
[6044]780           
781        except AttributeError:
782            # Coerce into setting SSL Context Proxy attributes
783            try:
784                setattr(self.sslCtxProxy, name, value)
785            except:
[6050]786                raise
Note: See TracBrowser for help on using the repository browser.