Changeset 6044


Ignore:
Timestamp:
24/11/09 17:02:57 (10 years ago)
Author:
pjkersha
Message:

Made new SAML SOAP bindings specialisations for SAML Attribute Query and query over SSL.

Location:
TI12-security/trunk/python
Files:
3 edited

Legend:

Unmodified
Added
Removed
  • TI12-security/trunk/python/ndg_security_common/ndg/security/common/credentialwallet.py

    r6043 r6044  
    17951795                             StatusCode, 
    17961796                             StatusMessage) 
    1797 from saml.xml.etree import AssertionElementTree, ResponseElementTree 
     1797from saml.xml.etree import ResponseElementTree 
    17981798    
    1799 from ndg.security.common.saml_utils.bindings import SOAPBinding as SamlSoapBinding 
     1799from ndg.security.common.saml_utils.bindings import SOAPBinding as \ 
     1800                                                                SamlSoapBinding 
    18001801from ndg.security.common.saml_utils.esg import EsgSamlNamespaces 
    18011802from ndg.security.common.X509 import X500DN 
  • TI12-security/trunk/python/ndg_security_common/ndg/security/common/saml_utils/bindings.py

    r6034 r6044  
    1212log = logging.getLogger(__name__) 
    1313 
     14import re 
     15from datetime import datetime, timedelta 
     16 
    1417from M2Crypto.m2urllib2 import HTTPSHandler 
    1518 
    16 from saml.saml2.core import Response, AttributeQuery 
     19from saml.common import SAMLObject 
     20from saml.utils import SAMLDateTime 
     21from saml.saml2.core import Attribute, AttributeQuery, StatusCode, Response 
    1722from saml.xml.etree import AttributeQueryElementTree, ResponseElementTree 
    1823 
    19 from ndg.security.common.utils.etree import QName 
     24# Prevent whole module breaking if this is not available - it's only needed for 
     25# AttributeQuerySslSOAPBinding 
     26try: 
     27    from ndg.security.common.utils.m2crypto import SSLContextProxy 
     28    _sslContextProxySupport = True 
     29     
     30except ImportError: 
     31    _sslContextProxySupport = False 
     32 
     33from ndg.security.common.utils import TypedList 
     34from ndg.security.common.utils.etree import QName    
     35from ndg.security.common.X509 import X500DN  
    2036from ndg.security.common.soap import SOAPEnvelopeBase 
    2137from ndg.security.common.soap.etree import SOAPEnvelope 
    2238from ndg.security.common.soap.client import (UrlLib2SOAPClient,  
    23     UrlLib2SOAPRequest) 
     39                                             UrlLib2SOAPRequest) 
    2440 
    2541 
     
    3652 
    3753class SOAPBinding(object): 
    38     '''Client SAML SOAP Binding for Attribute Query''' 
     54    '''Client SAML SOAP Binding''' 
    3955     
    4056    isIterable = staticmethod(_isIterable) 
     57    __slots__ = ( 
     58        "client", 
     59        "requestEnvelopeClass", 
     60        "serialise", 
     61        "deserialise" 
     62    ) 
     63    __PRIVATE_ATTR_PREFIX = '_SOAPBinding__' 
     64    __slots__ += tuple([__PRIVATE_ATTR_PREFIX + i for i in __slots__]) 
     65    del i 
    4166     
    4267    def __init__(self,  
     
    4469                 responseEnvelopeClass=SOAPEnvelope, 
    4570                 handlers=(HTTPSHandler,)): 
    46         '''Create SOAP Client''' 
     71        '''Create SAML SOAP Client''' 
    4772        self.__client = None 
    4873           
     
    5984        for handler in handlers: 
    6085            self.client.openerDirector.add_handler(handler()) 
    61              
    62         self.serialise = AttributeQueryElementTree.toXML 
    63         self.deserialise = ResponseElementTree.fromXML 
    6486 
    6587    def _getSerialise(self): 
     
    114136    client = property(_getClient, _setClient,  
    115137                      doc="SOAP Client object")    
    116           
    117     def attributeQuery(self, attributeQuery, uri=None, request=None): 
     138 
     139    def send(self, samlObj, uri=None, request=None): 
     140        '''Make an request/query to a remote SAML service 
     141         
     142        @type samlObj: saml.common.SAMLObject 
     143        @param samlObj: SAML query/request object 
     144        @type uri: basestring  
     145        @param uri: uri of service.  May be omitted if set from request.url 
     146        @type request: ndg.security.common.soap.UrlLib2SOAPRequest 
     147        @param request: SOAP request object to which query will be attached 
     148        defaults to ndg.security.common.soap.client.UrlLib2SOAPRequest 
     149        ''' 
     150        if not isinstance(samlObj, SAMLObject): 
     151            raise TypeError('Expecting %r for input attribute query; got %r' 
     152                            % (SAMLObject, type(samlObj))) 
     153             
     154        if request is None: 
     155            request = UrlLib2SOAPRequest()             
     156            request.envelope = self.requestEnvelopeClass() 
     157            request.envelope.create() 
     158             
     159        if uri is not None: 
     160            request.url = uri 
     161         
     162        samlElem = self.serialise(samlObj) 
     163 
     164        # Attach query to SOAP body 
     165        request.envelope.body.elem.append(samlElem) 
     166             
     167        response = self.client.send(request) 
     168         
     169        if len(response.envelope.body.elem) != 1: 
     170            raise SOAPBindingInvalidResponse("Expecting single child element " 
     171                                             "is SOAP body") 
     172             
     173        if QName.getLocalPart(response.envelope.body.elem[0].tag)!='Response': 
     174            raise SOAPBindingInvalidResponse('Expecting "Response" element in ' 
     175                                             'SOAP body') 
     176             
     177        response = self.deserialise(response.envelope.body.elem[0]) 
     178         
     179        return response 
     180         
     181    def __getstate__(self): 
     182        '''Specific implementation needed with __slots__''' 
     183        return dict([(attrName, getattr(self, attrName))  
     184                     for attrName in self.__class__.__slots__]) 
     185         
     186    def __setstate__(self, attrDict): 
     187        '''Specific implementation needed with __slots__''' 
     188        for attr, val in attrDict.items(): 
     189            setattr(self, attr, val) 
     190             
     191 
     192class AttributeQueryResponseError(SOAPBindingInvalidResponse): 
     193    """Attribute Authority returned a SAML Response error code""" 
     194    def __init__(self, *arg, **kw): 
     195        SOAPBindingInvalidResponse.__init__(self, *arg, **kw) 
     196        self.__response = None 
     197     
     198    def _getResponse(self): 
     199        '''Gets the response corresponding to this error 
     200         
     201        @return the response 
     202        ''' 
     203        return self.__response 
     204 
     205    def _setResponse(self, value): 
     206        '''Sets the response corresponding to this error. 
     207         
     208        @param value: the response 
     209        ''' 
     210        if not isinstance(value, Response): 
     211            raise TypeError('"response" must be a %r, got %r' % (Response, 
     212                                                                 type(value))) 
     213        self.__response = value 
     214         
     215    response = property(fget=_getResponse, fset=_setResponse,  
     216                        doc="SAML Response associated with this exception") 
     217 
     218 
     219class AttributeQuerySOAPBinding(SOAPBinding):  
     220    """SAML Attribute Query SOAP Binding""" 
     221    ISSUER_DN_OPTNAME = 'issuerDN' 
     222    CLOCK_SKEW_OPTNAME = 'clockSkew' 
     223     
     224    CONFIG_FILE_OPTNAMES = ( 
     225        ISSUER_DN_OPTNAME,                  
     226        CLOCK_SKEW_OPTNAME             
     227    ) 
     228     
     229    QUERY_ATTRIBUTES_ATTRNAME = 'queryAttributes' 
     230    LEN_QUERY_ATTRIBUTES_ATTRNAME = len(QUERY_ATTRIBUTES_ATTRNAME) 
     231    QUERY_ATTRIBUTES_PAT = re.compile(',\s*') 
     232     
     233    __slots__ = ( 
     234       QUERY_ATTRIBUTES_ATTRNAME, 
     235    ) 
     236    __slots__ += CONFIG_FILE_OPTNAMES 
     237    __PRIVATE_ATTR_PREFIX = '_AttributeQuerySOAPBinding__' 
     238    __slots__ += tuple([__PRIVATE_ATTR_PREFIX + i for i in __slots__]) 
     239    del i 
     240     
     241    def __init__(self, **kw): 
     242        '''Create SOAP Client for SAML Attribute Query''' 
     243        self.__issuerDN = None 
     244        self.__queryAttributes = TypedList(Attribute) 
     245        self.__clockSkew = timedelta(seconds=0.) 
     246                 
     247        super(AttributeQuerySOAPBinding, self).__init__(**kw) 
     248         
     249        self.serialise = AttributeQueryElementTree.toXML 
     250        self.deserialise = ResponseElementTree.fromXML 
     251 
     252    @classmethod 
     253    def fromConfig(cls, cfg, **kw): 
     254        '''Alternative constructor makes object from config file settings 
     255        @type cfg: basestring /ConfigParser derived type 
     256        @param cfg: configuration file path or ConfigParser type object 
     257        @rtype: ndg.security.common.credentialWallet.AttributeQuery 
     258        @return: new instance of this class 
     259        ''' 
     260        obj = cls() 
     261        obj.parseConfig(cfg, **kw) 
     262         
     263        return obj 
     264 
     265    def parseConfig(self, cfg, prefix='', section='DEFAULT'): 
     266        '''Read config file settings 
     267        @type cfg: basestring /ConfigParser derived type 
     268        @param cfg: configuration file path or ConfigParser type object 
     269        @type prefix: basestring 
     270        @param prefix: prefix for option names e.g. "attributeQuery." 
     271        @type section: baestring 
     272        @param section: configuration file section from which to extract 
     273        parameters. 
     274        '''   
     275        if isinstance(cfg, basestring): 
     276            cfgFilePath = path.expandvars(cfg) 
     277            _cfg = CaseSensitiveConfigParser() 
     278            _cfg.read(cfgFilePath) 
     279             
     280        elif isinstance(cfg, ConfigParser): 
     281            _cfg = cfg    
     282        else: 
     283            raise AttributeError('Expecting basestring or ConfigParser type ' 
     284                                 'for "cfg" attribute; got %r type' % type(cfg))  
     285         
     286        prefixLen = len(prefix) 
     287        for optName, val in _cfg.items(section): 
     288            if prefix and optName.startswith(prefix): 
     289                optName = optName[prefixLen:] 
     290                 
     291            setattr(self, optName, val) 
     292             
     293    def __setattr__(self, name, value): 
     294        """Enable setting of SAML query attribute objects via a comma separated 
     295        string suitable for use reading from an ini file.   
     296        """ 
     297        try: 
     298            super(AttributeQuerySOAPBinding, self).__setattr__(name, value) 
     299             
     300        except AttributeError: 
     301            if name.startswith( 
     302                        AttributeQuerySOAPBinding.QUERY_ATTRIBUTES_ATTRNAME): 
     303                # Special handler for parsing string format settings 
     304                if not isinstance(value, basestring): 
     305                    raise TypeError('Expecting string format for special ' 
     306                                    '%r attribute; got %r instead' % 
     307                                    (name, type(value))) 
     308                     
     309                pat = AttributeQuerySOAPBinding.QUERY_ATTRIBUTES_PAT 
     310                attribute = Attribute() 
     311                 
     312                (attribute.name,  
     313                 attribute.friendlyName,  
     314                 attribute.format) = pat.split(value) 
     315                  
     316                self.queryAttributes.append(attribute) 
     317            else: 
     318                raise 
     319            
     320    def _getQueryAttributes(self): 
     321        """Returns a *COPY* of the attributes to avoid overwriting the  
     322        member variable content 
     323        """ 
     324        return self.__queryAttributes 
     325 
     326    def _setQueryAttributes(self, value): 
     327        if not isinstance(value, TypedList) and value.elementType != Attribute: 
     328            raise TypeError('Expecting TypedList(Attribute) type for ' 
     329                            '"queryAttributes"; got %r instead' % type(value))  
     330         
     331        self.__queryAttributes = value 
     332     
     333    queryAttributes = property(_getQueryAttributes,  
     334                               _setQueryAttributes,  
     335                               doc="List of attributes to query from the " 
     336                                   "Attribute Authority") 
     337 
     338    def _getIssuerDN(self): 
     339        return self.__issuerDN 
     340 
     341    def _setIssuerDN(self, value): 
     342        if isinstance(value, basestring): 
     343            self.__issuerDN = X500DN.fromString(value) 
     344             
     345        elif isinstance(value, X500DN): 
     346            self.__issuerDN = value 
     347        else: 
     348            raise TypeError('Expecting string or X500DN type for "issuerDN"; ' 
     349                            'got %r instead' % type(value)) 
     350        self.__issuerDN = value 
     351 
     352    issuerDN = property(_getIssuerDN, _setIssuerDN,  
     353                        doc="Distinguished Name of issuer of SAML Attribute " 
     354                            "Query to Attribute Authority") 
     355 
     356    def _getClockSkew(self): 
     357        return self.__clockSkew 
     358 
     359    def _setClockSkew(self, value): 
     360        if isinstance(value, (float, int, long)): 
     361            self.__clockSkew = timedelta(seconds=value) 
     362             
     363        elif isinstance(value, basestring): 
     364            self.__clockSkew = timedelta(seconds=float(value)) 
     365        else: 
     366            raise TypeError('Expecting float, int, long or string type for ' 
     367                            '"clockSkew"; got %r' % type(value)) 
     368 
     369    clockSkew = property(fget=_getClockSkew,  
     370                         fset=_setClockSkew,  
     371                         doc="Allow a clock skew in seconds for SAML Attribute" 
     372                             " Query issueInstant parameter check")   
     373               
     374    def send(self, attributeQuery, **kw): 
    118375        '''Make an attribute query to a remote SAML service 
    119376         
     
    130387                            % (AttributeQuery, type(attributeQuery))) 
    131388             
    132         if request is None: 
    133             request = UrlLib2SOAPRequest()             
    134             request.envelope = self.requestEnvelopeClass() 
    135             request.envelope.create() 
    136              
    137         if uri is not None: 
    138             request.url = uri 
    139          
    140         attributeQueryElem = self.serialise(attributeQuery) 
    141  
    142         # Attach query to SOAP body 
    143         request.envelope.body.elem.append(attributeQueryElem) 
    144              
    145         response = self.client.send(request) 
    146          
    147         if len(response.envelope.body.elem) != 1: 
    148             raise SOAPBindingInvalidResponse("Expecting single child element " 
    149                                              "is SOAP body") 
    150              
    151         if QName.getLocalPart(response.envelope.body.elem[0].tag)!='Response': 
    152             raise SOAPBindingInvalidResponse('Expecting "Response" element in ' 
    153                                              'SOAP body') 
    154              
    155         response = self.deserialise(response.envelope.body.elem[0]) 
    156          
    157         return response 
    158  
     389        response = super(AttributeQuerySOAPBinding, self).send(attributeQuery,  
     390                                                               **kw) 
     391 
     392        # Perform validation 
     393        if response.status.statusCode.value != StatusCode.SUCCESS_URI: 
     394            msg = ('Return status code flagged an error.  The message is: %r' % 
     395                   response.status.statusMessage.value) 
     396            samlRespError = AttributeQueryResponseError(msg) 
     397            samlRespError.response = response 
     398            raise samlRespError 
     399         
     400        # Check Query ID matches the query ID the service received 
     401        if response.inResponseTo != attributeQuery.id: 
     402            msg = ('Response in-response-to ID %r, doesn\'t match the original ' 
     403                   'query ID, %r' % (response.inResponseTo, attributeQuery.id)) 
     404             
     405            samlRespError = AttributeQueryResponseError(msg) 
     406            samlRespError.response = response 
     407            raise samlRespError 
     408         
     409        utcNow = datetime.utcnow() + self.clockSkew 
     410        if response.issueInstant > utcNow: 
     411            msg = ('SAML Attribute Response issueInstant [%s] is after ' 
     412                   'the current clock time [%s]' %  
     413                   (attributeQuery.issueInstant, SAMLDateTime.toString(utcNow))) 
     414             
     415            samlRespError = AttributeQueryResponseError(msg)                   
     416            samlRespError.response = response 
     417            raise samlRespError 
     418         
     419        for assertion in response.assertions: 
     420            if utcNow < assertion.conditions.notBefore:             
     421                msg = ('The current clock time [%s] is before the SAML ' 
     422                       'Attribute Response assertion conditions not before ' 
     423                       'time [%s]' %  
     424                       (SAMLDateTime.toString(utcNow), 
     425                        assertion.conditions.notBefore)) 
     426                           
     427                samlRespError = AttributeQueryResponseError(msg) 
     428                samlRespError.response = response 
     429                raise samlRespError 
     430              
     431            if utcNow >= assertion.conditions.notOnOrAfter:            
     432                msg = ('The current clock time [%s] is on or after the SAML ' 
     433                       'Attribute Response assertion conditions not on or ' 
     434                       'after time [%s]' %  
     435                       (SAMLDateTime.toString(utcNow), 
     436                        response.assertion.conditions.notOnOrAfter)) 
     437                 
     438                samlRespError = AttributeQueryResponseError(msg)  
     439                samlRespError.response = response 
     440                raise samlRespError     
     441 
     442     
     443class AttributeQuerySslSOAPBinding(AttributeQuerySOAPBinding): 
     444    """Specialisation of AttributeQuerySOAPbinding taking in the setting of 
     445    SSL parameters for mutual authentication 
     446    """ 
     447    SSL_CONTEXT_PROXY_SUPPORT = _sslContextProxySupport 
     448     
     449    def __init__(self, **kw): 
     450        if not AttributeQuerySslSOAPBinding.SSL_CONTEXT_PROXY_SUPPORT: 
     451            raise ImportError("ndg.security.common.utils.m2crypto import " 
     452                              "failed - missing M2Crypto package?") 
     453         
     454        # Miss out default HTTPSHandler and set in send() instead 
     455        if 'handlers' in kw: 
     456            raise TypeError("__init__() got an unexpected keyword argument " 
     457                            "'handlers'") 
     458             
     459        super(AttributeQuerySslSOAPBinding, self).__init__(handlers=(), **kw) 
     460        self.__sslCtxProxy = SSLContextProxy() 
     461 
     462    def send(self, **kw): 
     463        """Override base class implementation to pass explicit SSL Context 
     464        """ 
     465        httpsHandler = HTTPSHandler(ssl_context=self.sslCtxProxy.createCtx()) 
     466        self.client.openerDirector.add_handler(httpsHandler) 
     467        return super(AttributeQuerySslSOAPBinding, self).send(**kw) 
     468         
     469    @property 
     470    def sslCtxProxy(self): 
     471        """SSL Context Proxy object used for setting up an SSL Context for 
     472        queries 
     473        """ 
     474        return self.__sslCtxProxy 
     475             
     476    def __setattr__(self, name, value): 
     477        """Enable setting of SSLContextProxy attributes as if they were  
     478        attributes of this class.  This is intended as a convenience for  
     479        making settings parameters read from a config file 
     480        """ 
     481        try: 
     482            super(AttributeQuerySOAPBinding, self).__setattr__(name, value) 
     483             
     484        except AttributeError: 
     485            # Coerce into setting SSL Context Proxy attributes 
     486            try: 
     487                setattr(self.sslCtxProxy, name, value) 
     488            except: 
     489                raise e 
  • TI12-security/trunk/python/ndg_security_test/ndg/security/test/unit/authz/msi/test_msi.py

    r6043 r6044  
    1515                                           PIPAttributeQuery, 
    1616                                           PIPAttributeResponse) 
     17 
    1718 
    1819class MsiBaseTestCase(BaseTestCase): 
Note: See TracChangeset for help on using the changeset viewer.