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