Handle user session data for both internal and external authentication
authorJohn Dennis <jdennis@redhat.com>
Sat, 10 Oct 2015 14:47:04 +0000 (10:47 -0400)
committerPatrick Uiterwijk <puiterwijk@redhat.com>
Tue, 27 Oct 2015 15:59:54 +0000 (16:59 +0100)
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 <jdennis@redhat.com>
Reviewed-by: Patrick Uiterwijk <puiterwijk@redhat.com>
ipsilon/login/common.py
ipsilon/providers/openid/auth.py
ipsilon/providers/openid/meta.py
ipsilon/providers/persona/auth.py
ipsilon/providers/saml2/auth.py
ipsilon/providers/saml2/logout.py
ipsilon/providers/saml2idp.py

index cd4f166..ef84e10 100644 (file)
@@ -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)
index 1ecbe43..561cd5a 100644 (file)
@@ -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
 
index 27017c5..604e42d 100644 (file)
@@ -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
 
index 09c73a1..daa64f6 100644 (file)
@@ -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):
index 5412240..940746c 100644 (file)
@@ -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
index cec1d03..b9b205c 100644 (file)
@@ -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')
index 0404fe8..3ed95d8 100644 (file)
@@ -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):