source: TI12-security/trunk/ndg_saml/ndg/saml/saml2/binding/soap/client/subjectquery.py @ 7259

Subversion URL: http://proj.badc.rl.ac.uk/svn/ndg/TI12-security/trunk/ndg_saml/ndg/saml/saml2/binding/soap/client/subjectquery.py@7259
Revision 7259, 13.3 KB checked in by pjkersha, 10 years ago (diff)

Fixes for NDG SAML:

  • SOAPBindingInvalidResponse - incorrect superclass and no Response import
  • added support for 'here' directory to config parser
  • cumulative error checking for validation of query parameters prior to despatch
  • Allow for no subject set in ElementTree parser
  • Property svn:keywords set to Id
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__ = "http://www.apache.org/licenses/LICENSE-2.0"
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 ndg.saml.common import SAMLObject
18from ndg.saml.utils import SAMLDateTime
19from ndg.saml.saml2.core import (SubjectQuery, StatusCode, Response,
20                             Issuer, Subject, SAMLVersion, NameID)
21
22from ndg.saml.utils import str2Bool
23from ndg.saml.saml2.binding.soap.client import (SOAPBinding,
24    SOAPBindingInvalidResponse)
25
26
27class SubjectQueryResponseError(SOAPBindingInvalidResponse):
28    """SAML Response error from Subject Query"""
29   
30
31class IssueInstantInvalid(SubjectQueryResponseError):
32    """Issue instant of SAML artifact is invalid"""
33
34 
35class ResponseIssueInstantInvalid(IssueInstantInvalid):
36    """Issue instant of a response is after the current time"""
37
38   
39class AssertionIssueInstantInvalid(IssueInstantInvalid):
40    """Issue instant of an assertion is after the current time"""
41
42
43class AssertionConditionNotBeforeInvalid(SubjectQueryResponseError):
44    """An assertion condition notBefore time is set after the current clock
45    time"""
46   
47
48class AssertionConditionNotOnOrAfterInvalid(SubjectQueryResponseError):
49    """An assertion condition notOnOrAfter time is set before the current clock
50    time"""
51
52   
53class SubjectQuerySOAPBinding(SOAPBinding): 
54    """SAML Subject Query SOAP Binding
55    """
56    SUBJECT_ID_OPTNAME = 'subjectID'
57    SUBJECT_ID_FORMAT_OPTNAME = 'subjectIdFormat'
58    ISSUER_NAME_OPTNAME = 'issuerName'
59    ISSUER_FORMAT_OPTNAME = 'issuerFormat'
60    CLOCK_SKEW_OPTNAME = 'clockSkewTolerance'
61    VERIFY_TIME_CONDITIONS_OPTNAME = 'verifyTimeConditions'
62   
63    CONFIG_FILE_OPTNAMES = (
64        SUBJECT_ID_OPTNAME,
65        SUBJECT_ID_FORMAT_OPTNAME,
66        ISSUER_NAME_OPTNAME, 
67        ISSUER_FORMAT_OPTNAME,               
68        CLOCK_SKEW_OPTNAME,
69        VERIFY_TIME_CONDITIONS_OPTNAME           
70    )
71   
72    __PRIVATE_ATTR_PREFIX = "__"
73    __slots__ = tuple([__PRIVATE_ATTR_PREFIX + i
74                       for i in CONFIG_FILE_OPTNAMES + ('query', )])
75    del i
76   
77    QUERY_TYPE = SubjectQuery
78   
79    def __init__(self, **kw):
80        '''Create SOAP Client for SAML Subject Query'''       
81        self.__clockSkewTolerance = timedelta(seconds=0.)
82        self.__verifyTimeConditions = True
83       
84        self._initQuery()
85       
86        super(SubjectQuerySOAPBinding, self).__init__(**kw)
87
88    def _initQuery(self):
89        """Initialise query settings"""
90        self.__query = self.__class__.QUERY_TYPE()
91        self.__query.version = SAMLVersion(SAMLVersion.VERSION_20)
92        self.__query.id = str(uuid4())
93       
94        # These properties access the __query instance
95        self.issuerFormat = Issuer.X509_SUBJECT
96        self.subjectIdFormat = NameID.UNSPECIFIED
97
98    def _getQuery(self):
99        return self.__query
100
101    def _setQuery(self, value):
102        if not isinstance(value, self.__class__.QUERY_TYPE):
103            raise TypeError('Expecting %r query type got %r instead' %
104                            (self.__class__, type(value)))
105        self.__query = value
106
107    query = property(_getQuery, _setQuery, 
108                     doc="SAML Subject Query or derived query type")
109
110    def _getSubjectID(self):
111        if self.__query.subject is None or self.__query.subject.nameID is None:
112            return None
113        else:
114            return self.__query.subject.nameID.value
115
116    def _setSubjectID(self, value):
117        if self.__query.subject is None:
118            self.__query.subject = Subject()
119           
120        if self.__query.subject.nameID is None:
121            self.__query.subject.nameID = NameID()
122           
123        self.__query.subject.nameID.value = value
124
125    subjectID = property(_getSubjectID, _setSubjectID, 
126                         doc="ID to be sent as query subject")
127   
128    def _getSubjectIdFormat(self):
129        if self.__query.subject is None or self.__query.subject.nameID is None:
130            return None
131        else:
132            return self.__query.subject.nameID.format
133
134    def _setSubjectIdFormat(self, value):
135        if self.__query.subject is None:
136            self.__query.subject = Subject()
137           
138        if self.__query.subject.nameID is None:
139            self.__query.subject.nameID = NameID()
140           
141        self.__query.subject.nameID.format = value
142
143    subjectIdFormat = property(_getSubjectIdFormat, _setSubjectIdFormat, 
144                               doc="Subject Name ID format")
145
146    def _getIssuerFormat(self):
147        if self.__query.issuer is None:
148            return None
149        else:
150            return self.__query.issuer.value
151
152    def _setIssuerFormat(self, value):
153        if self.__query.issuer is None:
154            self.__query.issuer = Issuer()
155           
156        self.__query.issuer.format = value
157
158    issuerFormat = property(_getIssuerFormat, _setIssuerFormat, 
159                            doc="Issuer format")
160
161    def _getIssuerName(self):
162        if self.__query.issuer is None:
163            return None
164        else:
165            return self.__query.issuer.value
166
167    def _setIssuerName(self, value):
168        if self.__query.issuer is None:
169            self.__query.issuer = Issuer()
170           
171        self.__query.issuer.value = value
172
173    issuerName = property(_getIssuerName, _setIssuerName, 
174                          doc="Name of issuer of SAML Subject Query")
175
176    def _getVerifyTimeConditions(self):
177        return self.__verifyTimeConditions
178
179    def _setVerifyTimeConditions(self, value):
180        if isinstance(value, bool):
181            self.__verifyTimeConditions = value
182           
183        if isinstance(value, basestring):
184            self.__verifyTimeConditions = str2Bool(value)
185        else:
186            raise TypeError('Expecting bool or string type for '
187                            '"verifyTimeConditions"; got %r instead' % 
188                            type(value))
189
190    verifyTimeConditions = property(_getVerifyTimeConditions, 
191                                    _setVerifyTimeConditions, 
192                                    doc='Set to True to verify any time '
193                                        'Conditions set in the returned '
194                                        'response assertions') 
195
196    def _getClockSkewTolerance(self):
197        return self.__clockSkewTolerance
198
199    def _setClockSkewTolerance(self, value):
200        if isinstance(value, timedelta):
201            self.__clockSkewTolerance = value
202           
203        elif isinstance(value, (float, int, long)):
204            self.__clockSkewTolerance = timedelta(seconds=value)
205           
206        elif isinstance(value, basestring):
207            self.__clockSkewTolerance = timedelta(seconds=float(value))
208        else:
209            raise TypeError('Expecting timedelta, float, int, long or string '
210                            'type for "clockSkewTolerance"; got %r' % 
211                            type(value))
212
213    clockSkewTolerance = property(fget=_getClockSkewTolerance, 
214                                  fset=_setClockSkewTolerance, 
215                                  doc="Allow a tolerance in seconds for SAML "
216                                      "Query issueInstant parameter check and "
217                                      "assertion condition notBefore and "
218                                      "notOnOrAfter times to allow for clock "
219                                      "skew")
220   
221    def _validateQueryParameters(self):
222        """Perform sanity check immediately before creating the query and
223        sending it"""
224        errors = []
225       
226        if self.issuerName is None:
227            errors.append('issuer name')
228
229        if self.issuerFormat is None:
230            errors.append('issuer format')
231       
232        if self.subjectID is None:
233            errors.append('subject')
234       
235        if self.subjectIdFormat is None:
236            errors.append('subject format')
237       
238        if errors:
239            raise AttributeError('Missing attribute(s) for SAML Query: %s' %
240                                 ', '.join(errors))
241
242    def _initSend(self):
243        """Perform any final initialisation prior to sending the query - derived
244        classes may overload to specify as required"""
245        self.__query.issueInstant = datetime.utcnow()
246
247    def _verifyTimeConditions(self, response):
248        """Verify time conditions set in a response
249        @param response: SAML Response returned from remote service
250        @type response: ndg.saml.saml2.core.Response
251        @raise SubjectQueryResponseError: if a timestamp is invalid
252        """
253       
254        if not self.verifyTimeConditions:
255            log.debug("Skipping verification of SAML Response time conditions")
256           
257        utcNow = datetime.utcnow() 
258        nowMinusSkew = utcNow - self.clockSkewTolerance
259        nowPlusSkew = utcNow + self.clockSkewTolerance
260       
261        if response.issueInstant > nowPlusSkew:
262            msg = ('SAML Attribute Response issueInstant [%s] is after '
263                   'the clock time [%s] (skewed +%s)' % 
264                   (response.issueInstant, 
265                    SAMLDateTime.toString(nowPlusSkew),
266                    self.clockSkewTolerance))
267             
268            samlRespError = ResponseIssueInstantInvalid(msg)
269            samlRespError.response = response
270            raise samlRespError
271       
272        for assertion in response.assertions:
273            if assertion.issueInstant is None:
274                samlRespError = AssertionIssueInstantInvalid("No issueInstant "
275                                                             "set in response "
276                                                             "assertion")
277                samlRespError.response = response
278                raise samlRespError
279           
280            elif nowPlusSkew < assertion.issueInstant:
281                msg = ('The clock time [%s] (skewed +%s) is before the '
282                       'SAML Attribute Response assertion issue instant [%s]' % 
283                       (SAMLDateTime.toString(utcNow),
284                        self.clockSkewTolerance,
285                        assertion.issueInstant))
286                samlRespError = AssertionIssueInstantInvalid(msg)
287                samlRespError.response = response
288                raise samlRespError
289           
290            if assertion.conditions is not None:
291                if nowPlusSkew < assertion.conditions.notBefore:           
292                    msg = ('The clock time [%s] (skewed +%s) is before the '
293                           'SAML Attribute Response assertion conditions not '
294                           'before time [%s]' % 
295                           (SAMLDateTime.toString(utcNow),
296                            self.clockSkewTolerance,
297                            assertion.conditions.notBefore))
298                             
299                    samlRespError = AssertionConditionNotBeforeInvalid(msg)
300                    samlRespError.response = response
301                    raise samlRespError
302                 
303                if nowMinusSkew >= assertion.conditions.notOnOrAfter:           
304                    msg = ('The clock time [%s] (skewed -%s) is on or after '
305                           'the SAML Attribute Response assertion conditions '
306                           'not on or after time [%s]' % 
307                           (SAMLDateTime.toString(utcNow),
308                            self.clockSkewTolerance,
309                            assertion.conditions.notOnOrAfter))
310                   
311                    samlRespError = AssertionConditionNotOnOrAfterInvalid(msg) 
312                    samlRespError.response = response
313                    raise samlRespError
314               
315    def send(self, **kw):
316        '''Make an attribute query to a remote SAML service
317       
318        @type uri: basestring
319        @param uri: uri of service.  May be omitted if set from request.url
320        @type request: ndg.security.common.soap.UrlLib2SOAPRequest
321        @param request: SOAP request object to which query will be attached
322        defaults to ndg.security.common.soap.client.UrlLib2SOAPRequest
323        '''
324        self._validateQueryParameters() 
325        self._initSend()
326           
327        response = super(SubjectQuerySOAPBinding, self).send(self.query, **kw)
328
329        # Perform validation
330        if response.status.statusCode.value != StatusCode.SUCCESS_URI:
331            msg = ('Return status code flagged an error, %r.  '
332                   'The message is, %r' %
333                   (response.status.statusCode.value,
334                    response.status.statusMessage.value))
335            samlRespError = SubjectQueryResponseError(msg)
336            samlRespError.response = response
337            raise samlRespError
338       
339        # Check Query ID matches the query ID the service received
340        if response.inResponseTo != self.query.id:
341            msg = ('Response in-response-to ID %r, doesn\'t match the original '
342                   'query ID, %r' % (response.inResponseTo, query.id))
343           
344            samlRespError = SubjectQueryResponseError(msg)
345            samlRespError.response = response
346            raise samlRespError
347       
348        self._verifyTimeConditions(response)
349           
350        return response
Note: See TracBrowser for help on using the repository browser.