#5 Logout over SOAP
Merged 8 years ago by puiterwijk. Opened 8 years ago by rcritten.
rcritten/ipsilon soap_logout  into  master

@@ -97,6 +97,8 @@ 

      m.set_entity_id(url_sp)

      m.add_certs(c)

      m.add_service(SAML2_SERVICE_MAP['logout-redirect'], url_logout)

+     if not args['no_saml_soap_logout']:

+         m.add_service(SAML2_SERVICE_MAP['slo-soap'], url_logout)

      m.add_service(SAML2_SERVICE_MAP['response-post'], url_post, index="0")

      m.add_allowed_name_format(SAML2_NAMEID_MAP[args['saml_nameid']])

      sp_metafile = os.path.join(path, 'metadata.xml')
@@ -334,6 +336,9 @@ 

                          help="Single Logout URL")

      parser.add_argument('--saml-sp-post', default=None,

                          help="Post response URL")

+     parser.add_argument('--no-saml-soap-logout', action='store_true',

+                         default=False,

+                         help="Disable Single Logout over SOAP")

      parser.add_argument('--saml-secure-setup', action='store_true',

                          default=True, help="Turn on all security checks")

      parser.add_argument('--saml-nameid', default='unspecified',

@@ -278,10 +278,13 @@ 

  

          lasso_session = lasso.Session()

          lasso_session.addAssertion(login.remoteProviderId, login.assertion)

+         provider = ServiceProvider(self.cfg, login.remoteProviderId)

          saml_sessions.add_session(login.assertion.id,

                                    login.remoteProviderId,

                                    user.name,

-                                   lasso_session.dump())

+                                   lasso_session.dump(),

+                                   None,

+                                   provider.logout_mechs)

  

      def saml2error(self, login, code, message):

          status = lasso.Samlp2Status()

@@ -4,8 +4,10 @@ 

  from ipsilon.providers.common import InvalidRequest

  from ipsilon.providers.saml2.auth import UnknownProvider

  from ipsilon.util.user import UserSession

+ from ipsilon.util.constants import SOAP_MEDIA_TYPE

  import cherrypy

  import lasso

+ import requests

  

  

  class LogoutRequest(ProviderPageBase):
@@ -58,7 +60,7 @@ 

          # all the session indexes and mark them as logging out but only one

          # is needed to handle the request.

          if len(session_indexes) < 1:

-             self.error('SLO empty session Indexes: %s')

+             self.error('SLO empty session Indexes')

              raise cherrypy.HTTPError(400, 'Invalid logout request')

          session = saml_sessions.get_session_by_id(session_indexes[0])

          if session:
@@ -181,11 +183,36 @@ 

          else:

              raise cherrypy.HTTPError(400, 'Not logged in')

  

+     def _soap_logout(self, logout):

+         """

+         Send a SOAP logout request over HTTP and return the result.

+         """

+         headers = {'Content-Type': SOAP_MEDIA_TYPE}

+         try:

+             response = requests.post(logout.msgUrl, data=logout.msgBody,

+                                      headers=headers)

+         except Exception as e:  # pylint: disable=broad-except

+             self.error('SOAP HTTP request failed: (%s) (on %s)' %

+                        (e, logout.msgUrl))

+             raise

+ 

+         if response.status_code != 200:

+             self.error('SOAP error (%s) (on %s)' %

+                        (response.status, logout.msgUrl))

+             raise InvalidRequest('SOAP HTTP error code', response.status_code)

+ 

+         if not response.text:

+             self.error('Empty SOAP response')

+             raise InvalidRequest('No content in SOAP response')

+ 

+         return response.text

