Package ndg :: Package saml :: Package saml2 :: Package binding :: Package soap :: Package server :: Package wsgi :: Module queryinterface
[hide private]

Source Code for Module ndg.saml.saml2.binding.soap.server.wsgi.queryinterface

  1  """WSGI SAML package for SAML 2.0 Attribute and Authorisation Decision Query/ 
  2  Request Profile interfaces 
  3   
  4  NERC DataGrid Project 
  5  """ 
  6  __author__ = "P J Kershaw" 
  7  __date__ = "15/02/10" 
  8  __copyright__ = "(C) 2010 Science and Technology Facilities Council" 
  9  __contact__ = "Philip.Kershaw@stfc.ac.uk" 
 10  __revision__ = "$Id: queryinterface.py 8051 2012-04-04 21:04:14Z pjkersha $" 
 11  __license__ = "http://www.apache.org/licenses/LICENSE-2.0" 
 12  import logging 
 13  log = logging.getLogger(__name__) 
 14  import traceback 
 15  from cStringIO import StringIO 
 16  from uuid import uuid4 
 17  from datetime import datetime, timedelta 
 18   
 19  from ndg.soap.server.wsgi.middleware import SOAPMiddleware 
 20  from ndg.soap.etree import SOAPEnvelope 
 21   
 22  from ndg.saml.utils import str2Bool 
 23  from ndg.saml.utils.factory import importModuleObject 
 24  from ndg.saml.xml import UnknownAttrProfile 
 25  from ndg.saml.xml.etree import QName 
 26  from ndg.saml.common import SAMLVersion 
 27  from ndg.saml.utils import SAMLDateTime 
 28  from ndg.saml.saml2.core import (Response, Status, StatusCode, StatusMessage,  
 29                                   Issuer)  
 30  from ndg.saml.saml2.binding.soap import SOAPBindingInvalidResponse 
 31   
 32  try: 
 33      from ndg.saml.saml2.xacml_profile import XACMLAuthzDecisionQuery 
 34      import ndg.saml.xml.etree_xacml_profile as etree_xacml_profile 
 35  except ImportError, e: 
 36      from warnings import warn 
 37      warn('Error importing XACML packages - disabling SAML XACML profile ' + \ 
 38           'support.  (Error is: %s)' % e) 
