pam: use a pam object method instead of pam module function
[cascardo/ipsilon.git] / ipsilon / login / common.py
index 60f6df1..ef84e10 100644 (file)
@@ -1,55 +1,99 @@
-# Copyright (C) 2013  Simo Sorce <simo@redhat.com>
-#
-# 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 <http://www.gnu.org/licenses/>.
+# Copyright (C) 2013 Ipsilon project Contributors, for license see COPYING
 
 from ipsilon.util.page import Page
 from ipsilon.util.user import UserSession
 from ipsilon.util.plugin import PluginInstaller, PluginLoader
-from ipsilon.util.plugin import PluginObject, PluginConfig
+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(PluginConfig, PluginObject):
+class LoginHelper(Log):
 
-    def __init__(self, *args):
-        PluginConfig.__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()
 
@@ -79,6 +123,29 @@ class LoginManagerBase(PluginConfig, 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)
@@ -99,7 +166,7 @@ class LoginManagerBase(PluginConfig, PluginObject):
             trans.wipe()
         raise cherrypy.HTTPRedirect(redirect)
 
-    def auth_failed(self, trans):
+    def auth_failed(self, trans, message=None):
         # try with next module
         next_login = self.next_login()
         if next_login:
@@ -118,10 +185,13 @@ class LoginManagerBase(PluginConfig, PluginObject):
         # destroy session and return error
         if 'login_return' not in transdata:
             session.logout(None)
-            raise cherrypy.HTTPError(401)
+            raise cherrypy.HTTPError(401, message)
 
         raise cherrypy.HTTPRedirect(transdata['login_return'])
 
+    def set_auth_error(self):
+        cherrypy.response.status = 401
+
     def get_tree(self, site):
         raise NotImplementedError
 
@@ -138,6 +208,19 @@ class LoginManagerBase(PluginConfig, PluginObject):
         except (ValueError, IndexError):
             return None
 
+    def other_login_stacks(self):
+        plugins = self._site[FACILITY]
+        stack = list()
+        try:
+            idx = plugins.enabled.index(self.name)
+        except (ValueError, IndexError):
+            idx = None
+        for i in range(0, len(plugins.enabled)):
+            if i == idx:
+                continue
+            stack.append(plugins.available[plugins.enabled[i]])
+        return stack
+
     def on_enable(self):
 
         # and add self to the root
@@ -168,7 +251,6 @@ class LoginFormBase(LoginPageBase):
 
     def GET(self, *args, **kwargs):
         context = self.create_tmpl_context()
-        # pylint: disable=star-args
         return self._template(self.formtemplate, **context)
 
     def root(self, *args, **kwargs):
@@ -178,11 +260,16 @@ class LoginFormBase(LoginPageBase):
             return op(*args, **kwargs)
 
     def create_tmpl_context(self, **kwargs):
-        next_url = None
-        next_login = self.lm.next_login()
-        if next_login:
-            next_url = '%s?%s' % (next_login.path,
-                                  self.trans.get_GET_arg())
+        other_stacks = None
+        other_login_stacks = self.lm.other_login_stacks()
+        if other_login_stacks:
+            other_stacks = list()
+            for ls in other_login_stacks:
+                url = '%s/login/%s?%s' % (
+                    self.basepath, ls.path, self.trans.get_GET_arg()
+                )
+                name = ls.name
+                other_stacks.append({'url': url, 'name': name})
 
         cookie = SecureCookie(USERNAME_COOKIE)
         cookie.receive()
@@ -206,7 +293,7 @@ class LoginFormBase(LoginPageBase):
             "username_text": self.lm.username_text,
             "password_text": self.lm.password_text,
             "description": self.lm.help_text,
-            "next_url": next_url,
+            "other_stacks": other_stacks,
             "username": username,
             "login_target": target,
             "cancel_url": '%s/login/cancel?%s' % (self.basepath,
@@ -235,14 +322,14 @@ class Login(Page):
         self._site[FACILITY] = plugins
 
         available = plugins.available.keys()
-        self._debug('Available login managers: %s' % str(available))
+        self.debug('Available login managers: %s' % str(available))
 
         for item in plugins.available:
             plugin = plugins.available[item]
             plugin.register(self, self._site)
 
         for item in plugins.enabled:
-            self._debug('Login plugin in enabled list: %s' % item)
+            self.debug('Login plugin in enabled list: %s' % item)
             if item not in plugins.available:
                 continue
             plugins.available[item].enable()
@@ -270,11 +357,28 @@ class Login(Page):
 
 
 class Logout(Page):
+    def __init__(self, *args, **kwargs):
+        super(Logout, self).__init__(*args, **kwargs)
+        self.handlers = {}
 
     def root(self, *args, **kwargs):
-        UserSession().logout(self.user)
+        us = UserSession()
+
+        for provider in self.handlers:
+            self.debug("Calling logout for provider %s" % provider)
+            obj = self.handlers[provider]
+            obj()
+
+        us.logout(self.user)
         return self._template('logout.html', title='Logout')
 
+    def add_handler(self, provider, handler):
+        """
+        Providers can register a logout handler here that is called
+        when the IdP logout link is accessed.
+        """
+        self.handlers[provider] = handler
+
 
 class Cancel(Page):
 
@@ -301,13 +405,16 @@ class LoginManagerInstaller(object):
         self.ptype = 'login'
         self.name = None
 
-    def unconfigure(self, opts):
+    def unconfigure(self, opts, changes):
         return
 
     def install_args(self, group):
         raise NotImplementedError
 
-    def configure(self, opts):
+    def validate_args(self, args):
+        return
+
+    def configure(self, opts, changes):
         raise NotImplementedError