Package ndg :: Package saml :: Package saml2 :: Package binding :: Package soap :: Package client :: Module requestbase
[hide private]

Source Code for Module ndg.saml.saml2.binding.soap.client.requestbase

  1  """SAML 2.0 bindings module implements SOAP binding for base request 
  2   
  3  NERC 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: subjectquery.py 7634 2010-10-20 20:23:29Z pjkersha $' 
 11  import logging 
 12  log = logging.getLogger(__name__) 
 13   
 14  import copy 
 15  from datetime import datetime, timedelta 
 16  from uuid import uuid4 
 17   
 18  from ndg.saml.utils import SAMLDateTime 
 19  from ndg.saml.saml2.core import (RequestAbstractType, StatusCode, Issuer, 
 20                                   SAMLVersion) 
 21   
 22  from ndg.saml.utils import str2Bool 
 23  from ndg.saml.saml2.binding.soap.client import (SOAPBinding, 
 24      SOAPBindingInvalidResponse) 
 25   
 26   
27 -class RequestResponseError(SOAPBindingInvalidResponse):
28 """SAML Response error from request"""
29 30
31 -class IssueInstantInvalid(RequestResponseError):
32 """Issue instant of SAML artifact is invalid"""
33 34
35 -class ResponseIssueInstantInvalid(IssueInstantInvalid):
36 """Issue instant of a response is after the current time"""
37 38
39 -class AssertionIssueInstantInvalid(IssueInstantInvalid):
40 """Issue instant of an assertion is after the current time"""
41 42
43 -class AssertionConditionNotBeforeInvalid(RequestResponseError):
44 """An assertion condition notBefore time is set after the current clock 45 time"""
46 47
48 -class AssertionConditionNotOnOrAfterInvalid(RequestResponseError):
49 """An assertion condition notOnOrAfter time is set before the current clock 50 time"""
51 52
53 -class RequestBaseSOAPBinding(SOAPBinding):
54 """SAML Request Base SOAP Binding 55 """ 56 ISSUER_NAME_OPTNAME = 'issuerName' 57 ISSUER_FORMAT_OPTNAME = 'issuerFormat' 58 CLOCK_SKEW_OPTNAME = 'clockSkewTolerance' 59 VERIFY_TIME_CONDITIONS_OPTNAME = 'verifyTimeConditions' 60 61 CONFIG_FILE_OPTNAMES = ( 62 ISSUER_NAME_OPTNAME, 63 ISSUER_FORMAT_OPTNAME, 64 CLOCK_SKEW_OPTNAME, 65 VERIFY_TIME_CONDITIONS_OPTNAME 66 ) 67 68 __PRIVATE_ATTR_PREFIX = "__" 69 __slots__ = tuple([__PRIVATE_ATTR_PREFIX + i 70 for i in CONFIG_FILE_OPTNAMES + ('issuer',)]) 71 del i 72 73 QUERY_TYPE = RequestAbstractType 74
75 - def __init__(self, **kw):
76 '''Create SOAP Client for a SAML Subject Query''' 77 self.__clockSkewTolerance = timedelta(seconds=0.) 78 self.__verifyTimeConditions = True 79 self.__issuer = Issuer() 80 self.__issuer.format = Issuer.X509_SUBJECT 81 82 super(RequestBaseSOAPBinding, self).__init__(**kw)
83
84 - def makeQuery(self):
85 query = self.__class__.QUERY_TYPE() 86 query.version = SAMLVersion(SAMLVersion.VERSION_20) 87 self.addQueryAttributes(query) 88 return query
89
90 - def addQueryAttributes(self, query):
91 query.issuer = copy.deepcopy(self.issuer)
92
93 - def _getIssuer(self):
94 return self.__issuer
95
96 - def _setIssuer(self, value):
97 self.__issuer = value
98 99 issuer = property(_getIssuer, _setIssuer, 100 doc="Issuer") 101
102 - def _getIssuerFormat(self):
103 if self.issuer is None: 104 return None 105 else: 106 return self.issuer.format
107
108 - def _setIssuerFormat(self, value):
109 if self.issuer is None: 110 self.issuer = Issuer() 111 112 self.issuer.format = value
113 114 issuerFormat = property(_getIssuerFormat, _setIssuerFormat, 115 doc="Issuer format") 116
117 - def _getIssuerName(self):
118 if self.issuer is None: 119 return None 120 else: 121 return self.issuer.value
122
123 - def _setIssuerName(self, value):
124 if self.issuer is None: 125 self.issuer = Issuer() 126 127 self.issuer.value = value
128 129 issuerName = property(_getIssuerName, _setIssuerName, 130 doc="Name of issuer of SAML Subject Query") 131
132 - def _getVerifyTimeConditions(self):
133 return self.__verifyTimeConditions
134
135 - def _setVerifyTimeConditions(self, value):
136 if isinstance(value, bool): 137 self.__verifyTimeConditions = value 138 139 if isinstance(value, basestring): 140 self.__verifyTimeConditions = str2Bool(value) 141 else: 142 raise TypeError('Expecting bool or string type for ' 143 '"verifyTimeConditions"; got %r instead' % 144 type(value))
145 146 verifyTimeConditions = property(_getVerifyTimeConditions, 147 _setVerifyTimeConditions, 148 doc='Set to True to verify any time ' 149 'Conditions set in the returned ' 150 'response assertions') 151
152 - def _getClockSkewTolerance(self):
153 return self.__clockSkewTolerance
154
155 - def _setClockSkewTolerance(self, value):
156 if isinstance(value, timedelta): 157 self.__clockSkewTolerance = value 158 159 elif isinstance(value, (float, int, long)): 160 self.__clockSkewTolerance = timedelta(seconds=value) 161 162 elif isinstance(value, basestring): 163 self.__clockSkewTolerance = timedelta(seconds=float(value)) 164 else: 165 raise TypeError('Expecting timedelta, float, int, long or string ' 166 'type for "clockSkewTolerance"; got %r' % 167 type(value))
168 169 clockSkewTolerance = property(fget=_getClockSkewTolerance, 170 fset=_setClockSkewTolerance, 171 doc="Allow a tolerance in seconds for SAML " 172 "Query issueInstant parameter check and " 173 "assertion condition notBefore and " 174 "notOnOrAfter times to allow for clock " 175 "skew") 176
177 - def _validateQueryParameters(self, query):
178 """Perform sanity check immediately before creating the query and 179 sending it""" 180 errors = [] 181 182 if query.issuer.value is None: 183 errors.append('issuer name') 184 185 if query.issuer.format is None: 186 errors.append('issuer format') 187 188 if errors: 189 raise AttributeError('Missing attribute(s) for SAML Query: %s' % 190 ', '.join(errors))
191
192 - def _initSend(self, query):
193 """Perform any final initialisation prior to sending the query - derived 194 classes may overload to specify as required""" 195 query.issueInstant = datetime.utcnow() 196 197 # Set ID here to ensure it's unique for each new call 198 query.id = str(uuid4())
199
200 - def _verifyTimeConditions(self, response):
201 """Verify time conditions set in a response 202 @param response: SAML Response returned from remote service 203 @type response: ndg.saml.saml2.core.Response 204 @raise RequestResponseError: if a timestamp is invalid 205 """ 206 207 if not self.verifyTimeConditions: 208 log.debug("Skipping verification of SAML Response time conditions") 209 210 utcNow = datetime.utcnow() 211 nowMinusSkew = utcNow - self.clockSkewTolerance 212 nowPlusSkew = utcNow + self.clockSkewTolerance 213 214 if response.issueInstant > nowPlusSkew: 215 msg = ('SAML Attribute Response issueInstant [%s] is after ' 216 'the clock time [%s] (skewed +%s)' % 217 (response.issueInstant, 218 SAMLDateTime.toString(nowPlusSkew), 219 self.clockSkewTolerance)) 220 221 samlRespError = ResponseIssueInstantInvalid(msg) 222 samlRespError.response = response 223 raise samlRespError 224 225 for assertion in response.assertions: 226 if assertion.issueInstant is None: 227 samlRespError = AssertionIssueInstantInvalid("No issueInstant " 228 "set in response " 229 "assertion") 230 samlRespError.response = response 231 raise samlRespError 232 233 elif nowPlusSkew < assertion.issueInstant: 234 msg = ('The clock time [%s] (skewed +%s) is before the ' 235 'SAML Attribute Response assertion issue instant [%s]' % 236 (SAMLDateTime.toString(utcNow), 237 self.clockSkewTolerance, 238 assertion.issueInstant)) 239 samlRespError = AssertionIssueInstantInvalid(msg) 240 samlRespError.response = response 241 raise samlRespError 242 243 if assertion.conditions is not None: 244 if nowPlusSkew < assertion.conditions.notBefore: 245 msg = ('The clock time [%s] (skewed +%s) is before the ' 246 'SAML Attribute Response assertion conditions not ' 247 'before time [%s]' % 248 (SAMLDateTime.toString(utcNow), 249 self.clockSkewTolerance, 250 assertion.conditions.notBefore)) 251 252 samlRespError = AssertionConditionNotBeforeInvalid(msg) 253 samlRespError.response = response 254 raise samlRespError 255 256 if nowMinusSkew >= assertion.conditions.notOnOrAfter: 257 msg = ('The clock time [%s] (skewed -%s) is on or after ' 258 'the SAML Attribute Response assertion conditions ' 259 'not on or after time [%s]' % 260 (SAMLDateTime.toString(utcNow), 261 self.clockSkewTolerance, 262 assertion.conditions.notOnOrAfter)) 263 264 samlRespError = AssertionConditionNotOnOrAfterInvalid(msg) 265 samlRespError.response = response 266 raise samlRespError
267
268 - def send(self, query, **kw):
269 '''Make an attribute query to a remote SAML service 270 271 @type uri: basestring 272 @param uri: uri of service. May be omitted if set from request.url 273 @type request: ndg.security.common.soap.UrlLib2SOAPRequest 274 @param request: SOAP request object to which query will be attached 275 defaults to ndg.security.common.soap.client.UrlLib2SOAPRequest 276 ''' 277 self._validateQueryParameters(query) 278 self._initSend(query) 279 280 log.debug("Sending request: query ID: %s", query.id) 281 response = super(RequestBaseSOAPBinding, self).send(query, **kw) 282 283 # Perform validation 284 if response.status.statusCode.value != StatusCode.SUCCESS_URI: 285 msg = ('Return status code flagged an error, %r. ' 286 'The message is, %r' % 287 (response.status.statusCode.value, 288 response.status.statusMessage.value)) 289 samlRespError = RequestResponseError(msg) 290 samlRespError.response = response 291 raise samlRespError 292 293 # Check Query ID matches the query ID the service received 294 if response.inResponseTo != query.id: 295 msg = ('Response in-response-to ID %r, doesn\'t match the original ' 296 'query ID, %r' % (response.inResponseTo, query.id)) 297 298 samlRespError = RequestResponseError(msg) 299 samlRespError.response = response 300 raise samlRespError 301 302 self._verifyTimeConditions(response) 303 304 return response
305