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

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

Incomplete - task 2: XACML-Security Integration

  • fixes to SOAP client bindings
  • 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 a 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       
93        # These properties access the __query instance
94        self.issuerFormat = Issuer.X509_SUBJECT
95        self.subjectIdFormat = NameID.UNSPECIFIED
96
97    def _getQuery(self):
98        return self.__query
99
100    def _setQuery(self, value):
101        if not isinstance(value, self.__class__.QUERY_TYPE):
102            raise TypeError('Expecting %r query type got %r instead' %
103                            (self.__class__, type(value)))
104        self.__query = value
105
106    query = property(_getQuery, _setQuery, 
107                     doc="SAML Subject Query or derived query type")
108
109    def _getSubjectID(self):
110        if self.__query.subject is None or self.__query.subject.nameID is None:
111            return None
112        else:
113            return self.__query.subject.nameID.value
114
115    def _setSubjectID(self, value):
116        if self.__query.subject is None:
117            self.__query.subject = Subject()
118           
119        if self.__query.subject.nameID is None:
120            self.__query.subject.nameID = NameID()
121           
122        self.__query.subject.nameID.value = value
123
124    subjectID = property(_getSubjectID, _setSubjectID, 
125                         doc="ID to be sent as query subject")
126   
127    def _getSubjectIdFormat(self):
128        if self.__query.subject is None or self.__query.subject.nameID is None:
129            return None
130        else:
131            return self.__query.subject.nameID.format
132
133    def _setSubjectIdFormat(self, value):
134        if self.__query.subject is None:
135            self.__query.subject = Subject()
136           
137        if self.__query.subject.nameID is None:
138            self.__query.subject.nameID = NameID()
139           
140        self.__query.subject.nameID.format = value
141
142    subjectIdFormat = property(_getSubjectIdFormat, _setSubjectIdFormat, 
143                               doc="Subject Name ID format")
144
145    def _getIssuerFormat(self):
146        if self.__query.issuer is None:
147            return None
148        else:
149            return self.__query.issuer.value
150
151    def _setIssuerFormat(self, value):
152        if self.__query.issuer is None:
153            self.__query.issuer = Issuer()
154           
155        self.__query.issuer.format = value
156
157    issuerFormat = property(_getIssuerFormat, _setIssuerFormat, 
158                            doc="Issuer format")
159
160    def _getIssuerName(self):
161        if self.__query.issuer is None:
162            return None
163        else:
164            return self.__query.issuer.value
165
166    def _setIssuerName(self, value):
167        if self.__query.issuer is None:
168            self.__query.issuer = Issuer()
169           
170        self.__query.issuer.value = value
171
172    issuerName = property(_getIssuerName, _setIssuerName, 
173                          doc="Name of issuer of SAML Subject Query")
174
175    def _getVerifyTimeConditions(self):
176        return self.__verifyTimeConditions
177
178    def _setVerifyTimeConditions(self, value):
179        if isinstance(value, bool):
180            self.__verifyTimeConditions = value
181           
182        if isinstance(value, basestring):
183            self.__verifyTimeConditions = str2Bool(value)
184        else:
185            raise TypeError('Expecting bool or string type for '
186                            '"verifyTimeConditions"; got %r instead' % 
187                            type(value))
188
189    verifyTimeConditions = property(_getVerifyTimeConditions, 
190                                    _setVerifyTimeConditions, 
191                                    doc='Set to True to verify any time '
192                                        'Conditions set in the returned '
193                                        'response assertions') 
194
195    def _getClockSkewTolerance(self):
196        return self.__clockSkewTolerance
197
198    def _setClockSkewTolerance(self, value):
199        if isinstance(value, timedelta):
200            self.__clockSkewTolerance = value
201           
202        elif 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 timedelta, float, int, long or string '
209                            'type for "clockSkewTolerance"; got %r' % 
210                            type(value))
211
212    clockSkewTolerance = property(fget=_getClockSkewTolerance, 
213                                  fset=_setClockSkewTolerance, 
214                                  doc="Allow a tolerance in seconds for SAML "
215                                      "Query issueInstant parameter check and "
216                                      "assertion condition notBefore and "
217                                      "notOnOrAfter times to allow for clock "
218                                      "skew")
219   
220    def _validateQueryParameters(self):
221        """Perform sanity check immediately before creating the query and
222        sending it"""
223        errors = []
224       
225        if self.issuerName is None:
226            errors.append('issuer name')
227
228        if self.issuerFormat is None:
229            errors.append('issuer format')
230       
231        if self.subjectID is None:
232            errors.append('subject')
233       
234        if self.subjectIdFormat is None:
235            errors.append('subject format')
236       
237        if errors:
238            raise AttributeError('Missing attribute(s) for SAML Query: %s' %
239                                 ', '.join(errors))
240
241    def _initSend(self):
242        """Perform any final initialisation prior to sending the query - derived
243        classes may overload to specify as required"""
244        self.__query.issueInstant = datetime.utcnow()
245       
246        # Set ID here to ensure it's unique for each new call
247        self.__query.id = str(uuid4())
248
249    def _verifyTimeConditions(self, response):
250        """Verify time conditions set in a response
251        @param response: SAML Response returned from remote service
252        @type response: ndg.saml.saml2.core.Response
253        @raise SubjectQueryResponseError: if a timestamp is invalid
254        """
255       
256        if not self.verifyTimeConditions:
257            log.debug("Skipping verification of SAML Response time conditions")
258           
259        utcNow = datetime.utcnow() 
260        nowMinusSkew = utcNow - self.clockSkewTolerance
261        nowPlusSkew = utcNow + self.clockSkewTolerance
262       
263        if response.issueInstant > nowPlusSkew:
264            msg = ('SAML Attribute Response issueInstant [%s] is after '
265                   'the clock time [%s] (skewed +%s)' % 
266                   (response.issueInstant, 
267                    SAMLDateTime.toString(nowPlusSkew),
268                    self.clockSkewTolerance))
269             
270            samlRespError = ResponseIssueInstantInvalid(msg)
271            samlRespError.response = response
272            raise samlRespError
273       
274        for assertion in response.assertions:
275            if assertion.issueInstant is None:
276                samlRespError = AssertionIssueInstantInvalid("No issueInstant "
277                                                             "set in response "
278                                                             "assertion")
279                samlRespError.response = response
280                raise samlRespError
281           
282            elif nowPlusSkew < assertion.issueInstant:
283                msg = ('The clock time [%s] (skewed +%s) is before the '
284                       'SAML Attribute Response assertion issue instant [%s]' % 
285                       (SAMLDateTime.toString(utcNow),
286                        self.clockSkewTolerance,
287                        assertion.issueInstant))
288                samlRespError = AssertionIssueInstantInvalid(msg)
289                samlRespError.response = response
290                raise samlRespError
291           
292            if assertion.conditions is not None:
293                if nowPlusSkew < assertion.conditions.notBefore:           
294                    msg = ('The clock time [%s] (skewed +%s) is before the '
295                           'SAML Attribute Response assertion conditions not '
296                           'before time [%s]' % 
297                           (SAMLDateTime.toString(utcNow),
298                            self.clockSkewTolerance,
299                            assertion.conditions.notBefore))
300                             
301                    samlRespError = AssertionConditionNotBeforeInvalid(msg)
302                    samlRespError.response = response
303                    raise samlRespError
304                 
305                if nowMinusSkew >= assertion.conditions.notOnOrAfter:           
306                    msg = ('The clock time [%s] (skewed -%s) is on or after '
307                           'the SAML Attribute Response assertion conditions '
308                           'not on or after time [%s]' % 
309                           (SAMLDateTime.toString(utcNow),
310                            self.clockSkewTolerance,
311                            assertion.conditions.notOnOrAfter))
312                   
313                    samlRespError = AssertionConditionNotOnOrAfterInvalid(msg) 
314                    samlRespError.response = response
315                    raise samlRespError
316               
317    def send(self, **kw):
318        '''Make an attribute query to a remote SAML service
319       
320        @type uri: basestring
321        @param uri: uri of service.  May be omitted if set from request.url
322        @type request: ndg.security.common.soap.UrlLib2SOAPRequest
323        @param request: SOAP request object to which query will be attached
324        defaults to ndg.security.common.soap.client.UrlLib2SOAPRequest
325        '''
326        self._validateQueryParameters() 
327        self._initSend()
328           
329        response = super(SubjectQuerySOAPBinding, self).send(self.query, **kw)
330
331        # Perform validation
332        if response.status.statusCode.value != StatusCode.SUCCESS_URI:
333            msg = ('Return status code flagged an error, %r.  '
334                   'The message is, %r' %
335                   (response.status.statusCode.value,
336                    response.status.statusMessage.value))
337            samlRespError = SubjectQueryResponseError(msg)
338            samlRespError.response = response
339            raise samlRespError
340       
341        # Check Query ID matches the query ID the service received
342        if response.inResponseTo != self.query.id:
343            msg = ('Response in-response-to ID %r, doesn\'t match the original '
344                   'query ID, %r' % (response.inResponseTo, query.id))
345           
346            samlRespError = SubjectQueryResponseError(msg)
347            samlRespError.response = response
348            raise samlRespError
349       
350        self._verifyTimeConditions(response)
351           
352        return response
Note: See TracBrowser for help on using the repository browser.