+ 

      def logout(self, message, relaystate=None, samlresponse=None):

          """

-         Handle HTTP Redirect logout. This is an asynchronous logout

-         request process that relies on the HTTP agent to forward

-         logout requests to any other SP's that are also logged in.

+         Handle HTTP logout. The supported logout methods are stored

+         in each session. First all the SOAP sessions are logged out

+         then the HTTP Redirect method is used for any remaining

+         sessions.

  

          The basic process is this:

           1. A logout request is received. It is processed and the response
@@ -198,6 +225,8 @@ 

           Repeat steps 2-3 until only the initial logout request is

           left unhandled, at which time the pre-generated response is sent

           back to the SP that originated the logout request.

+ 

+         The final logout response is always a redirect.

          """

          logout = self.cfg.idp.get_logout_handler()

  
@@ -217,8 +246,13 @@ 

          # Fall through to handle any remaining sessions.

  

          # Find the next SP to logout and send a LogoutRequest

-         session = saml_sessions.get_next_logout()

-         if session:

+         logout_order = [

+             lasso.SAML2_METADATA_BINDING_SOAP,

+             lasso.SAML2_METADATA_BINDING_REDIRECT,

+         ]

+         (logout_mech, session) = saml_sessions.get_next_logout(

+             logout_mechs=logout_order)

+         while session:

              self.debug('Going to log out %s' % session.provider_id)

  

              try:
@@ -227,8 +261,12 @@ 

                  self.error('Failed to load session: %s' % e)

                  raise cherrypy.HTTPRedirect(400, 'Failed to log out user: %s '

                                              % e)

- 

-             logout.initRequest(session.provider_id, lasso.HTTP_METHOD_REDIRECT)

+             if logout_mech == lasso.SAML2_METADATA_BINDING_REDIRECT:

+                 logout.initRequest(session.provider_id,

+                                    lasso.HTTP_METHOD_REDIRECT)

+             else:

+                 logout.initRequest(session.provider_id,

+                                    lasso.HTTP_METHOD_SOAP)

  

              try:

                  logout.buildRequestMsg()
@@ -243,7 +281,7 @@ 

              indexes = saml_sessions.get_session_id_by_provider_id(

                  session.provider_id

              )

-             self.debug('Requesting logout for sessions %s' % indexes)

+             self.debug('Requesting logout for sessions %s' % (indexes,))

              req = logout.get_request()

              req.setSessionIndexes(indexes)

  
@@ -253,13 +291,34 @@ 

  

              self.debug('Request logout ID %s for session ID %s' %

                         (logout.request.id, session.session_id))

-             self.debug('Redirecting to another SP to logout on %s at %s' %

-                        (logout.remoteProviderId, logout.msgUrl))

- 

-             raise cherrypy.HTTPRedirect(logout.msgUrl)

  

-         # Otherwise we're done, respond to the original request using the

-         # response we cached earlier.

+             if logout_mech == lasso.SAML2_METADATA_BINDING_REDIRECT:

+                 self.debug('Redirecting to another SP to logout on %s at %s' %

+                            (logout.remoteProviderId, logout.msgUrl))

+                 raise cherrypy.HTTPRedirect(logout.msgUrl)

+             else:

+                 self.debug('SOAP request to another SP to logout on %s at %s' %

+                            (logout.remoteProviderId, logout.msgUrl))

+                 if logout.msgBody:

+                     message = self._soap_logout(logout)

+                     try:

+                         self._handle_logout_response(us,

+                                                      logout,

+                                                      saml_sessions,

+                                                      message,

+                                                      samlresponse)

+                     except Exception as e:  # pylint: disable=broad-except

+                         self.error('SOAP SLO failed %s' % e)

+                 else:

+                     self.error('Provider does not support SOAP')

+ 

+             (logout_mech, session) = saml_sessions.get_next_logout(

+                 logout_mechs=logout_order)

+ 

+         # done while

+ 

+         # All sessions should be logged out now. Respond to the

+         # original request using the response we cached earlier.

  

          try:

              session = saml_sessions.get_initial_logout()

@@ -3,8 +3,9 @@ 

  from ipsilon.providers.common import ProviderException

  from ipsilon.util import config as pconfig

  from ipsilon.util.config import ConfigHelper

- from ipsilon.tools.saml2metadata import SAML2_NAMEID_MAP

