X-Git-Url: http://git.cascardo.eti.br/?a=blobdiff_plain;f=ipsilon%2Fproviders%2Fsaml2idp.py;h=93dcbc6c448828e3ca29104ac286124263a4dbb0;hb=07ec779defce9b0fecf4da8c726d1b492c147626;hp=cec3e88dae8270c14cbeb3ce29e8d628ac59a866;hpb=d51e2b7ff3b1ea8b41bd43d2b554398fc9b3e636;p=cascardo%2Fipsilon.git diff --git a/ipsilon/providers/saml2idp.py b/ipsilon/providers/saml2idp.py index cec3e88..93dcbc6 100644 --- a/ipsilon/providers/saml2idp.py +++ b/ipsilon/providers/saml2idp.py @@ -1,19 +1,4 @@ -# Copyright (C) 2014 Simo Sorce -# -# see file 'COPYING' for use and warranty information -# -# This program is free software; you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see . +# Copyright (C) 2014 Ipsilon project Contributors, for license see COPYING from ipsilon.providers.common import ProviderBase, ProviderPageBase, \ ProviderInstaller @@ -22,9 +7,13 @@ from ipsilon.providers.saml2.logout import LogoutRequest from ipsilon.providers.saml2.admin import Saml2AdminPage from ipsilon.providers.saml2.rest import Saml2RestBase from ipsilon.providers.saml2.provider import IdentityProvider +from ipsilon.providers.saml2.sessions import SAMLSessionFactory +from ipsilon.util.data import SAML2SessionStore from ipsilon.tools.certs import Certificate from ipsilon.tools import saml2metadata as metadata from ipsilon.tools import files +from ipsilon.util.http import require_content_type +from ipsilon.util.constants import SOAP_MEDIA_TYPE, XML_MEDIA_TYPE from ipsilon.util.user import UserSession from ipsilon.util.plugin import PluginObject from ipsilon.util import config as pconfig @@ -35,9 +24,54 @@ import os import time import uuid +cherrypy.tools.require_content_type = cherrypy.Tool('before_request_body', + require_content_type) + + +def is_lasso_ecp_enabled(): + # Full ECP support appeared in lasso version 2.4.2 + return lasso.checkVersion(2, 4, 2, lasso.CHECK_VERSION_NUMERIC) + + +class SSO_SOAP(AuthenticateRequest): + + def __init__(self, *args, **kwargs): + super(SSO_SOAP, self).__init__(*args, **kwargs) + self.binding = metadata.SAML2_SERVICE_MAP['sso-soap'][1] + + @cherrypy.tools.require_content_type( + required=[SOAP_MEDIA_TYPE, XML_MEDIA_TYPE]) + @cherrypy.tools.accept(media=[SOAP_MEDIA_TYPE, XML_MEDIA_TYPE]) + @cherrypy.tools.response_headers( + headers=[('Content-Type', 'SOAP_MEDIA_TYPE')]) + def POST(self, *args, **kwargs): + self.debug("SSO_SOAP.POST() begin") + + self.debug("SSO_SOAP transaction provider=%s id=%s" % + (self.trans.provider, self.trans.transaction_id)) + + us = UserSession() + us.remote_login() + user = us.get_user() + self.debug("SSO_SOAP user=%s" % (user.name)) + + if not user: + raise cherrypy.HTTPError(403, 'No user specified for SSO_SOAP') + + soap_xml_doc = cherrypy.request.rfile.read() + soap_xml_doc = soap_xml_doc.strip() + self.debug("SSO_SOAP soap_xml_doc=%s" % soap_xml_doc) + login = self.saml2login(soap_xml_doc) + + return self.auth(login) + class Redirect(AuthenticateRequest): + def __init__(self, *args, **kwargs): + super(Redirect, self).__init__(*args, **kwargs) + self.binding = metadata.SAML2_SERVICE_MAP['sso-redirect'][1] + def GET(self, *args, **kwargs): query = cherrypy.request.query_string @@ -48,6 +82,10 @@ class Redirect(AuthenticateRequest): class POSTAuth(AuthenticateRequest): + def __init__(self, *args, **kwargs): + super(POSTAuth, self).__init__(*args, **kwargs) + self.binding = metadata.SAML2_SERVICE_MAP['sso-post'][1] + def POST(self, *args, **kwargs): request = kwargs.get(lasso.SAML2_FIELD_REQUEST) @@ -68,14 +106,14 @@ class Continue(AuthenticateRequest): self.stage = transdata['saml2_stage'] if user.is_anonymous: - self._debug("User is marked anonymous?!") + self.debug("User is marked anonymous?!") # TODO: Return to SP with auth failed error raise cherrypy.HTTPError(401) - self._debug('Continue auth for %s' % user.name) + self.debug('Continue auth for %s' % user.name) if 'saml2_request' not in transdata: - self._debug("Couldn't find Request dump?!") + self.debug("Couldn't find Request dump?!") # TODO: Return to SP with auth failed error raise cherrypy.HTTPError(400) dump = transdata['saml2_request'] @@ -83,17 +121,17 @@ class Continue(AuthenticateRequest): try: login = self.cfg.idp.get_login_handler(dump) except Exception, e: # pylint: disable=broad-except - self._debug('Failed to load status from dump: %r' % e) + self.debug('Failed to load status from dump: %r' % e) if not login: - self._debug("Empty Request dump?!") + self.debug("Empty Request dump?!") # TODO: Return to SP with auth failed error raise cherrypy.HTTPError(400) return self.auth(login) -class RedirectLogout(LogoutRequest): +class Logout(LogoutRequest): def GET(self, *args, **kwargs): query = cherrypy.request.query_string @@ -113,14 +151,15 @@ class SSO(ProviderPageBase): self.Redirect = Redirect(*args, **kwargs) self.POST = POSTAuth(*args, **kwargs) self.Continue = Continue(*args, **kwargs) + self.SOAP = SSO_SOAP(*args, **kwargs) class SLO(ProviderPageBase): def __init__(self, *args, **kwargs): super(SLO, self).__init__(*args, **kwargs) - self._debug('SLO init') - self.Redirect = RedirectLogout(*args, **kwargs) + self.debug('SLO init') + self.Redirect = Logout(*args, **kwargs) # one week @@ -133,7 +172,7 @@ class Metadata(ProviderPageBase): def GET(self, *args, **kwargs): body = self._get_metadata() - cherrypy.response.headers["Content-Type"] = "text/xml" + cherrypy.response.headers["Content-Type"] = XML_MEDIA_TYPE cherrypy.response.headers["Content-Disposition"] = \ 'attachment; filename="metadata.xml"' return body @@ -176,6 +215,7 @@ class IdpProvider(ProviderBase): self.rest = None self.page = None self.idp = None + self.sessionfactory = None self.description = """ Provides SAML 2.0 authentication infrastructure. """ @@ -233,6 +273,10 @@ Provides SAML 2.0 authentication infrastructure. """ 'default allowed attributes', 'Defines a list of allowed attributes, applied after mapping', ['*']), + pconfig.String( + 'session database url', + 'Database URL for SAML2 sessions', + 'saml2.sessions.db.sqlite'), ) if cherrypy.config.get('debug', False): import logging @@ -242,6 +286,14 @@ Provides SAML 2.0 authentication infrastructure. """ logger.addHandler(lh) logger.setLevel(logging.DEBUG) + store = SAML2SessionStore( + database_url=self.get_config_value('session database url') + ) + bt = cherrypy.process.plugins.BackgroundTask( + 60, store.remove_expired_sessions + ) + bt.start() + @property def allow_self_registration(self): return self.get_config_value('allow self registration') @@ -294,7 +346,6 @@ Provides SAML 2.0 authentication infrastructure. """ return self.get_config_value('default allowed attributes') def get_tree(self, site): - self.idp = self.init_idp() self.page = SAML2(site, self) self.admin = Saml2AdminPage(site, self) self.rest = Saml2RestBase(site, self) @@ -302,11 +353,15 @@ Provides SAML 2.0 authentication infrastructure. """ def init_idp(self): idp = None + self.sessionfactory = SAMLSessionFactory( + database_url=self.get_config_value('session database url') + ) # Init IDP data try: - idp = IdentityProvider(self) + idp = IdentityProvider(self, + sessionfactory=self.sessionfactory) except Exception, e: # pylint: disable=broad-except - self._debug('Failed to init SAML2 provider: %r' % e) + self.debug('Failed to init SAML2 provider: %r' % e) return None self._root.logout.add_handler(self.name, self.idp_initiated_logout) @@ -322,7 +377,7 @@ Provides SAML 2.0 authentication infrastructure. """ try: idp.add_provider(sp) except Exception, e: # pylint: disable=broad-except - self._debug('Failed to add SP %s: %r' % (sp['name'], e)) + self.debug('Failed to add SP %s: %r' % (sp['name'], e)) return idp @@ -338,30 +393,23 @@ 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") + self.debug("IdP-initiated SAML2 logout") us = UserSession() + user = us.get_user() - saml_sessions = us.get_provider_data('saml2') - if saml_sessions is None: - self._debug("No SAML2 sessions to logout") - return - session = saml_sessions.get_next_logout(remove=False) + saml_sessions = self.sessionfactory + # pylint: disable=unused-variable + (mech, session) = saml_sessions.get_next_logout( + logout_mechs=[lasso.SAML2_METADATA_BINDING_REDIRECT]) if session is None: return - # Add a fake session to indicate where the user should - # be redirected to when all SP's are logged out. - idpurl = self._root.instance_base_url() - saml_sessions.add_session("_idp_initiated_logout", - idpurl, - "") - init_session = saml_sessions.find_session_by_provider(idpurl) - init_session.set_logoutstate(idpurl, "idp_initiated_logout", None) - saml_sessions.start_logout(init_session) - logout = self.idp.get_logout_handler() - logout.setSessionFromDump(session.session.dump()) + logout.setSessionFromDump(session.login_session) logout.initRequest(session.provider_id) try: logout.buildRequestMsg() @@ -370,6 +418,22 @@ Provides SAML 2.0 authentication infrastructure. """ raise cherrypy.HTTPRedirect(400, 'Failed to log out user: %s ' % e) + # Add a fake session to indicate where the user should + # 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, "", "", + [lasso.SAML2_METADATA_BINDING_REDIRECT]) + init_session = saml_sessions.get_session_by_id(session_id) + saml_sessions.start_logout(init_session, relaystate=idpurl) + + # Add the logout request id we just created to the session to be + # logged out so that when it responds we can find the right + # session. + session.set_logoutstate(request_id=logout.request.id) + saml_sessions.start_logout(session, initial=False) + + self.debug('Sending initial logout request to %s' % logout.msgUrl) raise cherrypy.HTTPRedirect(logout.msgUrl) @@ -383,6 +447,9 @@ class IdpMetadataGenerator(object): '%s/saml2/SSO/POST' % url) self.meta.add_service(metadata.SAML2_SERVICE_MAP['sso-redirect'], '%s/saml2/SSO/Redirect' % url) + if is_lasso_ecp_enabled(): + self.meta.add_service(metadata.SAML2_SERVICE_MAP['sso-soap'], + '%s/saml2/SSO/SOAP' % url) self.meta.add_service(metadata.SAML2_SERVICE_MAP['logout-redirect'], '%s/saml2/SLO/Redirect' % url) self.meta.add_allowed_name_format( @@ -411,8 +478,10 @@ class Installer(ProviderInstaller): help=('Metadata validity period in days ' '(default - %d)' % METADATA_DEFAULT_VALIDITY_PERIOD)) + group.add_argument('--saml2-session-dburl', + help='session database URL') - def configure(self, opts): + def configure(self, opts, changes): if opts['saml2'] != 'yes': return @@ -433,7 +502,7 @@ class Installer(ProviderInstaller): validity = int(opts['saml2_metadata_validity']) meta = IdpMetadataGenerator(url, cert, timedelta(validity)) - if 'krb' in opts and opts['krb'] == 'yes': + if 'gssapi' in opts and opts['gssapi'] == 'yes': meta.meta.add_allowed_name_format( lasso.SAML2_NAME_IDENTIFIER_FORMAT_KERBEROS) @@ -449,7 +518,11 @@ class Installer(ProviderInstaller): 'idp certificate file': cert.cert, 'idp key file': cert.key, 'idp nameid salt': uuid.uuid4().hex, - 'idp metadata validity': opts['saml2_metadata_validity']} + 'idp metadata validity': opts['saml2_metadata_validity'], + 'session database url': opts['saml2_session_dburl'] or + opts['database_url'] % { + 'datadir': opts['data_dir'], + 'dbname': 'saml2.sessions.db'}} po.save_plugin_config(config) # Update global config to add login plugin