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?

Line 
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"
10__revision__ = '$Id: $'
11import logging
12log = logging.getLogger(__name__)
13
14import re
15from os import path
16from datetime import datetime, timedelta
17from uuid import uuid4
18from ConfigParser import ConfigParser
19
20from M2Crypto.m2urllib2 import HTTPSHandler
21
22from saml.common import SAMLObject
23from saml.utils import SAMLDateTime
24from saml.saml2.core import (Attribute, AttributeQuery, StatusCode, Response,
25                             Issuer, Subject, SAMLVersion, NameID)
26from saml.xml.etree import AttributeQueryElementTree, ResponseElementTree
27
28from ndg.security.common.saml_utils.esg import EsgSamlNamespaces
29from ndg.security.common.utils import TypedList, str2Bool
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
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
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   
59
60class SOAPBinding(object):
61    '''Client SAML SOAP Binding'''
62   
63    isIterable = staticmethod(_isIterable)
64    __slots__ = (
65        "__client",
66        "__requestEnvelopeClass",
67        "__serialise",
68        "__deserialise"
69    )
70   
71    def __init__(self, 
72                 requestEnvelopeClass=SOAPEnvelope,
73                 responseEnvelopeClass=SOAPEnvelope,
74                 serialise=AttributeQueryElementTree.toXML,
75                 deserialise=ResponseElementTree.fromXML,
76                 handlers=(HTTPSHandler,)):
77        '''Create SAML SOAP Client - Nb. serialisation functions assume
78        AttributeQuery/Response'''
79        self.__client = None
80        self.serialise = serialise
81        self.deserialise = deserialise
82       
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")   
147
148    def send(self, samlObj, uri=None, request=None):
149        '''Make an request/query to a remote SAML service
150       
151        @type samlObj: saml.common.SAMLObject
152        @param samlObj: SAML query/request object
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        '''
159        if not isinstance(samlObj, SAMLObject):
160            raise TypeError('Expecting %r for input attribute query; got %r'
161                            % (SAMLObject, type(samlObj)))
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       
171        samlElem = self.serialise(samlObj)
172
173        # Attach query to SOAP body
174        request.envelope.body.elem.append(samlElem)
175           
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
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)
200       
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       
234    def __getstate__(self):
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
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           
252
253class SubjectQueryResponseError(SOAPBindingInvalidResponse):
254    """SAML Response error from Subject Query"""
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
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
479class AttributeQuerySOAPBinding(SOAPBinding): 
480    """SAML Attribute Query SOAP Binding
481   
482    Nb. Assumes X.509 subject type for query issuer
483    """
484    SUBJECT_ID_OPTNAME = 'subjectID'
485    ISSUER_NAME_OPTNAME = 'issuerName'
486    CLOCK_SKEW_OPTNAME = 'clockSkewTolerance'
487   
488    CONFIG_FILE_OPTNAMES = (
489        SUBJECT_ID_OPTNAME,
490        ISSUER_NAME_OPTNAME,                 
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   
498    __PRIVATE_ATTR_PREFIX = "__"
499    __slots__ = tuple([__PRIVATE_ATTR_PREFIX + i
500                       for i in \
501                       CONFIG_FILE_OPTNAMES + (QUERY_ATTRIBUTES_ATTRNAME,)])
502    del i
503   
504    def __init__(self, **kw):
505        '''Create SOAP Client for SAML Attribute Query'''
506        self.__issuerName = None
507        self.__queryAttributes = TypedList(Attribute)
508        self.__clockSkewTolerance = timedelta(seconds=0.)
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 '
544                                 'for "cfg" attribute; got %r type' % type(cfg))
545       
546        prefixLen = len(prefix)
547        for optName, val in _cfg.items(section):
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)
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, 
577                 attribute.nameFormat) = pat.split(value)
578                 
579                self.queryAttributes.append(attribute)
580            else:
581                raise
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             
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
613    def _getIssuerName(self):
614        return self.__issuerName
615
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))
620           
621        self.__issuerName = value
622
623    issuerName = property(_getIssuerName, _setIssuerName, 
624                        doc="Distinguished Name of issuer of SAML Attribute "
625                            "Query to Attribute Authority")
626
627    def _getClockSkewTolerance(self):
628        return self.__clockSkewTolerance
629
630    def _setClockSkewTolerance(self, value):
631        if isinstance(value, (float, int, long)):
632            self.__clockSkewTolerance = timedelta(seconds=value)
633           
634        elif isinstance(value, basestring):
635            self.__clockSkewTolerance = timedelta(seconds=float(value))
636        else:
637            raise TypeError('Expecting float, int, long or string type for '
638                            '"clockSkewTolerance"; got %r' % type(value))
639
640    clockSkewTolerance = property(fget=_getClockSkewTolerance, 
641                         fset=_setClockSkewTolerance, 
642                         doc="Allow a clock skew in seconds for SAML Attribute"
643                             " Query issueInstant parameter check") 
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       
652        if self.issuerName is None:
653            raise AttributeError('No issuer DN has been set for SAML Attribute '
654                                 'Query')
655       
656        attributeQuery.issuer = Issuer()
657        attributeQuery.issuer.format = Issuer.X509_SUBJECT
658        attributeQuery.issuer.value = self.issuerName
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):
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        '''
680        attributeQuery = self._createQuery()
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       
702        utcNow = datetime.utcnow() + self.clockSkewTolerance
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:
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   
735           
736        return response
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
744    __slots__ = ('__sslCtxProxy',)
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:
779            super(AttributeQuerySslSOAPBinding, self).__setattr__(name, value)
780           
781        except AttributeError:
782            # Coerce into setting SSL Context Proxy attributes
783            try:
784                setattr(self.sslCtxProxy, name, value)
785            except:
786                raise
Note: See TracBrowser for help on using the repository browser.