+ from ipsilon.tools.saml2metadata import SAML2_NAMEID_MAP, NSMAP

  from ipsilon.util.log import Log

+ from lxml import etree

  import lasso

  import re

  
@@ -49,6 +50,14 @@ 

          self._properties = data[idval]

          self._staging = dict()

          self.load_config()

+         self.logout_mechs = []

+         xmldoc = etree.XML(str(data[idval]['metadata']))

+         logout = xmldoc.xpath('//md:EntityDescriptor'

+                               '/md:SPSSODescriptor'

+                               '/md:SingleLogoutService',

+                               namespaces=NSMAP)

+         for service in logout:

+             self.logout_mechs.append(service.values()[0])

  

      def load_config(self):

          self.new_config(

@@ -4,6 +4,10 @@ 

  from ipsilon.util.log import Log

  from ipsilon.util.data import SAML2SessionStore

  import datetime

+ from lasso import (

+     SAML2_METADATA_BINDING_SOAP,

+     SAML2_METADATA_BINDING_REDIRECT,

+ )

  

  LOGGED_IN = 1

  INIT_LOGOUT = 2
@@ -29,11 +33,13 @@ 

                      which matches this.

         logout_request - the Logout request object

         expiration_time - the time the login session expires

+        supported_logout_mechs - logout mechanisms supported by this session

      """

      def __init__(self, uuidval, session_id, provider_id, user,

                   login_session, logoutstate=None, relaystate=None,

                   logout_request=None, request_id=None,

-                  expiration_time=None):

+                  expiration_time=None,

+                  supported_logout_mechs=None):

  

          self.uuidval = uuidval

          self.session_id = session_id
@@ -45,6 +51,9 @@ 

          self.request_id = request_id

          self.logout_request = logout_request

          self.expiration_time = expiration_time

+         if supported_logout_mechs is None:

+             supported_logout_mechs = []

+         self.supported_logout_mechs = supported_logout_mechs

  

      def set_logoutstate(self, relaystate=None, request=None, request_id=None):

          """
@@ -66,6 +75,7 @@ 

          self.debug('provider_id %s' % self.provider_id)

          self.debug('login session %s' % self.login_session)

          self.debug('logoutstate %s' % self.logoutstate)

+         self.debug('logout mech %s' % self.supported_logout_mechs)

  

      def convert(self):

          """
@@ -118,12 +128,20 @@ 

                             data.get('relaystate'),

                             data.get('logout_request'),

                             data.get('request_id'),

-                            data.get('expiration_time'))

+                            data.get('expiration_time'),

+                            data.get('supported_logout_mechs'))

  

      def add_session(self, session_id, provider_id, user, login_session,

-                     request_id=None):

+                     request_id, supported_logout_mechs):

          """

          Add a new login session to the table.

+ 

+         :param session_id: The login session ID

+         :param provider_id: The URL of the SP

+         :param user: The NameID username

+         :param login_session: The lasso Login session

+         :param request_id: The request ID of the Logout

+         :param supported_logout_mechs: A list of logout protocols supported

          """

          self.user = user

  
@@ -136,9 +154,9 @@ 

                  'user': user,

                  'login_session': login_session,

                  'logoutstate': LOGGED_IN,

-                 'expiration_time': expiration_time}

-         if request_id:

-             data['request_id'] = request_id

+                 'expiration_time': expiration_time,

+                 'request_id': request_id,

+                 'supported_logout_mechs': supported_logout_mechs}

  

          uuidval = self._ss.new_session(data)

  
@@ -209,7 +227,8 @@ 

          datum = samlsession.convert()

          self._ss.update_session(datum)

  

-     def get_next_logout(self, peek=False):

+     def get_next_logout(self, peek=False,

+                         logout_mechs=None):

          """

          Get the next session in the logged-in state and move

          it to the logging_out state.  Return the session that is
@@ -218,24 +237,34 @@ 

          :param peek: for IdP-initiated logout we can't remove the

                       session otherwise when the request comes back

                       in the user won't be seen as being logged-on.

- 

-         Return None if no more sessions in LOGGED_IN state.

+         :param logout_mechs: An ordered list of logout mechanisms

+                      you're looking for. For each mechanism in order

+                      loop through all sessions. If If no sessions of

+                      this method are available then try the next mechanism

+                      until exhausted. In that case None is returned.

+ 

+         Returns a tuple of (mechanism, session) or

+         (None, None) if no more sessions in LOGGED_IN state.

          """

          candidates = self._ss.get_user_sessions(self.user)

- 

-         for c in candidates:

-             key = c.keys()[0]

-             if int(c[key].get('logoutstate', 0)) == LOGGED_IN:

-                 samlsession = self._data_to_samlsession(key, c[key])

-                 self.start_logout(samlsession, initial=False)

-                 return samlsession

-         return None

+         if logout_mechs is None:

+             logout_mechs = [SAML2_METADATA_BINDING_REDIRECT, ]

+ 

+         for mech in logout_mechs:

+             for c in candidates:

+                 key = c.keys()[0]

+                 if ((int(c[key].get('logoutstate', 0)) == LOGGED_IN) and

+                    (mech in c[key].get('supported_logout_mechs'))):

I don't think this can pass pep8/pylint, does it?
I think it's kinda unclear where indentation starts and ends.

+                     samlsession = self._data_to_samlsession(key, c[key])

+                     self.start_logout(samlsession, initial=False)

+                     return (mech, samlsession)

+         return (None, None)

  

      def get_initial_logout(self):

          """

          Get the initial logout request.

  

-         Return None if no sessions in INIT_LOGOUT state.

+         Raises ValueError if no sessions in INIT_LOGOUT state.

          """

          candidates = self._ss.get_user_sessions(self.user)

  
@@ -248,7 +277,7 @@ 

              if int(c[key].get('logoutstate', 0)) == INIT_LOGOUT:

                  samlsession = self._data_to_samlsession(key, c[key])

                  return samlsession

-         return None

+         raise ValueError()

  

      def wipe_data(self):

          self._ss.wipe_data()
@@ -276,14 +305,21 @@ 

      factory = SAMLSessionFactory('/tmp/saml2sessions.sqlite')

      factory.wipe_data()

  

-     sess1 = factory.add_session('_123456', provider1, "admin", "<Login/>")

-     sess2 = factory.add_session('_789012', provider2, "testuser", "<Login/>")

+     sess1 = factory.add_session('_123456', provider1, "admin",

+                                 "<Login/>", '_1234',

+                                 [SAML2_METADATA_BINDING_REDIRECT])

+     sess2 = factory.add_session('_789012', provider2, "testuser",

+                                 "<Login/>", '_7890',

+                                 [SAML2_METADATA_BINDING_SOAP,

+                                  SAML2_METADATA_BINDING_REDIRECT])

  

      # Test finding sessions by provider

      ids = factory.get_session_id_by_provider_id(provider2)

      assert(len(ids) == 1)

  

-     sess3 = factory.add_session('_345678', provider2, "testuser", "<Login/>")

+     sess3 = factory.add_session('_345678', provider2, "testuser",

+                                 "<Login/>", '_3456',

+                                 [SAML2_METADATA_BINDING_REDIRECT])

      ids = factory.get_session_id_by_provider_id(provider2)

      assert(len(ids) == 2)

  
@@ -307,7 +343,7 @@ 

      test2 = factory.get_session_by_id('_789012')

      factory.start_logout(test2, initial=True)

  

-     test3 = factory.get_next_logout()

+     (lmech, test3) = factory.get_next_logout()

      assert(test3.session_id == '_345678')

  

      test4 = factory.get_initial_logout()

file modified
+10 -4
@@ -131,7 +131,7 @@ 

          return self.auth(login)

  

  

- class RedirectLogout(LogoutRequest):

+ class Logout(LogoutRequest):

  

      def GET(self, *args, **kwargs):

          query = cherrypy.request.query_string
@@ -159,7 +159,7 @@ 

      def __init__(self, *args, **kwargs):

          super(SLO, self).__init__(*args, **kwargs)

          self.debug('SLO init')

-         self.Redirect = RedirectLogout(*args, **kwargs)

+         self.Redirect = Logout(*args, **kwargs)

  

  

  # one week
@@ -394,13 +394,18 @@ 

          Logout all SP sessions when the logout comes from the IdP.

  

          For the current user only.

+ 

+         Only use HTTP-Redirect to start the logout. This is guaranteed

+         to be supported in SAML 2.

          """

          self.debug("IdP-initiated SAML2 logout")

          us = UserSession()

          user = us.get_user()

  

          saml_sessions = self.sessionfactory

-         session = saml_sessions.get_next_logout()

+         # pylint: disable=unused-variable

+         (mech, session) = saml_sessions.get_next_logout(

+             logout_mechs=[lasso.SAML2_METADATA_BINDING_REDIRECT])

          if session is None:

              return

  
@@ -418,7 +423,8 @@ 

          # be redirected to when all SP's are logged out.

          idpurl = self._root.instance_base_url()

          session_id = "_" + uuid.uuid4().hex.upper()

-         saml_sessions.add_session(session_id, idpurl, user.name, "")

+         saml_sessions.add_session(session_id, idpurl, user.name, "", "",

+                                   [lasso.SAML2_METADATA_BINDING_REDIRECT])

          init_session = saml_sessions.get_session_by_id(session_id)

          saml_sessions.start_logout(init_session, relaystate=idpurl)

  

@@ -29,6 +29,8 @@ 

                   lasso.SAML2_METADATA_BINDING_SOAP),

      'logout-redirect': ('SingleLogoutService',

                          lasso.SAML2_METADATA_BINDING_REDIRECT),

