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

Subversion URL: http://proj.badc.rl.ac.uk/svn/ndg-security/TI12-security/trunk/NDGSecurity/python/ndg_security_common/ndg/security/common/saml_utils/binding/soap/subjectquery.py@6578
Revision 6578, 11.8 KB checked in by pjkersha, 11 years ago (diff)
  • Important fix for SOAP client used with SAML SOAP binding: set text/xml content type.
  • Refactored SAML SOAP binding query clients.
Line 
1"""SAML 2.0 bindings module implements SOAP binding for subject query
2
3NERC DataGrid Project
4"""
5__author__ = "P J Kershaw"
6__date__ = "12/02/10"
7__copyright__ = "(C) 2010 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 datetime import datetime, timedelta
15from uuid import uuid4
16
17from saml.common import SAMLObject
18from saml.utils import SAMLDateTime
19from saml.saml2.core import (SubjectQuery, StatusCode, Response,
20                             Issuer, Subject, SAMLVersion, NameID)
21
22from ndg.security.common.utils import str2Bool
23from ndg.security.common.saml_utils.binding.soap import (SOAPBinding,
24    SOAPBindingInvalidResponse)
25
26
27class SubjectQueryResponseError(SOAPBindingInvalidResponse):
28    """SAML Response error from Subject Query"""
29    def __init__(self, *arg, **kw):
30        SOAPBindingInvalidResponse.__init__(self, *arg, **kw)
31        self.__response = None
32   
33    def _getResponse(self):
34        '''Gets the response corresponding to this error
35       
36        @return the response
37        '''
38        return self.__response
39
40    def _setResponse(self, value):
41        '''Sets the response corresponding to this error.
42       
43        @param value: the response
44        '''
45        if not isinstance(value, Response):
46            raise TypeError('"response" must be a %r, got %r' % (Response,
47                                                                 type(value)))
48        self.__response = value
49       
50    response = property(fget=_getResponse, fset=_setResponse, 
51                        doc="SAML Response associated with this exception")
52   
53   
54class SubjectQuerySOAPBinding(SOAPBinding): 
55    """SAML Subject Query SOAP Binding
56    """
57    SUBJECT_ID_OPTNAME = 'subjectID'
58    SUBJECT_ID_FORMAT_OPTNAME = 'subjectIdFormat'
59    ISSUER_NAME_OPTNAME = 'issuerName'
60    ISSUER_FORMAT_OPTNAME = 'issuerFormat'
61    CLOCK_SKEW_OPTNAME = 'clockSkewTolerance'
62    VERIFY_TIME_CONDITIONS_OPTNAME = 'verifyTimeConditions'
63   
64    CONFIG_FILE_OPTNAMES = (
65        SUBJECT_ID_OPTNAME,
66        SUBJECT_ID_FORMAT_OPTNAME,
67        ISSUER_NAME_OPTNAME, 
68        ISSUER_FORMAT_OPTNAME,               
69        CLOCK_SKEW_OPTNAME,
70        VERIFY_TIME_CONDITIONS_OPTNAME           
71    )
72   
73    __PRIVATE_ATTR_PREFIX = "__"
74    __slots__ = tuple([__PRIVATE_ATTR_PREFIX + i
75                       for i in CONFIG_FILE_OPTNAMES + ('query', )])
76    del i
77   
78    QUERY_TYPE = SubjectQuery
79   
80    def __init__(self, **kw):
81        '''Create SOAP Client for SAML Subject Query'''       
82        self.__clockSkewTolerance = timedelta(seconds=0.)
83        self.__verifyTimeConditions = True
84       
85        self._initQuery()
86       
87        super(SubjectQuerySOAPBinding, self).__init__(**kw)
88
89    def _initQuery(self):
90        """Initialise query settings"""
91        self.__query = self.__class__.QUERY_TYPE()
92        self.__query.version = SAMLVersion(SAMLVersion.VERSION_20)
93        self.__query.id = str(uuid4())
94       
95        # These properties access the __query instance
96        self.issuerFormat = Issuer.X509_SUBJECT
97        self.subjectIdFormat = NameID.UNSPECIFIED
98
99    def _getQuery(self):
100        return self.__query
101
102    def _setQuery(self, value):
103        if not isinstance(value, self.__class__.QUERY_TYPE):
104            raise TypeError('Expecting %r query type got %r instead' %
105                            (self.__class__, type(value)))
106        self.__query = value
107
108    query = property(_getQuery, _setQuery, 
109                     doc="SAML Subject Query or derived query type")
110
111    def _getSubjectID(self):
112        if self.__query.subject is None or self.__query.subject.nameID is None:
113            return None
114        else:
115            return self.__query.subject.nameID.value
116
117    def _setSubjectID(self, value):
118        if self.__query.subject is None:
119            self.__query.subject = Subject()
120           
121        if self.__query.subject.nameID is None:
122            self.__query.subject.nameID = NameID()
123           
124        self.__query.subject.nameID.value = value
125
126    subjectID = property(_getSubjectID, _setSubjectID, 
127                         doc="ID to be sent as query subject")
128   
129    def _getSubjectIdFormat(self):
130        if self.__query.subject is None or self.__query.subject.nameID is None:
131            return None
132        else:
133            return self.__query.subject.nameID.format
134
135    def _setSubjectIdFormat(self, value):
136        if self.__query.subject is None:
137            self.__query.subject = Subject()
138           
139        if self.__query.subject.nameID is None:
140            self.__query.subject.nameID = NameID()
141           
142        self.__query.subject.nameID.format = value
143
144    subjectIdFormat = property(_getSubjectIdFormat, _setSubjectIdFormat, 
145                               doc="Subject Name ID format")
146
147    def _getIssuerFormat(self):
148        if self.__query.issuer is None:
149            return None
150        else:
151            return self.__query.issuer.value
152
153    def _setIssuerFormat(self, value):
154        if self.__query.issuer is None:
155            self.__query.issuer = Issuer()
156           
157        self.__query.issuer.format = value
158
159    issuerFormat = property(_getIssuerFormat, _setIssuerFormat, 
160                            doc="Issuer format")
161
162    def _getIssuerName(self):
163        if self.__query.issuer is None:
164            return None
165        else:
166            return self.__query.issuer.value
167
168    def _setIssuerName(self, value):
169        if self.__query.issuer is None:
170            self.__query.issuer = Issuer()
171           
172        self.__query.issuer.value = value
173
174    issuerName = property(_getIssuerName, _setIssuerName, 
175                        doc="Distinguished Name of issuer of SAML Attribute "
176                            "Query to Attribute Authority")
177
178    def _getVerifyTimeConditions(self):
179        return self.__verifyTimeConditions
180
181    def _setVerifyTimeConditions(self, value):
182        if isinstance(value, bool):
183            self.__verifyTimeConditions = value
184           
185        if isinstance(value, basestring):
186            self.__verifyTimeConditions = str2Bool(value)
187        else:
188            raise TypeError('Expecting bool or string type for '
189                            '"verifyTimeConditions"; got %r instead' % 
190                            type(value))
191
192    verifyTimeConditions = property(_getVerifyTimeConditions, 
193                                    _setVerifyTimeConditions, 
194                                    doc='Set to True to verify any time '
195                                        'Conditions set in the returned '
196                                        'response assertions') 
197
198    def _getClockSkewTolerance(self):
199        return self.__clockSkewTolerance
200
201    def _setClockSkewTolerance(self, value):
202        if isinstance(value, (float, int, long)):
203            self.__clockSkewTolerance = timedelta(seconds=value)
204           
205        elif isinstance(value, basestring):
206            self.__clockSkewTolerance = timedelta(seconds=float(value))
207        else:
208            raise TypeError('Expecting float, int, long or string type for '
209                            '"clockSkewTolerance"; got %r' % type(value))
210
211    clockSkewTolerance = property(fget=_getClockSkewTolerance, 
212                                  fset=_setClockSkewTolerance, 
213                                  doc="Allow a tolerance in seconds for SAML "
214                                      "Query issueInstant parameter check and "
215                                      "assertion condition notBefore and "
216                                      "notOnOrAfter times to allow for clock "
217                                      "skew")
218   
219    def _validateQueryParameters(self):
220        """Perform sanity check immediately before creating the query and
221        sending it"""
222        if self.issuerName is None:
223            raise AttributeError('No issuer name has been set for SAML Query')
224
225        if self.issuerFormat is None:
226            raise AttributeError('No issuer format has been set for SAML Query')
227       
228        if self.subjectID is None:
229            raise AttributeError('No subject has been set for SAML Query')
230       
231        if self.subjectIdFormat is None:
232            raise AttributeError('No subject format has been set for SAML '
233                                 'Query')
234
235    def _initSend(self):
236        """Perform any final initialisation prior to sending the query - derived
237        classes may overload to specify as required"""
238        self.__query.issueInstant = datetime.utcnow()
239
240    def send(self, **kw):
241        '''Make an attribute query to a remote SAML service
242       
243        @type uri: basestring
244        @param uri: uri of service.  May be omitted if set from request.url
245        @type request: ndg.security.common.soap.UrlLib2SOAPRequest
246        @param request: SOAP request object to which query will be attached
247        defaults to ndg.security.common.soap.client.UrlLib2SOAPRequest
248        '''
249        self._validateQueryParameters() 
250        self._initSend()
251           
252        response = super(SubjectQuerySOAPBinding, self).send(self.query, **kw)
253
254        # Perform validation
255        if response.status.statusCode.value != StatusCode.SUCCESS_URI:
256            msg = ('Return status code flagged an error, %r.  '
257                   'The message is, %r' %
258                   (response.status.statusCode.value,
259                    response.status.statusMessage.value))
260            samlRespError = SubjectQueryResponseError(msg)
261            samlRespError.response = response
262            raise samlRespError
263       
264        # Check Query ID matches the query ID the service received
265        if response.inResponseTo != query.id:
266            msg = ('Response in-response-to ID %r, doesn\'t match the original '
267                   'query ID, %r' % (response.inResponseTo, query.id))
268           
269            samlRespError = SubjectQueryResponseError(msg)
270            samlRespError.response = response
271            raise samlRespError
272       
273        utcNow = datetime.utcnow() + self.clockSkewTolerance
274        if response.issueInstant > utcNow:
275            msg = ('SAML Attribute Response issueInstant [%s] is after '
276                   'the current clock time [%s]' % 
277                   (query.issueInstant, SAMLDateTime.toString(utcNow)))
278           
279            samlRespError = SubjectQueryResponseError(msg)                 
280            samlRespError.response = response
281            raise samlRespError
282       
283        for assertion in response.assertions:
284            if self.verifyTimeConditions and assertion.conditions is not None:
285                if utcNow < assertion.conditions.notBefore:           
286                    msg = ('The current clock time [%s] is before the SAML '
287                           'Attribute Response assertion conditions not before '
288                           'time [%s]' % 
289                           (SAMLDateTime.toString(utcNow),
290                            assertion.conditions.notBefore))
291                             
292                    samlRespError = SubjectQueryResponseError(msg)
293                    samlRespError.response = response
294                    raise samlRespError
295                 
296                if utcNow >= assertion.conditions.notOnOrAfter:           
297                    msg = ('The current clock time [%s] is on or after the '
298                           'SAML Attribute Response assertion conditions not '
299                           'on or after time [%s]' % 
300                           (SAMLDateTime.toString(utcNow),
301                            response.assertion.conditions.notOnOrAfter))
302                   
303                    samlRespError = SubjectQueryResponseError(msg) 
304                    samlRespError.response = response
305                    raise samlRespError   
306           
307        return response
Note: See TracBrowser for help on using the repository browser.