From: John Dennis Date: Sat, 10 Oct 2015 14:47:04 +0000 (-0400) Subject: Handle user session data for both internal and external authentication X-Git-Url: http://git.cascardo.info/?p=cascardo%2Fipsilon.git;a=commitdiff_plain;h=678c35ffc62b62645ee8d8a86b38f445d0bbfa2d Handle user session data for both internal and external authentication Ipsilon can authtenticate a user by itself via it's own login handlers (classes derived from `LoginManager`) or it can capitalize on the authentication provided by the container Ipsilon is running in (currently WSGI inside Apache). We refer to the later as "external authentication" because it occurs outside of Ipsilon. However in both cases there is a common need to execute the same code irregardless of where the authntication occurred. Establish a new mixin class LoginHelper and use it in both the LoginManagerBase class and the SAML2 SSO SOAP endpoint handler. The SOAP endpoint handler requires extenal authentication. LoginHelper.initialize_login_session() performs the common duty of establishing a login session and binding user attributes to that session. LoginHelper.get_external_auth_info() determines if external authentication has been performed and returns the name of the principal and the authentication method. Since SSO_SOAP utilizes external login it needs access to the Info providers in order to populate the user attributes in the returned SAML Assertion. The Info provider should be initialized only once and is done via the normal Ipsilon login provider initialization. SSO_SOAP obtains a reference to the Info provider bound to the login provider by accessing the provider._root.login.info member. In order to access the provider it was advantageous to explictily name the positional parameters passed to the __init__ calls instead of the previous practice of passing parameters anonymously in a *args tuple. In this manner the provider parameter is explicit instead having used a hardcoded index into the args tuple (e.g. provider = args[1]). The result is much cleaner, easier to read and more robust software. Thus the patch also modifies the __init__ argument list to explictly pass the site and provider parameters as the first and second positional parameters instead of having them be anonymously subsumed in the *args parameter. These parameters must always be passed because the ProviderPageBase __init__ requires them. Also modify the super calls used to initialize the parent class to pass the site and provider parameters. Calls to initialize ProviderPageBase only pass the site and provider parameters, they do not pass any additional anonymous parameters from the subclass. Ticket: 191 Signed-off-by: John Dennis Reviewed-by: Patrick Uiterwijk --- diff --git a/ipsilon/login/common.py b/ipsilon/login/common.py index cd4f166..ef84e10 100644 --- a/ipsilon/login/common.py +++ b/ipsilon/login/common.py @@ -7,35 +7,93 @@ from ipsilon.util.plugin import PluginObject from ipsilon.util.config import ConfigHelper from ipsilon.info.common import Info from ipsilon.util.cookies import SecureCookie +from ipsilon.util.log import Log import cherrypy USERNAME_COOKIE = 'ipsilon_default_username' -class LoginManagerBase(ConfigHelper, PluginObject): +class LoginHelper(Log): - def __init__(self, *args): - ConfigHelper.__init__(self) - PluginObject.__init__(self, *args) - self._root = None - self._site = None - self.path = '/' - self.info = None + """Common code supporing login operations. - def redirect_to_path(self, path, trans=None): - base = cherrypy.config.get('base.mount', "") - url = '%s/login/%s' % (base, path) - if trans: - url += '?%s' % trans.get_GET_arg() - raise cherrypy.HTTPRedirect(url) + Ipsilon can authtenticate a user by itself via it's own login + handlers (classes derived from `LoginManager`) or it can + capitalize on the authentication provided by the container Ipsilon + is running in (currently WSGI inside Apache). We refer to the + later as "external authentication" because it occurs outside of + Ipsilon. However in both cases there is a common need to execute + the same code irregardless of where the authntication + occurred. This class serves that purpose. + """ + + def get_external_auth_info(self): + """Return the username and auth type for external authentication. + + If the container Ipsilon is running inside of has already + authenticated the user prior to reaching one of our endpoints + return the username and the name of authenticaion method + used. In Apache this will be REMOTE_USER and AUTH_TYPE. + + The returned auth_type will be prefixed with the string + "external:" to clearly distinguish between the same method + being used internally by Ipsilon from the same method used by + the container hosting Ipsilon. The returned auth_type string + will be lower case. + + If there was no external authentication both username and + auth_type will be None. It is possible for a username to be + returned without knowing the auth_type. + + :return: tuple of (username, auth_type) + """ + + auth_type = None + username = cherrypy.request.login + if username: + auth_type = cherrypy.request.wsgi_environ.get('AUTH_TYPE') + if auth_type: + auth_type = 'external:%s' % (auth_type.lower()) + + self.debug("get_external_auth_info: username=%s auth_type=%s" % ( + username, auth_type)) + + return username, auth_type + + def initialize_login_session(self, username, info=None, + auth_type=None, userdata=None): + """Establish a login session for a user. + + Builds a `UserSession` object and bind attributes associated + with the user to the session. + + User attributes derive from two sources, the `Info` object + passed as the info parameter and the userdata dict. The `Info` + object encapsulates the info plugins run by Ipsilon. The + userdata dict is additional information typically derived + during authentication. + + The `Info` derived attributes are merged with the userdata + attributes to form one set of user attributes. The user + attributes are checked for consistenccy. Additional attrbutes + may be synthesized and added to the user attributes. The final + set of user attributes is then bound to the returned + `UserSession` object. + + :param username: The username bound to the identity principal + :param info: A `Info` object providing user attributes + :param auth_type: Authenication method name + :param userdata: Dict of additional user attributes + + :return: `UserSession` object + """ - def auth_successful(self, trans, username, auth_type=None, userdata=None): session = UserSession() # merge attributes from login plugin and info plugin - if self.info: - infoattrs = self.info.get_user_attrs(username) + if info: + infoattrs = info.get_user_attrs(username) else: infoattrs = dict() @@ -65,6 +123,29 @@ class LoginManagerBase(ConfigHelper, PluginObject): # create session login including all the userdata just gathered session.login(username, userdata) + return session + + +class LoginManagerBase(ConfigHelper, PluginObject, LoginHelper): + + def __init__(self, *args): + ConfigHelper.__init__(self) + PluginObject.__init__(self, *args) + self._root = None + self._site = None + self.path = '/' + self.info = None + + def redirect_to_path(self, path, trans=None): + base = cherrypy.config.get('base.mount', "") + url = '%s/login/%s' % (base, path) + if trans: + url += '?%s' % trans.get_GET_arg() + raise cherrypy.HTTPRedirect(url) + + def auth_successful(self, trans, username, auth_type=None, userdata=None): + self.initialize_login_session(username, self.info, auth_type, userdata) + # save username into a cookie if parent was form base auth if auth_type == 'password': cookie = SecureCookie(USERNAME_COOKIE, username) diff --git a/ipsilon/providers/openid/auth.py b/ipsilon/providers/openid/auth.py index 1ecbe43..561cd5a 100644 --- a/ipsilon/providers/openid/auth.py +++ b/ipsilon/providers/openid/auth.py @@ -17,8 +17,8 @@ import json class AuthenticateRequest(ProviderPageBase): - def __init__(self, *args, **kwargs): - super(AuthenticateRequest, self).__init__(*args, **kwargs) + def __init__(self, site, provider, *args, **kwargs): + super(AuthenticateRequest, self).__init__(site, provider) self.stage = 'init' self.trans = None diff --git a/ipsilon/providers/openid/meta.py b/ipsilon/providers/openid/meta.py index 27017c5..604e42d 100644 --- a/ipsilon/providers/openid/meta.py +++ b/ipsilon/providers/openid/meta.py @@ -7,8 +7,8 @@ import cherrypy class MetaHandler(ProviderPageBase): - def __init__(self, *args, **kwargs): - super(MetaHandler, self).__init__(*args, **kwargs) + def __init__(self, site, provider, *args, **kwargs): + super(MetaHandler, self).__init__(site, provider) self._template_name = None self._take_args = False diff --git a/ipsilon/providers/persona/auth.py b/ipsilon/providers/persona/auth.py index 09c73a1..daa64f6 100644 --- a/ipsilon/providers/persona/auth.py +++ b/ipsilon/providers/persona/auth.py @@ -13,8 +13,8 @@ import M2Crypto class AuthenticateRequest(ProviderPageBase): - def __init__(self, *args, **kwargs): - super(AuthenticateRequest, self).__init__(*args, **kwargs) + def __init__(self, site, provider, *args, **kwargs): + super(AuthenticateRequest, self).__init__(site, provider) self.trans = None def _preop(self, *args, **kwargs): diff --git a/ipsilon/providers/saml2/auth.py b/ipsilon/providers/saml2/auth.py index 5412240..940746c 100644 --- a/ipsilon/providers/saml2/auth.py +++ b/ipsilon/providers/saml2/auth.py @@ -25,8 +25,8 @@ class UnknownProvider(ProviderException): class AuthenticateRequest(ProviderPageBase): - def __init__(self, *args, **kwargs): - super(AuthenticateRequest, self).__init__(*args, **kwargs) + def __init__(self, site, provider, *args, **kwargs): + super(AuthenticateRequest, self).__init__(site, provider) self.stage = 'init' self.trans = None self.binding = None diff --git a/ipsilon/providers/saml2/logout.py b/ipsilon/providers/saml2/logout.py index cec1d03..b9b205c 100644 --- a/ipsilon/providers/saml2/logout.py +++ b/ipsilon/providers/saml2/logout.py @@ -28,8 +28,8 @@ class LogoutRequest(ProviderPageBase): deleted. """ - def __init__(self, *args, **kwargs): - super(LogoutRequest, self).__init__(*args, **kwargs) + def __init__(self, site, provider, *args, **kwargs): + super(LogoutRequest, self).__init__(site, provider) def _handle_logout_request(self, us, logout, saml_sessions, message): self.debug('Logout request') diff --git a/ipsilon/providers/saml2idp.py b/ipsilon/providers/saml2idp.py index 0404fe8..3ed95d8 100644 --- a/ipsilon/providers/saml2idp.py +++ b/ipsilon/providers/saml2idp.py @@ -1,5 +1,6 @@ # Copyright (C) 2014 Ipsilon project Contributors, for license see COPYING +from ipsilon.login.common import LoginHelper from ipsilon.providers.common import ProviderBase, ProviderPageBase, \ ProviderInstaller from ipsilon.providers.saml2.auth import AuthenticateRequest @@ -32,10 +33,12 @@ def is_lasso_ecp_enabled(): return 'ECP_ERROR_MISSING_AUTHN_REQUEST' in dir(lasso) -class SSO_SOAP(AuthenticateRequest): +class SSO_SOAP(AuthenticateRequest, LoginHelper): - def __init__(self, *args, **kwargs): - super(SSO_SOAP, self).__init__(*args, **kwargs) + def __init__(self, site, provider, *args, **kwargs): + super(SSO_SOAP, self).__init__(site, provider, *args, **kwargs) + # pylint: disable=protected-access + self.info = provider._root.login.info self.binding = metadata.SAML2_SERVICE_MAP['sso-soap'][1] @cherrypy.tools.require_content_type( @@ -49,13 +52,11 @@ class SSO_SOAP(AuthenticateRequest): 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: + username, auth_type = self.get_external_auth_info() + if not username: raise cherrypy.HTTPError(403, 'No user specified for SSO_SOAP') + self.debug("SSO_SOAP user=%s auth_type=%s" % (username, auth_type)) + self.initialize_login_session(username, self.info, auth_type) soap_xml_doc = cherrypy.request.rfile.read() soap_xml_doc = soap_xml_doc.strip() @@ -67,8 +68,8 @@ class SSO_SOAP(AuthenticateRequest): class Redirect(AuthenticateRequest): - def __init__(self, *args, **kwargs): - super(Redirect, self).__init__(*args, **kwargs) + def __init__(self, site, provider, *args, **kwargs): + super(Redirect, self).__init__(site, provider, *args, **kwargs) self.binding = metadata.SAML2_SERVICE_MAP['sso-redirect'][1] def GET(self, *args, **kwargs): @@ -81,8 +82,8 @@ class Redirect(AuthenticateRequest): class POSTAuth(AuthenticateRequest): - def __init__(self, *args, **kwargs): - super(POSTAuth, self).__init__(*args, **kwargs) + def __init__(self, site, provider, *args, **kwargs): + super(POSTAuth, self).__init__(site, provider, *args, **kwargs) self.binding = metadata.SAML2_SERVICE_MAP['sso-post'][1] def POST(self, *args, **kwargs): @@ -145,20 +146,20 @@ class Logout(LogoutRequest): class SSO(ProviderPageBase): - def __init__(self, *args, **kwargs): - super(SSO, self).__init__(*args, **kwargs) - self.Redirect = Redirect(*args, **kwargs) - self.POST = POSTAuth(*args, **kwargs) - self.Continue = Continue(*args, **kwargs) - self.SOAP = SSO_SOAP(*args, **kwargs) + def __init__(self, site, provider, *args, **kwargs): + super(SSO, self).__init__(site, provider) + self.Redirect = Redirect(site, provider, *args, **kwargs) + self.POST = POSTAuth(site, provider, *args, **kwargs) + self.Continue = Continue(site, provider, *args, **kwargs) + self.SOAP = SSO_SOAP(site, provider, *args, **kwargs) class SLO(ProviderPageBase): - def __init__(self, *args, **kwargs): - super(SLO, self).__init__(*args, **kwargs) + def __init__(self, site, provider, *args, **kwargs): + super(SLO, self).__init__(site, provider) self.debug('SLO init') - self.Redirect = Logout(*args, **kwargs) + self.Redirect = Logout(site, provider, *args, **kwargs) # one week @@ -199,11 +200,11 @@ class Metadata(ProviderPageBase): class SAML2(ProviderPageBase): - def __init__(self, *args, **kwargs): - super(SAML2, self).__init__(*args, **kwargs) - self.metadata = Metadata(*args, **kwargs) - self.SSO = SSO(*args, **kwargs) - self.SLO = SLO(*args, **kwargs) + def __init__(self, site, provider, *args, **kwargs): + super(SAML2, self).__init__(site, provider) + self.metadata = Metadata(site, provider, *args, **kwargs) + self.SSO = SSO(site, provider, *args, **kwargs) + self.SLO = SLO(site, provider, *args, **kwargs) class IdpProvider(ProviderBase):