+     'slo-soap': ('SingleLogoutService',

+                  lasso.SAML2_METADATA_BINDING_SOAP),

      'response-post': ('AssertionConsumerService',

                        lasso.SAML2_METADATA_BINDING_POST)

  }

file modified
+7 -1
@@ -551,6 +551,10 @@ 

          return self.get_unique_data(self.table, idval, name, value)

  

      def new_session(self, datum):

+         if 'supported_logout_mechs' in datum:

+             datum['supported_logout_mechs'] = ','.join(

+                 datum['supported_logout_mechs']

+             )

          return self.new_unique_data(self.table, datum)

  

      def get_session(self, session_id=None, request_id=None):
@@ -567,7 +571,7 @@ 

  

      def get_user_sessions(self, user):

          """

-         Retrun a list of all sessions for a given user.

+         Return a list of all sessions for a given user.

          """

          rows = self.get_unique_data(self.table, name='user', value=user)

  
@@ -575,6 +579,8 @@ 

          logged_in = []

          for r in rows:

              data = self.get_unique_data(self.table, uuidval=r)

+             data[r]['supported_logout_mechs'] = data[r].get(

+                 'supported_logout_mechs', '').split(',')

              logged_in.append(data)

  

          return logged_in

@@ -55,6 +55,9 @@ 

  \fB\-\-saml\-sp\-logout\fR \fISAML_SP_LOGOUT\fR

  Single Logout URL. The default is /saml2/logout.

  .TP

