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)
42
45 """Base class for WSGI SAML 2.0 SOAP Query Interface Errors"""
46
49 """WSGI SAML 2.0 SOAP Query Interface Configuration problem"""
50
53 """Invalid timestamp for incoming query"""
54
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
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
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
156
166
167 serialise = property(_getSerialise, _setSerialise,
168 doc="callable to serialise request into XML type")
169
172
182
183 deserialise = property(_getDeserialise,
184 _setDeserialise,
185 doc="callable to de-serialise response from XML "
186 "type")
187
189 return self.__deserialiseXacmlProfile
190
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
208
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
225
231
232 issuerFormat = property(_getIssuerFormat, _setIssuerFormat,
233 doc="Issuer format")
234
236 if self.__issuerProxy is None:
237 return None
238 else:
239 return self.__issuerProxy.value
240
243
244 issuerName = property(_getIssuerName, _setIssuerName,
245 doc="Name of issuer of SAML Query Response")
246
249
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
268 return self.__verifySAMLVersion
269
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
290
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
312 return self.__samlVersion
313
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
325 return self.__mountPath
326
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
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
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
395 if environ['PATH_INFO'] not in (self.mountPath, self.mountPath + '/'):
396 return self._app(environ, start_response)
397
398
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
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
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
431
432 samlResponse = self._initResponse()
433
434 try:
435 queryType = QName.getLocalPart(queryElem.tag)
436 if queryType == XACMLAuthzDecisionQuery.DEFAULT_ELEMENT_LOCAL_NAME:
437
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
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
463 self._validateQuery(samlQuery, samlResponse)
464
465 samlResponse.inResponseTo = samlQuery.id
466
467
468 queryInterface(samlQuery, samlResponse)
469
470
471
472 samlResponseElem = self.serialise(samlResponse)
473
474
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
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
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
548
549
580