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

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

0.5.1 Release - fixes:

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