X-Git-Url: http://git.cascardo.info/?p=cascardo%2Fipsilon.git;a=blobdiff_plain;f=ipsilon%2Flogin%2Fcommon.py;h=31053a0bcb82759d202cdb176bc9611b8f1bc119;hp=e303866ca7669116f4bdc8282f6f5c7d280337c8;hb=426f03dffc8f648a12b5f8a4b2ab30c8b4498be3;hpb=6e7edfd368105341aa852620c2d2b1c7e842ad25 diff --git a/ipsilon/login/common.py b/ipsilon/login/common.py old mode 100755 new mode 100644 index e303866..31053a0 --- a/ipsilon/login/common.py +++ b/ipsilon/login/common.py @@ -1,58 +1,152 @@ -#!/usr/bin/python -# -# 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 PluginLoader, PluginObject +from ipsilon.util.plugin import PluginInstaller, PluginLoader +from ipsilon.util.plugin import PluginObject +from ipsilon.util.config import ConfigHelper +from ipsilon.info.common import Info +from ipsilon.util.cookies import SecureCookie import cherrypy -class LoginManagerBase(PluginObject): +USERNAME_COOKIE = 'ipsilon_default_username' - def __init__(self): - super(LoginManagerBase, self).__init__() + +class LoginManagerBase(ConfigHelper, PluginObject): + + def __init__(self, *args): + ConfigHelper.__init__(self) + PluginObject.__init__(self, *args) + self._root = None + self._site = None self.path = '/' - self.next_login = None + self.info = None - def redirect_to_path(self, path): + def redirect_to_path(self, path, trans=None): base = cherrypy.config.get('base.mount', "") - raise cherrypy.HTTPRedirect('%s/login/%s' % (base, path)) + 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): + session = UserSession() + + # merge attributes from login plugin and info plugin + if self.info: + infoattrs = self.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'] + + 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}) + else: + userdata = {'_auth_type': auth_type} + + # create session login including all the userdata just gathered + session.login(username, userdata) + + # save username into a cookie if parent was form base auth + if auth_type == 'password': + cookie = SecureCookie(USERNAME_COOKIE, username) + # 15 days + cookie.maxage = 1296000 + cookie.send() + + transdata = trans.retrieve() + self.debug(transdata) + redirect = transdata.get('login_return', + cherrypy.config.get('base.mount', "") + '/') + self.debug('Redirecting back to: %s' % redirect) + + # on direct login the UI (ie not redirected by a provider) we ned to + # remove the transaction cookie as it won't be needed anymore + if trans.provider == 'login': + self.debug('Wiping transaction data') + trans.wipe() + raise cherrypy.HTTPRedirect(redirect) + + 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, trans) - def auth_successful(self, username): - # save ref before calling UserSession login() as it - # may regenerate the session - ref = cherrypy.config.get('base.mount', "") + '/' - if 'referral' in cherrypy.session: - ref = cherrypy.session['referral'] + # return to the caller if any + session = UserSession() - UserSession().login(username) - raise cherrypy.HTTPRedirect(ref) + transdata = trans.retrieve() - def auth_failed(self): - # Just make sure we destroy the session - UserSession().logout(None) + # on direct login the UI (ie not redirected by a provider) we ned to + # remove the transaction cookie as it won't be needed anymore + if trans.provider == 'login': + trans.wipe() - if self.next_login: - return self.redirect_to_path(self.next_login.path) + # destroy session and return error + if 'login_return' not in transdata: + session.logout(None) + raise cherrypy.HTTPError(401, message) - ref = cherrypy.config.get('base.mount', "") + '/unauthorized' - raise cherrypy.HTTPRedirect(ref) + raise cherrypy.HTTPRedirect(transdata['login_return']) + + def set_auth_error(self): + cherrypy.response.status = 401 + + def get_tree(self, site): + raise NotImplementedError + + def register(self, root, site): + self._root = root + self._site = site + + def next_login(self): + plugins = self._site[FACILITY] + try: + idx = plugins.enabled.index(self.name) + item = plugins.enabled[idx + 1] + return plugins.available[item] + 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 + self._root.add_subtree(self.name, self.get_tree(self._site)) + + # Get handle of the info plugin + self.info = self._root.info class LoginPageBase(Page): @@ -60,11 +154,76 @@ class LoginPageBase(Page): def __init__(self, site, mgr): super(LoginPageBase, self).__init__(site) self.lm = mgr + self._Transaction = None def root(self, *args, **kwargs): raise cherrypy.HTTPError(500) +class LoginFormBase(LoginPageBase): + + def __init__(self, site, mgr, page, template=None): + super(LoginFormBase, self).__init__(site, mgr) + self.formpage = page + self.formtemplate = template or 'login/form.html' + self.trans = None + + def GET(self, *args, **kwargs): + context = self.create_tmpl_context() + return self._template(self.formtemplate, **context) + + def root(self, *args, **kwargs): + self.trans = self.get_valid_transaction('login', **kwargs) + op = getattr(self, cherrypy.request.method, self.GET) + if callable(op): + return op(*args, **kwargs) + + def create_tmpl_context(self, **kwargs): + 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?%s' % (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 + + 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 username is None: + username = '' + + context = { + "title": 'Login', + "action": '%s/%s' % (self.basepath, self.formpage), + "service_name": self.lm.service_name, + "username_text": self.lm.username_text, + "password_text": self.lm.password_text, + "description": self.lm.help_text, + "other_stacks": other_stacks, + "username": username, + "login_target": target, + "cancel_url": '%s/login/cancel?%s' % (self.basepath, + self.trans.get_GET_arg()), + } + context.update(kwargs) + if self.trans is not None: + t = self.trans.get_POST_tuple() + context.update({t[0]: t[1]}) + + return context + + FACILITY = 'login_config' @@ -72,43 +231,112 @@ class Login(Page): def __init__(self, *args, **kwargs): super(Login, self).__init__(*args, **kwargs) - self.first_login = None + self.cancel = Cancel(*args, **kwargs) + self.info = Info(self._site) - loader = PluginLoader(Login, FACILITY, 'LoginManager') - self._site[FACILITY] = loader.get_plugin_data() - plugins = self._site[FACILITY] + plugins = PluginLoader(Login, FACILITY, 'LoginManager') + plugins.get_plugin_data() + self._site[FACILITY] = plugins + + available = plugins.available.keys() + self.debug('Available login managers: %s' % str(available)) + + for item in plugins.available: + plugin = plugins.available[item] + plugin.register(self, self._site) - prev_obj = None - for item in plugins['whitelist']: - self._log('Login plugin in whitelist: %s' % item) - if item not in plugins['available']: + for item in plugins.enabled: + self.debug('Login plugin in enabled list: %s' % item) + if item not in plugins.available: continue - self._log('Login plugin enabled: %s' % item) - plugins['enabled'].append(item) - obj = plugins['available'][item] - if prev_obj: - prev_obj.next_login = obj - else: - self.first_login = obj - prev_obj = obj - if item in plugins['config']: - obj.set_config(plugins['config'][item]) - self.__dict__[item] = obj.get_tree(self._site) + plugins.available[item].enable() + + def add_subtree(self, name, page): + self.__dict__[name] = page - def _log(self, fact): - if cherrypy.config.get('debug', False): - cherrypy.log(fact) + def get_first_login(self): + plugin = None + plugins = self._site[FACILITY] + if plugins.enabled: + first = plugins.enabled[0] + plugin = plugins.available[first] + return plugin def root(self, *args, **kwargs): - if self.first_login: - raise cherrypy.HTTPRedirect('%s/login/%s' % - (self.basepath, - self.first_login.path)) + plugin = self.get_first_login() + if plugin: + trans = self.get_valid_transaction('login', **kwargs) + redirect = '%s/login/%s?%s' % (self.basepath, + plugin.path, + trans.get_GET_arg()) + raise cherrypy.HTTPRedirect(redirect) return self._template('login/index.html', title='Login') 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): + + def GET(self, *args, **kwargs): + + session = UserSession() + session.logout(None) + + # return to the caller if any + transdata = self.get_valid_transaction('login', **kwargs).retrieve() + if 'login_return' not in transdata: + raise cherrypy.HTTPError(401) + raise cherrypy.HTTPRedirect(transdata['login_return']) + + def root(self, *args, **kwargs): + op = getattr(self, cherrypy.request.method, self.GET) + if callable(op): + 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): + pi = PluginInstaller(LoginMgrsInstall, FACILITY) + self.plugins = pi.get_plugins()