pam: use a pam object method instead of pam module function
[cascardo/ipsilon.git] / ipsilon / login / common.py
old mode 100755 (executable)
new mode 100644 (file)
index b394fa0..ef84e10
-#!/usr/bin/python
-#
-# 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.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.info.common import Info
 from ipsilon.util.cookies import SecureCookie
+from ipsilon.util.log import Log
 import cherrypy
 
 
 USERNAME_COOKIE = 'ipsilon_default_username'
 
 
 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):
-        base = cherrypy.config.get('base.mount', "")
-        raise cherrypy.HTTPRedirect('%s/login/%s' % (base, path))
+    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()
 
         session = UserSession()
 
-        if self.info:
-            userattrs = self.info.get_user_attrs(username)
-            if userdata:
-                userdata.update(userattrs.get('userdata', {}))
-            else:
-                userdata = userattrs.get('userdata', {})
+        # merge attributes from login plugin and info plugin
+        if info:
+            infoattrs = info.get_user_attrs(username)
+        else:
+            infoattrs = dict()
+
+        if userdata is None:
+            userdata = dict()
+
+        if '_groups' in infoattrs:
+            userdata['_groups'] = list(set(userdata.get('_groups', []) +
+                                           infoattrs['_groups']))
+            del infoattrs['_groups']
 
 
-            # merge groups and extras from login plugin and info plugin
-            userdata['groups'] = list(set(userdata.get('groups', []) +
-                                          userattrs.get('groups', [])))
+        if '_extras' in infoattrs:
+            userdata['_extras'] = userdata.get('_extras', {})
+            userdata['_extras'].update(infoattrs['_extras'])
+            del infoattrs['_extras']
 
 
-            userdata['extras'] = userdata.get('extras', {})
-            userdata['extras'].update(userattrs.get('extras', {}))
+        userdata.update(infoattrs)
 
 
-            self.debug("User %s attributes: %s" % (username, repr(userdata)))
+        self.debug("User %s attributes: %s" % (username, repr(userdata)))
 
         if auth_type:
             if userdata:
 
         if auth_type:
             if userdata:
-                userdata.update({'auth_type': auth_type})
+                userdata.update({'_auth_type': auth_type})
             else:
             else:
-                userdata = {'auth_type': auth_type}
+                userdata = {'_auth_type': auth_type}
 
         # create session login including all the userdata just gathered
         session.login(username, userdata)
 
 
         # 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)
         # save username into a cookie if parent was form base auth
         if auth_type == 'password':
             cookie = SecureCookie(USERNAME_COOKIE, username)
@@ -91,11 +166,11 @@ class LoginManagerBase(PluginConfig, PluginObject):
             trans.wipe()
         raise cherrypy.HTTPRedirect(redirect)
 
             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:
         # try with next module
         next_login = self.next_login()
         if next_login:
-            return self.redirect_to_path(next_login.path)
+            return self.redirect_to_path(next_login.path, trans)
 
         # return to the caller if any
         session = UserSession()
 
         # return to the caller if any
         session = UserSession()
@@ -110,10 +185,13 @@ class LoginManagerBase(PluginConfig, PluginObject):
         # destroy session and return error
         if 'login_return' not in transdata:
             session.logout(None)
         # 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'])
 
 
         raise cherrypy.HTTPRedirect(transdata['login_return'])
 
+    def set_auth_error(self):
+        cherrypy.response.status = 401
+
     def get_tree(self, site):
         raise NotImplementedError
 
     def get_tree(self, site):
         raise NotImplementedError
 
@@ -130,6 +208,19 @@ class LoginManagerBase(PluginConfig, PluginObject):
         except (ValueError, IndexError):
             return None
 
         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
     def on_enable(self):
 
         # and add self to the root
@@ -160,7 +251,6 @@ class LoginFormBase(LoginPageBase):
 
     def GET(self, *args, **kwargs):
         context = self.create_tmpl_context()
 
     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):
         return self._template(self.formtemplate, **context)
 
     def root(self, *args, **kwargs):
@@ -170,25 +260,32 @@ class LoginFormBase(LoginPageBase):
             return op(*args, **kwargs)
 
     def create_tmpl_context(self, **kwargs):
             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()
         username = cookie.value
 
         cookie = SecureCookie(USERNAME_COOKIE)
         cookie.receive()
         username = cookie.value
-        if username is None:
-            username = ''
 
         target = None
         if self.trans is not None:
             tid = self.trans.transaction_id
             target = self.trans.retrieve().get('login_target')
 
         target = None
         if self.trans is not None:
             tid = self.trans.transaction_id
             target = self.trans.retrieve().get('login_target')
+            username = self.trans.retrieve().get('login_username')
         if tid is None:
             tid = ''
 
         if tid is None:
             tid = ''
 
+        if username is None:
+            username = ''
+
         context = {
             "title": 'Login',
             "action": '%s/%s' % (self.basepath, self.formpage),
         context = {
             "title": 'Login',
             "action": '%s/%s' % (self.basepath, self.formpage),
@@ -196,7 +293,7 @@ class LoginFormBase(LoginPageBase):
             "username_text": self.lm.username_text,
             "password_text": self.lm.password_text,
             "description": self.lm.help_text,
             "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,
             "username": username,
             "login_target": target,
             "cancel_url": '%s/login/cancel?%s' % (self.basepath,
@@ -225,14 +322,14 @@ class Login(Page):
         self._site[FACILITY] = plugins
 
         available = plugins.available.keys()
         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:
 
         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()
             if item not in plugins.available:
                 continue
             plugins.available[item].enable()
@@ -260,11 +357,28 @@ class Login(Page):
 
 
 class Logout(Page):
 
 
 class Logout(Page):
+    def __init__(self, *args, **kwargs):
+        super(Logout, self).__init__(*args, **kwargs)
+        self.handlers = {}
 
     def root(self, *args, **kwargs):
 
     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')
 
         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):
 
 
 class Cancel(Page):
 
@@ -285,6 +399,25 @@ class Cancel(Page):
             return op(*args, **kwargs)
 
 
             return op(*args, **kwargs)
 
 
+class LoginManagerInstaller(object):
+    def __init__(self):
+        self.facility = FACILITY
+        self.ptype = 'login'
+        self.name = None
+
+    def unconfigure(self, opts, changes):
+        return
+
+    def install_args(self, group):
+        raise NotImplementedError
+
+    def validate_args(self, args):
+        return
+
+    def configure(self, opts, changes):
+        raise NotImplementedError
+
+
 class LoginMgrsInstall(object):
 
     def __init__(self):
 class LoginMgrsInstall(object):
 
     def __init__(self):