source: TI12-security/trunk/NDGSecurity/python/ndg_security_common/ndg/security/common/saml_utils/binding/soap/attributequery.py @ 6566

Subversion URL: http://proj.badc.rl.ac.uk/svn/ndg/TI12-security/trunk/NDGSecurity/python/ndg_security_common/ndg/security/common/saml_utils/binding/soap/attributequery.py@6566
Revision 6566, 13.4 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
14from ndg.security.common.sam_utils.binding.soap.subjectquery import (
15                                                    SubjectQuery,
16                                                    SubjectQueryResponseError)
17
18
19class AttributeQueryResponseError(SubjectQueryResponseError):
20    """SAML Response error from Attribute Query"""
21   
22
23class AttributeQuerySOAPBinding(SOAPBinding): 
24    """SAML Attribute Query SOAP Binding
25   
26    Nb. Assumes X.509 subject type for query issuer
27    """
28    SUBJECT_ID_OPTNAME = 'subjectID'
29    ISSUER_NAME_OPTNAME = 'issuerName'
30    CLOCK_SKEW_OPTNAME = 'clockSkewTolerance'
31   
32    CONFIG_FILE_OPTNAMES = (
33        SUBJECT_ID_OPTNAME,
34        ISSUER_NAME_OPTNAME,                 
35        CLOCK_SKEW_OPTNAME           
36    )
37   
38    QUERY_ATTRIBUTES_ATTRNAME = 'queryAttributes'
39    LEN_QUERY_ATTRIBUTES_ATTRNAME = len(QUERY_ATTRIBUTES_ATTRNAME)
40    QUERY_ATTRIBUTES_PAT = re.compile(',\s*')
41   
42    __PRIVATE_ATTR_PREFIX = "__"
43    __slots__ = tuple([__PRIVATE_ATTR_PREFIX + i
44                       for i in \
45                       CONFIG_FILE_OPTNAMES + (QUERY_ATTRIBUTES_ATTRNAME,)])
46    del i
47   
48    def __init__(self, **kw):
49        '''Create SOAP Client for SAML Attribute Query'''
50        self.__issuerName = None
51        self.__queryAttributes = TypedList(Attribute)
52        self.__clockSkewTolerance = timedelta(seconds=0.)
53               
54        super(AttributeQuerySOAPBinding, self).__init__(**kw)
55
56    @classmethod
57    def fromConfig(cls, cfg, **kw):
58        '''Alternative constructor makes object from config file settings
59        @type cfg: basestring /ConfigParser derived type
60        @param cfg: configuration file path or ConfigParser type object
61        @rtype: ndg.security.common.credentialWallet.AttributeQuery
62        @return: new instance of this class
63        '''
64        obj = cls()
65        obj.parseConfig(cfg, **kw)
66       
67        return obj
68
69    def parseConfig(self, cfg, prefix='', section='DEFAULT'):
70        '''Read config file settings
71        @type cfg: basestring /ConfigParser derived type
72        @param cfg: configuration file path or ConfigParser type object
73        @type prefix: basestring
74        @param prefix: prefix for option names e.g. "attributeQuery."
75        @type section: baestring
76        @param section: configuration file section from which to extract
77        parameters.
78        ''' 
79        if isinstance(cfg, basestring):
80            cfgFilePath = path.expandvars(cfg)
81            _cfg = CaseSensitiveConfigParser()
82            _cfg.read(cfgFilePath)
83           
84        elif isinstance(cfg, ConfigParser):
85            _cfg = cfg   
86        else:
87            raise AttributeError('Expecting basestring or ConfigParser type '
88                                 'for "cfg" attribute; got %r type' % type(cfg))
89       
90        prefixLen = len(prefix)
91        for optName, val in _cfg.items(section):
92            if prefix:
93                # Filter attributes based on prefix
94                if optName.startswith(prefix):
95                    setattr(self, optName[prefixLen:], val)
96            else:
97                # No prefix set - attempt to set all attributes   
98                setattr(self, optName, val)
99           
100    def __setattr__(self, name, value):
101        """Enable setting of SAML query attribute objects via a comma separated
102        string suitable for use reading from an ini file. 
103        """
104        try:
105            super(AttributeQuerySOAPBinding, self).__setattr__(name, value)
106           
107        except AttributeError:
108            if name.startswith(
109                        AttributeQuerySOAPBinding.QUERY_ATTRIBUTES_ATTRNAME):
110                # Special handler for parsing string format settings
111                if not isinstance(value, basestring):
112                    raise TypeError('Expecting string format for special '
113                                    '%r attribute; got %r instead' %
114                                    (name, type(value)))
115                   
116                pat = AttributeQuerySOAPBinding.QUERY_ATTRIBUTES_PAT
117                attribute = Attribute()
118               
119                (attribute.name, 
120                 attribute.friendlyName, 
121                 attribute.nameFormat) = pat.split(value)
122                 
123                self.queryAttributes.append(attribute)
124            else:
125                raise
126
127    def _getSubjectID(self):
128        return self.__subjectID
129
130    def _setSubjectID(self, value):
131        if not isinstance(value, basestring):
132            raise TypeError('Expecting string type for "subjectID"; got %r '
133                            'instead' % type(value))
134        self.__subjectID = value
135
136    subjectID = property(_getSubjectID, _setSubjectID, 
137                         doc="ID to be sent as query subject") 
138             
139    def _getQueryAttributes(self):
140        """Returns a *COPY* of the attributes to avoid overwriting the
141        member variable content
142        """
143        return self.__queryAttributes
144
145    def _setQueryAttributes(self, value):
146        if not isinstance(value, TypedList) and value.elementType != Attribute:
147            raise TypeError('Expecting TypedList(Attribute) type for '
148                            '"queryAttributes"; got %r instead' % type(value)) 
149       
150        self.__queryAttributes = value
151   
152    queryAttributes = property(_getQueryAttributes, 
153                               _setQueryAttributes, 
154                               doc="List of attributes to query from the "
155                                   "Attribute Authority")
156
157    def _getIssuerName(self):
158        return self.__issuerName
159
160    def _setIssuerName(self, value):
161        if not isinstance(value, basestring):
162            raise TypeError('Expecting string type for "issuerName"; '
163                            'got %r instead' % type(value))
164           
165        self.__issuerName = value
166
167    issuerName = property(_getIssuerName, _setIssuerName, 
168                        doc="Distinguished Name of issuer of SAML Attribute "
169                            "Query to Attribute Authority")
170
171    def _getClockSkewTolerance(self):
172        return self.__clockSkewTolerance
173
174    def _setClockSkewTolerance(self, value):
175        if isinstance(value, (float, int, long)):
176            self.__clockSkewTolerance = timedelta(seconds=value)
177           
178        elif isinstance(value, basestring):
179            self.__clockSkewTolerance = timedelta(seconds=float(value))
180        else:
181            raise TypeError('Expecting float, int, long or string type for '
182                            '"clockSkewTolerance"; got %r' % type(value))
183
184    clockSkewTolerance = property(fget=_getClockSkewTolerance, 
185                         fset=_setClockSkewTolerance, 
186                         doc="Allow a clock skew in seconds for SAML Attribute"
187                             " Query issueInstant parameter check") 
188
189    def _createQuery(self):
190        """ Create a SAML attribute query"""
191        attributeQuery = AttributeQuery()
192        attributeQuery.version = SAMLVersion(SAMLVersion.VERSION_20)
193        attributeQuery.id = str(uuid4())
194        attributeQuery.issueInstant = datetime.utcnow()
195       
196        if self.issuerName is None:
197            raise AttributeError('No issuer DN has been set for SAML Attribute '
198                                 'Query')
199       
200        attributeQuery.issuer = Issuer()
201        attributeQuery.issuer.format = Issuer.X509_SUBJECT
202        attributeQuery.issuer.value = self.issuerName
203                       
204        attributeQuery.subject = Subject() 
205        attributeQuery.subject.nameID = NameID()
206        attributeQuery.subject.nameID.format = EsgSamlNamespaces.NAMEID_FORMAT
207        attributeQuery.subject.nameID.value = self.subjectID
208                 
209        # Add list of attributes to query                     
210        for attribute in self.queryAttributes:
211            attributeQuery.attributes.append(attribute)
212           
213        return attributeQuery
214
215    def send(self, **kw):
216        '''Make an attribute query to a remote SAML service
217       
218        @type uri: basestring
219        @param uri: uri of service.  May be omitted if set from request.url
220        @type request: ndg.security.common.soap.UrlLib2SOAPRequest
221        @param request: SOAP request object to which query will be attached
222        defaults to ndg.security.common.soap.client.UrlLib2SOAPRequest
223        '''
224        attributeQuery = self._createQuery()
225           
226        response = super(AttributeQuerySOAPBinding, self).send(attributeQuery, 
227                                                               **kw)
228
229        # Perform validation
230        if response.status.statusCode.value != StatusCode.SUCCESS_URI:
231            msg = ('Return status code flagged an error.  The message is: %r' %
232                   response.status.statusMessage.value)
233            samlRespError = AttributeQueryResponseError(msg)
234            samlRespError.response = response
235            raise samlRespError
236       
237        # Check Query ID matches the query ID the service received
238        if response.inResponseTo != attributeQuery.id:
239            msg = ('Response in-response-to ID %r, doesn\'t match the original '
240                   'query ID, %r' % (response.inResponseTo, attributeQuery.id))
241           
242            samlRespError = AttributeQueryResponseError(msg)
243            samlRespError.response = response
244            raise samlRespError
245       
246        utcNow = datetime.utcnow() + self.clockSkewTolerance
247        if response.issueInstant > utcNow:
248            msg = ('SAML Attribute Response issueInstant [%s] is after '
249                   'the current clock time [%s]' % 
250                   (attributeQuery.issueInstant, SAMLDateTime.toString(utcNow)))
251           
252            samlRespError = AttributeQueryResponseError(msg)                 
253            samlRespError.response = response
254            raise samlRespError
255       
256        for assertion in response.assertions:
257            if assertion.conditions is not None:
258                if utcNow < assertion.conditions.notBefore:           
259                    msg = ('The current clock time [%s] is before the SAML '
260                           'Attribute Response assertion conditions not before '
261                           'time [%s]' % 
262                           (SAMLDateTime.toString(utcNow),
263                            assertion.conditions.notBefore))
264                             
265                    samlRespError = AttributeQueryResponseError(msg)
266                    samlRespError.response = response
267                    raise samlRespError
268                 
269                if utcNow >= assertion.conditions.notOnOrAfter:           
270                    msg = ('The current clock time [%s] is on or after the '
271                           'SAML Attribute Response assertion conditions not '
272                           'on or after time [%s]' % 
273                           (SAMLDateTime.toString(utcNow),
274                            response.assertion.conditions.notOnOrAfter))
275                   
276                    samlRespError = AttributeQueryResponseError(msg) 
277                    samlRespError.response = response
278                    raise samlRespError   
279           
280        return response
281
282   
283class AttributeQuerySslSOAPBinding(AttributeQuerySOAPBinding):
284    """Specialisation of AttributeQuerySOAPbinding taking in the setting of
285    SSL parameters for mutual authentication
286    """
287    SSL_CONTEXT_PROXY_SUPPORT = _sslContextProxySupport
288    __slots__ = ('__sslCtxProxy',)
289   
290    def __init__(self, **kw):
291        if not AttributeQuerySslSOAPBinding.SSL_CONTEXT_PROXY_SUPPORT:
292            raise ImportError("ndg.security.common.utils.m2crypto import "
293                              "failed - missing M2Crypto package?")
294       
295        # Miss out default HTTPSHandler and set in send() instead
296        if 'handlers' in kw:
297            raise TypeError("__init__() got an unexpected keyword argument "
298                            "'handlers'")
299           
300        super(AttributeQuerySslSOAPBinding, self).__init__(handlers=(), **kw)
301        self.__sslCtxProxy = SSLContextProxy()
302
303    def send(self, **kw):
304        """Override base class implementation to pass explicit SSL Context
305        """
306        httpsHandler = HTTPSHandler(ssl_context=self.sslCtxProxy.createCtx())
307        self.client.openerDirector.add_handler(httpsHandler)
308        return super(AttributeQuerySslSOAPBinding, self).send(**kw)
309       
310    @property
311    def sslCtxProxy(self):
312        """SSL Context Proxy object used for setting up an SSL Context for
313        queries
314        """
315        return self.__sslCtxProxy
316           
317    def __setattr__(self, name, value):
318        """Enable setting of SSLContextProxy attributes as if they were
319        attributes of this class.  This is intended as a convenience for
320        making settings parameters read from a config file
321        """
322        try:
323            super(AttributeQuerySslSOAPBinding, self).__setattr__(name, value)
324           
325        except AttributeError:
326            # Coerce into setting SSL Context Proxy attributes
327            try:
328                setattr(self.sslCtxProxy, name, value)
329            except:
330                raise
Note: See TracBrowser for help on using the repository browser.