+ \fB\-\-no\-saml\-soap\-logout\fR

+ Disable Single Logout using SOAP.

+ .TP

  \fB\-\-saml\-sp\-post\fR \fISAML_SP_POST\fR

  Post response URL. The default is /saml2/postResponse.

  .TP

file modified
+110 -97
@@ -44,17 +44,42 @@ 

          'httpd_user': '${TEST_USER}'}

  

  

- sp2_g = {'HTTPDCONFD': '${TESTDIR}/${NAME}/conf.d',

-          'SAML2_TEMPLATE': '${TESTDIR}/templates/install/saml2/sp.conf',

-          'SAML2_CONFFILE': '${TESTDIR}/${NAME}/conf.d/ipsilon-saml.conf',

-          'SAML2_HTTPDIR': '${TESTDIR}/${NAME}/saml2'}

+ sp_b = {'hostname': '${ADDRESS}:${PORT}',

+         'saml_idp_metadata': 'http://127.0.0.10:45080/idp1/saml2/metadata',

+         'saml_secure_setup': 'False',

+         'no_saml_soap_logout': 'True',

+         'saml_auth': '/sp',

+         'httpd_user': '${TEST_USER}'}

  

  

- sp2_a = {'hostname': '${ADDRESS}:${PORT}',

-          'saml_idp_metadata': 'http://127.0.0.10:45080/idp1/saml2/metadata',

-          'saml_secure_setup': 'False',

-          'saml_auth': '/sp',

-          'httpd_user': '${TEST_USER}'}

