From f378f17dee28007b12512db08045c7590d919f35 Mon Sep 17 00:00:00 2001 From: Rob Crittenden Date: Jul 15 2015 20:55:31 +0000 Subject: [PATCH 1/3] Add support for logout over SOAP As each login session comes in, store the supported logout mechanisms in the SP metadata. Upon a logout request, loop through all of those SP's that support SOAP and log those out first, then log out any remaining sessions using HTTP Redirect. https://fedorahosted.org/ipsilon/ticket/59 Signed-off-by: Rob Crittenden --- diff --git a/ipsilon/install/ipsilon-client-install b/ipsilon/install/ipsilon-client-install index 49d9e78..d8a310c 100755 --- a/ipsilon/install/ipsilon-client-install +++ b/ipsilon/install/ipsilon-client-install @@ -97,6 +97,7 @@ def saml2(): m.set_entity_id(url_sp) m.add_certs(c) m.add_service(SAML2_SERVICE_MAP['logout-redirect'], url_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') diff --git a/ipsilon/providers/saml2/auth.py b/ipsilon/providers/saml2/auth.py index c46d604..d856220 100644 --- a/ipsilon/providers/saml2/auth.py +++ b/ipsilon/providers/saml2/auth.py @@ -278,10 +278,13 @@ class AuthenticateRequest(ProviderPageBase): 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() diff --git a/ipsilon/providers/saml2/logout.py b/ipsilon/providers/saml2/logout.py index cc9b777..374e885 100644 --- a/ipsilon/providers/saml2/logout.py +++ b/ipsilon/providers/saml2/logout.py @@ -4,8 +4,10 @@ from ipsilon.providers.common import ProviderPageBase 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 @@ class LogoutRequest(ProviderPageBase): # 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 @@ class LogoutRequest(ProviderPageBase): 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 @@ class LogoutRequest(ProviderPageBase): 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 @@ class LogoutRequest(ProviderPageBase): # 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 @@ class LogoutRequest(ProviderPageBase): 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 @@ class LogoutRequest(ProviderPageBase): 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 @@ class LogoutRequest(ProviderPageBase): 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() diff --git a/ipsilon/providers/saml2/provider.py b/ipsilon/providers/saml2/provider.py index 3dea631..b70582e 100644 --- a/ipsilon/providers/saml2/provider.py +++ b/ipsilon/providers/saml2/provider.py @@ -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 @@ class ServiceProvider(ServiceProviderConfig): 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( diff --git a/ipsilon/providers/saml2/sessions.py b/ipsilon/providers/saml2/sessions.py index 1000a87..f40a80f 100644 --- a/ipsilon/providers/saml2/sessions.py +++ b/ipsilon/providers/saml2/sessions.py @@ -4,6 +4,10 @@ from cherrypy import config as cherrypy_config 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 @@ class SAMLSession(Log): 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 @@ class SAMLSession(Log): 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 @@ class SAMLSession(Log): 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 @@ class SAMLSessionFactory(Log): 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 @@ class SAMLSessionFactory(Log): '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 @@ class SAMLSessionFactory(Log): 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 @@ class SAMLSessionFactory(Log): :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'))): + 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 @@ class SAMLSessionFactory(Log): 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 @@ if __name__ == '__main__': factory = SAMLSessionFactory('/tmp/saml2sessions.sqlite') factory.wipe_data() - sess1 = factory.add_session('_123456', provider1, "admin", "") - sess2 = factory.add_session('_789012', provider2, "testuser", "") + sess1 = factory.add_session('_123456', provider1, "admin", + "", '_1234', + [SAML2_METADATA_BINDING_REDIRECT]) + sess2 = factory.add_session('_789012', provider2, "testuser", + "", '_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", "") + sess3 = factory.add_session('_345678', provider2, "testuser", + "", '_3456', + [SAML2_METADATA_BINDING_REDIRECT]) ids = factory.get_session_id_by_provider_id(provider2) assert(len(ids) == 2) @@ -307,7 +343,7 @@ if __name__ == '__main__': 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() diff --git a/ipsilon/providers/saml2idp.py b/ipsilon/providers/saml2idp.py index f771ef7..5ac83dd 100644 --- a/ipsilon/providers/saml2idp.py +++ b/ipsilon/providers/saml2idp.py @@ -131,7 +131,7 @@ class Continue(AuthenticateRequest): return self.auth(login) -class RedirectLogout(LogoutRequest): +class Logout(LogoutRequest): def GET(self, *args, **kwargs): query = cherrypy.request.query_string @@ -159,7 +159,7 @@ class SLO(ProviderPageBase): 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 @@ Provides SAML 2.0 authentication infrastructure. """ 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 @@ Provides SAML 2.0 authentication infrastructure. """ # 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) diff --git a/ipsilon/tools/saml2metadata.py b/ipsilon/tools/saml2metadata.py index 99857bf..98e7c67 100755 --- a/ipsilon/tools/saml2metadata.py +++ b/ipsilon/tools/saml2metadata.py @@ -29,6 +29,8 @@ SAML2_SERVICE_MAP = { 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) } diff --git a/ipsilon/util/data.py b/ipsilon/util/data.py index 53a1756..e0cd6e1 100644 --- a/ipsilon/util/data.py +++ b/ipsilon/util/data.py @@ -551,6 +551,10 @@ class SAML2SessionStore(Store): 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 @@ class SAML2SessionStore(Store): 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 @@ class SAML2SessionStore(Store): 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 From 75e533831deb5fa8654799dab311bae2484340b9 Mon Sep 17 00:00:00 2001 From: Rob Crittenden Date: Jul 15 2015 20:55:31 +0000 Subject: [PATCH 2/3] Add client install option to disable logout over SOAP This is at least somewhat for testing to ensure that a mixed environment works but HTTP Redirect is the recommended mechanism in the spec so some may want to use only that. https://fedorahosted.org/ipsilon/ticket/59 Signed-off-by: Rob Crittenden --- diff --git a/ipsilon/install/ipsilon-client-install b/ipsilon/install/ipsilon-client-install index d8a310c..452c7e0 100755 --- a/ipsilon/install/ipsilon-client-install +++ b/ipsilon/install/ipsilon-client-install @@ -97,7 +97,8 @@ def saml2(): m.set_entity_id(url_sp) m.add_certs(c) m.add_service(SAML2_SERVICE_MAP['logout-redirect'], url_logout) - m.add_service(SAML2_SERVICE_MAP['slo-soap'], 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') @@ -335,6 +336,9 @@ def parse_args(): 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', diff --git a/man/ipsilon-client-install.1 b/man/ipsilon-client-install.1 index 886fb48..5624d2f 100644 --- a/man/ipsilon-client-install.1 +++ b/man/ipsilon-client-install.1 @@ -55,6 +55,9 @@ Where saml communication happens. The default is /saml2. \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 From 97ab69b7c0127e95fe461d3d8b87e7aef6f8553d Mon Sep 17 00:00:00 2001 From: Rob Crittenden Date: Jul 15 2015 20:55:31 +0000 Subject: [PATCH 3/3] Refactor SP generation to simplify logout testing This adds the ability to quickly and easily add more SPs as needed to test more complex logout scenarios. Create five SP's, two of which support only HTTP-Redirect to ensure that logout works in a mixed environment. https://fedorahosted.org/ipsilon/ticket/59 Signed-off-by: Rob Crittenden --- diff --git a/tests/testlogout.py b/tests/testlogout.py index dad1019..efc4934 100755 --- a/tests/testlogout.py +++ b/tests/testlogout.py @@ -44,17 +44,42 @@ sp_a = {'hostname': '${ADDRESS}:${PORT}', '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 @@ Alias /open ${HTTPDIR}/open 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 @@ def ensure_logout(session, idp_name, spurl): 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 @@ class IpsilonTest(IpsilonTestBase): 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 @@ if __name__ == '__main__': 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 @@ if __name__ == '__main__': 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 @@ if __name__ == '__main__': 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 @@ if __name__ == '__main__': 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)