39 - class XACMLAuthzDecisionQuery(object):
40 """XACML Authz Decision Query substitute""" 41 DEFAULT_ELEMENT_LOCAL_NAME = 'XACMLAuthzDecisionQuery'
42
43 44 -class SOAPQueryInterfaceMiddlewareError(Exception):
45 """Base class for WSGI SAML 2.0 SOAP Query Interface Errors"""
46
47 48 -class SOAPQueryInterfaceMiddlewareConfigError(Exception):
49 """WSGI SAML 2.0 SOAP Query Interface Configuration problem"""
50
51 52 -class QueryIssueInstantInvalid(SOAPBindingInvalidResponse):
53 """Invalid timestamp for incoming query"""
54
55 56 -class SOAPQueryInterfaceMiddleware(SOAPMiddleware):
57 """Implementation of SAML 2.0 SOAP Binding for Query/Request Binding 58 59 @type PATH_OPTNAME: basestring 60 @cvar PATH_OPTNAME: name of app_conf option for specifying a path or paths 61 that this middleware will intercept and process 62 @type QUERY_INTERFACE_KEYNAME_OPTNAME: basestring 63 @cvar QUERY_INTERFACE_KEYNAME_OPTNAME: app_conf option name for key name 64 used to reference the SAML query interface in environ 65 @type DEFAULT_QUERY_INTERFACE_KEYNAME: basestring 66 @param DEFAULT_QUERY_INTERFACE_KEYNAME: default key name for referencing 67 SAML query interface in environ 68 """ 69 log = logging.getLogger('SOAPQueryInterfaceMiddleware') 70 PATH_OPTNAME = "mountPath" 71 QUERY_INTERFACE_KEYNAME_OPTNAME = "queryInterfaceKeyName" 72 DEFAULT_QUERY_INTERFACE_KEYNAME = ("ndg.security.server.wsgi.saml." 73 "SOAPQueryInterfaceMiddleware.queryInterface") 74 75 REQUEST_ENVELOPE_CLASS_OPTNAME = 'requestEnvelopeClass' 76 RESPONSE_ENVELOPE_CLASS_OPTNAME = 'responseEnvelopeClass' 77 SERIALISE_OPTNAME = 'serialise' 78 DESERIALISE_OPTNAME = 'deserialise' 79 DESERIALISE_XACML_PROFILE_OPTNAME = 'deserialiseXacmlProfile' 80 SAML_VERSION_OPTNAME = 'samlVersion' 81 ISSUER_NAME_OPTNAME = 'issuerName' 82 ISSUER_FORMAT_OPTNAME = 'issuerFormat' 83 CLOCK_SKEW_TOLERANCE_OPTNAME = 'clockSkewTolerance' 84 85 CONFIG_FILE_OPTNAMES = ( 86 PATH_OPTNAME, 87 QUERY_INTERFACE_KEYNAME_OPTNAME, 88 DEFAULT_QUERY_INTERFACE_KEYNAME, 89 REQUEST_ENVELOPE_CLASS_OPTNAME, 90 RESPONSE_ENVELOPE_CLASS_OPTNAME, 91 SERIALISE_OPTNAME, 92 DESERIALISE_OPTNAME, 93 DESERIALISE_XACML_PROFILE_OPTNAME, 94 SAML_VERSION_OPTNAME, 95 ISSUER_NAME_OPTNAME, 96 ISSUER_FORMAT_OPTNAME, 97 CLOCK_SKEW_TOLERANCE_OPTNAME 98 ) 99
100 - def __init__(self, app):
101 '''@type app: callable following WSGI interface 102 @param app: next middleware application in the chain 103 ''' 104 super(SOAPQueryInterfaceMiddleware, self).__init__() 105 106 self._app = app 107 108 # Set defaults 109 cls = SOAPQueryInterfaceMiddleware 110 self.__queryInterfaceKeyName = cls.DEFAULT_QUERY_INTERFACE_KEYNAME 111 self.__mountPath = '/' 112 self.__requestEnvelopeClass = None 113 self.__responseEnvelopeClass = None 114 self.__serialise = None 115 self.__deserialise = None 116 self.__deserialiseXacmlProfile = None 117 self.__issuer = None 118 self.__clockSkewTolerance = timedelta(seconds=0.) 119 self.__verifyTimeConditions = True 120 self.__verifySAMLVersion = True 121 self.__samlVersion = SAMLVersion.VERSION_20 122 123 # Proxy object for SAML Response Issuer attributes. By generating a 124 # proxy the Response objects inherent attribute validation can be 125 # applied to Issuer related config parameters before they're assigned to 126 # the response issuer object generated in the authorisation decision 127 # query response 128 self.__issuerProxy = Issuer()
129
130 - def initialise(self, global_conf, prefix='', **app_conf):
131 ''' 132 @type global_conf: dict 133 @param global_conf: PasteDeploy global configuration dictionary 134 @type prefix: basestring 135 @param prefix: prefix for configuration items 136 @type app_conf: dict 137 @param app_conf: PasteDeploy application specific configuration 138 dictionary 139 ''' 140 # Override where set in config 141 for name in SOAPQueryInterfaceMiddleware.CONFIG_FILE_OPTNAMES: 142 val = app_conf.get(prefix + name) 143 if val is not None: 144 setattr(self, name, val) 145 146 if self.serialise is None: 147 raise AttributeError('No "serialise" method set to serialise the ' 148 'SAML response from this middleware.') 149 150 if self.deserialise is None: 151 raise AttributeError('No "deserialise" method set to parse the ' 152 'SAML request to this middleware.')
153
154 - def _getSerialise(self):
155 return self.__serialise
156
157 - def _setSerialise(self, value):
158 if isinstance(value, basestring): 159 self.__serialise = importModuleObject(value) 160 161 elif callable(value): 162 self.__serialise = value 163 else: 164 raise TypeError('Expecting callable for "serialise"; got %r' % 165 value)
166 167 serialise = property(_getSerialise, _setSerialise, 168 doc="callable to serialise request into XML type") 169
170 - def _getDeserialise(self):
171 return self.__deserialise
172
173 - def _setDeserialise(self, value):
174 if isinstance(value, basestring): 175 self.__deserialise = importModuleObject(value) 176 177 elif callable(value): 178 self.__deserialise = value 179 else: 180 raise TypeError('Expecting callable for "deserialise"; got %r' % 181 value)
182 183 deserialise = property(_getDeserialise, 184 _setDeserialise, 185 doc="callable to de-serialise response from XML " 186 "type") 187
189 return self.__deserialiseXacmlProfile
190
191 - def _setDeserialiseXacmlProfile(self, value):
192 if isinstance(value, basestring): 193 self.__deserialiseXacmlProfile = importModuleObject(value) 194 195 elif callable(value): 196 self.__deserialiseXacmlProfile = value 197 else: 198 raise TypeError('Expecting callable for "deserialiseXacmlProfile"; ' 199 'got %r' % value)
200 201 deserialiseXacmlProfile = property(_getDeserialiseXacmlProfile, 202 _setDeserialiseXacmlProfile, 203 doc="callable to de-serialise response " 204 "from XML type with XACML profile") 205
206 - def _getIssuer(self):
207 return self.__issuer
208
209 - def _setIssuer(self, value):
210 if not isinstance(value, basestring): 211 raise TypeError('Expecting string type for "issuer"; got %r' % 212 type(value)) 213 214 self.__issuer = value
215 216 issuer = property(fget=_getIssuer, 217 fset=_setIssuer, 218 doc="Name of issuing authority") 219
220 - def _getIssuerFormat(self):
221 if self.__issuerProxy is None: 222 return None 223 else: 224 return self.__issuerProxy.format
225
226 - def _setIssuerFormat(self, value):
227 if self.__issuerProxy is None: 228 self.__issuerProxy = Issuer() 229 230 self.__issuerProxy.format = value
231 232 issuerFormat = property(_getIssuerFormat, _setIssuerFormat, 233 doc="Issuer format") 234
235 - def _getIssuerName(self):
236 if self.__issuerProxy is None: 237 return None 238 else: 239 return self.__issuerProxy.value
240
241 - def _setIssuerName(self, value):
242 self.__issuerProxy.value = value
243 244 issuerName = property(_getIssuerName, _setIssuerName, 245 doc="Name of issuer of SAML Query Response") 246
247 - def _getVerifyTimeConditions(self):
248 return self.__verifyTimeConditions
249
250 - def _setVerifyTimeConditions(self, value):
251 if isinstance(value, bool): 252 self.__verifyTimeConditions = value 253 254 if isinstance(value, basestring): 255 self.__verifyTimeConditions = str2Bool(value) 256 else: 257 raise TypeError('Expecting bool or string type for ' 258 '"verifyTimeConditions"; got %r instead' % 259 type(value))
260 261 verifyTimeConditions = property(_getVerifyTimeConditions, 262 _setVerifyTimeConditions, 263 doc='Set to True to verify any time ' 264 'Conditions set in the returned ' 265 'response assertions') 266
267 - def _getVerifySAMLVersion(self):
268 return self.__verifySAMLVersion
269
270 - def _setVerifySAMLVersion(self, value):
271 if isinstance(value, bool): 272 self.__verifySAMLVersion = value 273 274 if isinstance(value, basestring): 275 self.__verifySAMLVersion = str2Bool(value) 276 else: 277 raise TypeError('Expecting bool or string type for ' 278 '"verifySAMLVersion"; got %r instead' % 279 type(value))
280 281 verifySAMLVersion = property(_getVerifySAMLVersion, 282 _setVerifySAMLVersion, 283 doc='Set to True to verify the SAML version ' 284 'set in the query against the SAML ' 285 'Version set in the "samlVersion" ' 286 'attribute') 287
288 - def _getClockSkewTolerance(self):
289 return self.__clockSkewTolerance
290
291 - def _setClockSkewTolerance(self, value):
292 if isinstance(value, timedelta): 293 self.__clockSkewTolerance = value 294 295 elif isinstance(value, (float, int, long)): 296 self.__clockSkewTolerance = timedelta(seconds=value) 297 298 elif isinstance(value, basestring): 299 self.__clockSkewTolerance = timedelta(seconds=float(value)) 300 else: 301 raise TypeError('Expecting timedelta, float, int, long or string ' 302 'type for "clockSkewTolerance"; got %r' % 303 type(value))
304 305 clockSkewTolerance = property(fget=_getClockSkewTolerance, 306 fset=_setClockSkewTolerance, 307 doc="Set a tolerance of +/- n seconds to " 308 "allow for clock skew when checking the " 309 "timestamps of client queries") 310
311 - def _getSamlVersion(self):
312 return self.__samlVersion
313
314 - def _setSamlVersion(self, value):
315 if not isinstance(value, basestring): 316 raise TypeError('Expecting string type for "samlVersion"; got %r' % 317 type(value)) 318 self.__samlVersion = value
319 320 samlVersion = property(_getSamlVersion, _setSamlVersion, None, 321 "SAML Version to enforce for incoming queries. " 322 "Defaults to version 2.0") 323
324 - def _getMountPath(self):
325 return self.__mountPath
326
327 - def _setMountPath(self, value):
328 ''' 329 @type value: basestring 330 @param value: URL paths to apply this middleware to. Paths are relative 331 to the point at which this middleware is mounted as set in 332 environ['PATH_INFO'] 333 @raise TypeError: incorrect input type 334 ''' 335 336 if not isinstance(value, basestring): 337 raise TypeError('Expecting string type for "mountPath" attribute; ' 338 'got %r' % value) 339 340 self.__mountPath = value
341 342 mountPath = property(fget=_getMountPath, 343 fset=_setMountPath, 344 doc='URL path to mount this application equivalent to ' 345 'environ[\'PATH_INFO\'] (Nb. doesn\'t ' 346 'include server domain name or ' 347 'environ[\'SCRIPT_NAME\'] setting') 348 349 @classmethod
350 - def filter_app_factory(cls, app, global_conf, **app_conf):
351 """Set-up using a Paste app factory pattern. Set this method to avoid 352 possible conflicts from multiple inheritance 353 354 @type app: callable following WSGI interface 355 @param app: next middleware application in the chain 356 @type global_conf: dict 357 @param global_conf: PasteDeploy global configuration dictionary 358 @type prefix: basestring 359 @param prefix: prefix for configuration items 360 @type app_conf: dict 361 @param app_conf: PasteDeploy application specific configuration 362 dictionary 363 """ 364 app = cls(app) 365 app.initialise(global_conf, **app_conf) 366 367 return app
368
370 return self.__queryInterfaceKeyName
371
372 - def _setQueryInterfaceKeyName(self, value):
373 if not isinstance(value, basestring): 374 raise TypeError('Expecting string type for "queryInterfaceKeyName"' 375 ' got %r' % value) 376 377 self.__queryInterfaceKeyName = value
378 379 queryInterfaceKeyName = property(fget=_getQueryInterfaceKeyName, 380 fset=_setQueryInterfaceKeyName, 381 doc="environ key name for Attribute Query " 382 "interface") 383
384 - def __call__(self, environ, start_response):
385 """Check for and parse a SOAP SAML Attribute Query and return a 386 SAML Response 387 388 @type environ: dict 389 @param environ: WSGI environment variables dictionary 390 @type start_response: function 391 @param start_response: standard WSGI start response function 392 """ 393 394 # Ignore non-matching path 395 if environ['PATH_INFO'] not in (self.mountPath, self.mountPath + '/'): 396 return self._app(environ, start_response) 397 398 # Ignore non-POST requests 399 if environ.get('REQUEST_METHOD') != 'POST': 400 return self._app(environ, start_response) 401 402 soapRequestStream = environ.get('wsgi.input') 403 if soapRequestStream is None: 404 raise SOAPQueryInterfaceMiddlewareError('No "wsgi.input" in ' 405 'environ') 406 407 # TODO: allow for chunked data 408 contentLength = environ.get('CONTENT_LENGTH') 409 if contentLength is None: 410 raise SOAPQueryInterfaceMiddlewareError('No "CONTENT_LENGTH" in ' 411 'environ') 412 413 contentLength = int(contentLength) 414 if contentLength <= 0: 415 raise SOAPQueryInterfaceMiddlewareError('"CONTENT_LENGTH" in ' 416 'environ is %d' % 417 contentLength) 418 419 soapRequestTxt = soapRequestStream.read(contentLength) 420 421 # Parse into a SOAP envelope object 422 soapRequest = SOAPEnvelope() 423 soapRequest.parse(StringIO(soapRequestTxt)) 424 425 log.debug("SOAPQueryInterfaceMiddleware.__call__: received SAML " 426 "SOAP Query: %s", soapRequestTxt) 427 428 queryElem = soapRequest.body.elem[0] 429 430 # Create a response with basic attributes if provided in the 431 # initialisation config 432 samlResponse = self._initResponse() 433 434 try: 435 queryType = QName.getLocalPart(queryElem.tag) 436 if queryType == XACMLAuthzDecisionQuery.DEFAULT_ELEMENT_LOCAL_NAME: 437 # Set up additional ElementTree parsing for XACML profile. 438 etree_xacml_profile.setElementTreeMap() 439 samlQuery = self.deserialiseXacmlProfile(queryElem) 440 else: 441 samlQuery = self.deserialise(queryElem) 442 443 except UnknownAttrProfile, e: 444 log.exception("%r raised parsing incoming query: %s" % 445 (type(e), traceback.format_exc())) 446 samlResponse.status.statusCode.value = \ 447 StatusCode.UNKNOWN_ATTR_PROFILE_URI 448 else: 449 # Check for Query Interface in environ 450 queryInterface = environ.get(self.queryInterfaceKeyName, 451 NotImplemented) 452 if queryInterface == NotImplemented: 453 raise SOAPQueryInterfaceMiddlewareConfigError( 454 'No query interface %r key found in environ' % 455 self.queryInterfaceKeyName) 456 457 elif not callable(queryInterface): 458 raise SOAPQueryInterfaceMiddlewareConfigError( 459 'Query interface %r set in %r environ key is not callable' % 460 (queryInterface, self.queryInterfaceKeyName)) 461 462 # Basic validation 463 self._validateQuery(samlQuery, samlResponse) 464 465 samlResponse.inResponseTo = samlQuery.id 466 467 # Call query interface 468 queryInterface(samlQuery, samlResponse) 469 470 # Convert to ElementTree representation to enable attachment to SOAP 471 # response body 472 samlResponseElem = self.serialise(samlResponse) 473 474 # Create SOAP response and attach the SAML Response payload 475 soapResponse = SOAPEnvelope() 476 soapResponse.create() 477 soapResponse.body.elem.append(samlResponseElem) 478 479 response = soapResponse.serialize() 480 481 log.debug("SOAPQueryInterfaceMiddleware.__call__: sending response " 482 "...\n\n%s", 483 response) 484 start_response("200 OK", 485 [('Content-length', str(len(response))), 486 ('Content-type', 'text/xml')]) 487 return [response]
488
489 - def _validateQuery(self, query, response):
490 """Checking incoming query issue instant and version 491 @type query: saml.saml2.core.SubjectQuery 492 @param query: SAML subject query to be checked 493 @type: saml.saml2.core.Response 494 @param: SAML Response 495 """ 496 self._verifyQueryTimeConditions(query, response) 497 self._verifyQuerySAMLVersion(query, response)
498
499 - def _verifyQueryTimeConditions(self, query, response):
500 """Checking incoming query issue instant 501 @type query: saml.saml2.core.SubjectQuery 502 @param query: SAML subject query to be checked 503 @type: saml.saml2.core.Response 504 @param: SAML Response 505 @raise QueryIssueInstantInvalid: for invalid issue instant 506 """ 507 if not self.verifyTimeConditions: 508 log.debug("Skipping verification of SAML query time conditions") 509 return 510 511 utcNow = datetime.utcnow() 512 nowPlusSkew = utcNow + self.clockSkewTolerance 513 514 if query.issueInstant > nowPlusSkew: 515 msg = ('SAML Attribute Query issueInstant [%s] is after ' 516 'the clock time [%s] (skewed +%s)' % 517 (query.issueInstant, 518 SAMLDateTime.toString(nowPlusSkew), 519 self.clockSkewTolerance)) 520 521 samlRespError = QueryIssueInstantInvalid(msg) 522 samlRespError.response = response 523 raise samlRespError
524
525 - def _verifyQuerySAMLVersion(self, query, response):
526 """Checking incoming query issue SAML version 527 528 @type query: saml.saml2.core.SubjectQuery 529 @param query: SAML subject query to be checked 530 @type: saml.saml2.core.Response 531 @param: SAML Response 532 """ 533 if not self.verifySAMLVersion: 534 log.debug("Skipping verification of SAML query version") 535 return 536 537 if query.version < self.samlVersion: 538 log.debug("Query SAML version %r is lower than the supported " 539 "value %r", query.version, self.samlVersion) 540 response.status.statusCode.value = \ 541 StatusCode.REQUEST_VERSION_TOO_LOW_URI 542 543 elif query.version > self.samlVersion: 544 log.debug("Query SAML version %r is higher than the supported " 545 "value %r", query.version, self.samlVersion) 546 response.status.statusCode.value = \ 547 StatusCode.REQUEST_VERSION_TOO_HIGH_URI
548 549
550 - def _initResponse(self):
551 """Create a SAML Response object with basic settings if any have been 552 provided at initialisation of this class - see initialise 553 554 @return: SAML response object 555 @rtype: ndg.saml.saml2.core.Response 556 """ 557 samlResponse = Response() 558 utcNow = datetime.utcnow() 559 560 samlResponse.issueInstant = utcNow 561 samlResponse.id = str(uuid4()) 562 samlResponse.issuer = Issuer() 563 564 if self.issuerName is not None: 565 samlResponse.issuer.value = self.issuerName 566 567 if self.issuerFormat is not None: 568 # TODO: Check SAML 2.0 spec says issuer format must be omitted?? 569 samlResponse.issuer.format = self.issuerFormat 570 571 # Initialise to success status but reset on error 572 samlResponse.status = Status() 573 samlResponse.status.statusCode = StatusCode() 574 samlResponse.status.statusMessage = StatusMessage() 575 samlResponse.status.statusCode.value = StatusCode.SUCCESS_URI 576 577 samlResponse.status.statusMessage = StatusMessage() 578 579 return samlResponse
580