+ # Global list of SP's

+ splist = [

+     {

+         'nameid': 'sp1',

+         'addr': '127.0.0.11',

+         'port': '45081',

+     },

+     {

+         'nameid': 'sp2',

+         'addr': '127.0.0.11',

+         'port': '45082',

+     },

+     {

+         'nameid': 'sp3',

+         'addr': '127.0.0.11',

+         'port': '45083',

+     },

+     {

+         'nameid': 'sp4',

+         'addr': '127.0.0.11',

+         'port': '45084',

+     },

+     {

+         'nameid': 'sp5',

+         'addr': '127.0.0.11',

+         'port': '45085',

+     },

+ ]

  

  

  def fixup_sp_httpd(httpdir):
@@ -87,7 +112,7 @@ 

          f.write(logged_out)

  

  

- def ensure_logout(session, idp_name, spurl):

+ def ensure_logout(session, idp_name, sp_url):

      """

      Fetch the secure page without following redirects. If we get

      a 303 then we should be redirected to the IDP for authentication
@@ -96,7 +121,7 @@ 

      Returns nothing or raises exception on error

      """

      try:

-         logout_page = session.fetch_page(idp_name, spurl,

+         logout_page = session.fetch_page(idp_name, sp_url,

                                           follow_redirect=False)

          if logout_page.result.status_code != 303:

              raise ValueError('Still logged into url')
@@ -122,40 +147,41 @@ 

          print "Starting IDP's httpd server"

          self.start_http_server(conf, env)

  

-         print "Installing SP server"

-         name = 'sp1'

-         addr = '127.0.0.11'

-         port = '45081'

-         sp = self.generate_profile(sp_g, sp_a, name, addr, port)

-         conf = self.setup_sp_server(sp, name, addr, port, env)

-         fixup_sp_httpd(os.path.dirname(conf))

- 

-         print "Starting SP's httpd server"

-         self.start_http_server(conf, env)

- 

-         print "Installing second SP server"

-         name = 'sp2'

-         addr = '127.0.0.10'

-         port = '45082'

-         sp2 = self.generate_profile(sp2_g, sp2_a, name, addr, port)

-         conf = self.setup_sp_server(sp2, name, addr, port, env)

-         fixup_sp_httpd(os.path.dirname(conf))

- 

-         print "Starting SP's httpd server"

-         self.start_http_server(conf, env)

+         for spdata in splist:

+             nameid = spdata['nameid']

+             addr = spdata['addr']

+             port = spdata['port']

+             print "Installing SP server %s" % nameid

+ 

+             # Configure sp3 and sp4 for only HTTP Redirect to test

+             # that a mix of SOAP and HTTP Redirect will play nice

+             # together.

+             if nameid in ['sp3', 'sp4']:

+                 sp_prof = self.generate_profile(

+                     sp_g, sp_b, nameid, addr, str(port), nameid

+                 )

+             else:

+                 sp_prof = self.generate_profile(

+                     sp_g, sp_a, nameid, addr, str(port), nameid

+                 )

+             conf = self.setup_sp_server(sp_prof, nameid, addr, str(port), env)

+             fixup_sp_httpd(os.path.dirname(conf))

+ 

+             print "Starting SP's httpd server"

+             self.start_http_server(conf, env)

  

  

  if __name__ == '__main__':

  

      idpname = 'idp1'

-     spname = 'sp1'

-     sp2name = 'sp2'

      user = pwd.getpwuid(os.getuid())[0]

  

      sess = HttpSessions()

      sess.add_server(idpname, 'http://127.0.0.10:45080', user, 'ipsilon')

-     sess.add_server(spname, 'http://127.0.0.11:45081')

-     sess.add_server(sp2name, 'http://127.0.0.10:45082')

+     for sp in splist:

+         spname = sp['nameid']

+         spurl = 'http://%s:%s' % (sp['addr'], sp['port'])

+         sess.add_server(spname, spurl)

  

      print "testlogout: Authenticate to IDP ...",

      try:
@@ -165,21 +191,15 @@ 

          sys.exit(1)

      print " SUCCESS"

  

-     print "testlogout: Add SP Metadata to IDP ...",

-     try:

-         sess.add_sp_metadata(idpname, spname)

-     except Exception, e:  # pylint: disable=broad-except

-         print >> sys.stderr, " ERROR: %s" % repr(e)

-         sys.exit(1)

-     print " SUCCESS"

- 

-     print "testlogout: Add second SP Metadata to IDP ...",

-     try:

-         sess.add_sp_metadata(idpname, sp2name)

-     except Exception, e:  # pylint: disable=broad-except

-         print >> sys.stderr, " ERROR: %s" % repr(e)

-         sys.exit(1)

-     print " SUCCESS"

+     for sp in splist:

+         spname = sp['nameid']

+         print "testlogout: Add SP Metadata for %s to IDP ..." % spname,

+         try:

+             sess.add_sp_metadata(idpname, spname)

+         except Exception, e:  # pylint: disable=broad-except

+             print >> sys.stderr, " ERROR: %s" % repr(e)

+             sys.exit(1)

+         print " SUCCESS"

  

      print "testlogout: Logout without logging into SP ...",

      try:
@@ -231,50 +251,43 @@ 

          sys.exit(1)

      print " SUCCESS"

  

-     print "testlogout: Access SP Protected Area of SP1...",

-     try:

-         page = sess.fetch_page(idpname, 'http://127.0.0.11:45081/sp/')

-         page.expected_value('text()', 'WORKS!')

-     except ValueError, e:

-         print >> sys.stderr, " ERROR: %s" % repr(e)

-         sys.exit(1)

-     print " SUCCESS"

- 

-     print "testlogout: Access SP Protected Area of SP2...",

-     try:

-         page = sess.fetch_page(idpname, 'http://127.0.0.10:45082/sp/')

-         page.expected_value('text()', 'WORKS!')

-     except ValueError, e:

-         print >> sys.stderr, " ERROR: %s" % repr(e)

-         sys.exit(1)

-     print " SUCCESS"

- 

-     print "testlogout: Logout from both ...",

-     try:

-         page = sess.fetch_page(idpname, '%s/%s?%s' % (

-             'http://127.0.0.11:45081', 'saml2/logout',

-             'ReturnTo=http://127.0.0.11:45081/open/logged_out.html'))

-         page.expected_value('text()', 'Logged out')

-     except ValueError, e:

-         print >> sys.stderr, " ERROR: %s" % repr(e)

-         sys.exit(1)

-     print " SUCCESS"

- 

-     print "testlogout: Ensure logout of SP1 ...",

-     try:

-         ensure_logout(sess, idpname, 'http://127.0.0.11:45081/sp/')

-     except ValueError, e:

-         print >> sys.stderr, " ERROR: %s" % repr(e)

-         sys.exit(1)

-     print " SUCCESS"

- 

-     print "testlogout: Ensure logout of SP2 ...",

-     try:

-         ensure_logout(sess, idpname, 'http://127.0.0.10:45082/sp/')

-     except ValueError, e:

-         print >> sys.stderr, " ERROR: %s" % repr(e)

-         sys.exit(1)

-     print " SUCCESS"

+     # Test logout from each of the SP's in the list to ensure that the

+     # order of logout doesn't matter.

+     for sporder in splist:

+         print "testlogout: Access SP Protected Area of each SP ...",

+         for sp in splist:

+             spname = sp['nameid']

+             spurl = 'http://%s:%s/sp/' % (sp['addr'], sp['port'])

+             try:

+                 page = sess.fetch_page(idpname, spurl)

+                 page.expected_value('text()', 'WORKS!')

+             except ValueError, e:

+                 print >> sys.stderr, " ERROR: %s" % repr(e)

+                 sys.exit(1)

+         print " SUCCESS"

+ 

+         print "testlogout: Initiate logout from %s ..." % sporder['nameid'],

+         try:

+             logouturl = 'http://%s:%s' % (sp['addr'], sp['port'])

+             page = sess.fetch_page(idpname, '%s/%s?%s' % (

+                 logouturl, 'saml2/logout',

+                 'ReturnTo=http://127.0.0.11:45081/open/logged_out.html'))

+             page.expected_value('text()', 'Logged out')

+         except ValueError, e:

+             print >> sys.stderr, " ERROR: %s" % repr(e)

+             sys.exit(1)

+         print " SUCCESS"

+ 

+         print "testlogout: Ensure logout of each SP ...",

+         for sp in splist:

+             spname = sp['nameid']

+             spurl = 'http://%s:%s/sp/' % (sp['addr'], sp['port'])

+             try:

+                 ensure_logout(sess, idpname, spurl)

+             except ValueError, e:

+                 print >> sys.stderr, " ERROR: %s" % repr(e)

+                 sys.exit(1)

+         print " SUCCESS"

  

      # Test IdP-initiated logout

      print "testlogout: Access SP Protected Area of SP1...",
@@ -288,7 +301,7 @@ 

  

      print "testlogout: Access SP Protected Area of SP2...",

      try:

-         page = sess.fetch_page(idpname, 'http://127.0.0.10:45082/sp/')

+         page = sess.fetch_page(idpname, 'http://127.0.0.11:45082/sp/')

          page.expected_value('text()', 'WORKS!')

      except ValueError, e:

          print >> sys.stderr, " ERROR: %s" % repr(e)
@@ -325,7 +338,7 @@ 

  

      print "testlogout: Ensure logout of SP2 ...",

      try:

-         ensure_logout(sess, idpname, 'http://127.0.0.10:45082/sp/')

+         ensure_logout(sess, idpname, 'http://127.0.0.11:45082/sp/')

      except ValueError, e:

          print >> sys.stderr, " ERROR: %s" % repr(e)

          sys.exit(1)

no initial comment

I think you might want to rename this to --no-saml-soap-logout.

I'm ok to rename the option.

The .rd is basically a typo, probably a remnant of a previous comment I failed to clean up.

Maybe rephrase this to something like "Done logging out all sessions"?

I think this is a bad idea.
Instead, I would suggest you to make logout_mech=None and then have an "if not logout_mech: logout_mech = [].

There is a reason "[]" is called a dangerous default value :-)

Maybe rename to supported_logout_mechs or something else plural?

I think this might be a slightly confusing variable name.
Just having the join in tdata initialization shouldn't be too much a problem I'd say?

Wouldn't it be better to instead make this a prioritized list or something?
That way, the while loop that handles the actual logging out can be easier because it doesn't need the (in my opinion) confusing "if session is None: session = ..." at the end of the loop

This argument would DISABLE logout if provided, it does not accept any arguments (as it's a store_false).

I don't think this can pass pep8/pylint, does it?
I think it's kinda unclear where indentation starts and ends.

I've made a small change to the indentation, but otherwise looks good to me.