X-Git-Url: http://git.cascardo.info/?p=cascardo%2Fipsilon.git;a=blobdiff_plain;f=ipsilon%2Flogin%2Fcommon.py;h=ef84e1093f65ba8fa7910cf0c252f086a69ec501;hp=2dcdb672da1e9befe65b1e3b0ea1f6bb281174a2;hb=HEAD;hpb=45cb73a21a90084818c3057e362ef9459f1600f3 diff --git a/ipsilon/login/common.py b/ipsilon/login/common.py index 2dcdb67..ef84e10 100644 --- a/ipsilon/login/common.py +++ b/ipsilon/login/common.py @@ -1,74 +1,151 @@ -# Copyright (C) 2013 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) 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): - 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() - 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() - # merge groups and extras from login plugin and info plugin - userdata['groups'] = list(set(userdata.get('groups', []) + - userattrs.get('groups', []))) + if userdata is None: + userdata = dict() - userdata['extras'] = userdata.get('extras', {}) - userdata['extras'].update(userattrs.get('extras', {})) + if '_groups' in infoattrs: + userdata['_groups'] = list(set(userdata.get('_groups', []) + + infoattrs['_groups'])) + del infoattrs['_groups'] - self.debug("User %s attributes: %s" % (username, repr(userdata))) + if '_extras' in infoattrs: + userdata['_extras'] = userdata.get('_extras', {}) + userdata['_extras'].update(infoattrs['_extras']) + del infoattrs['_extras'] + + userdata.update(infoattrs) + + self.debug("User %s attributes: %s" % (username, repr(userdata))) if auth_type: if userdata: - userdata.update({'auth_type': auth_type}) + userdata.update({'_auth_type': auth_type}) else: - userdata = {'auth_type': auth_type} + userdata = {'_auth_type': auth_type} # 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) @@ -89,11 +166,11 @@ 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: - 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() @@ -108,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 @@ -128,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 @@ -158,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): @@ -168,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() @@ -196,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, @@ -225,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() @@ -260,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): @@ -285,6 +399,25 @@ class Cancel(Page): 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):