1 # Copyright (C) 2013 Ipsilon project Contributors, for license see COPYING
3 from ipsilon.util.page import Page
4 from ipsilon.util.user import UserSession
5 from ipsilon.util.plugin import PluginInstaller, PluginLoader
6 from ipsilon.util.plugin import PluginObject
7 from ipsilon.util.config import ConfigHelper
8 from ipsilon.info.common import Info
9 from ipsilon.util.cookies import SecureCookie
10 from ipsilon.util.log import Log
14 USERNAME_COOKIE = 'ipsilon_default_username'
17 class LoginHelper(Log):
19 """Common code supporing login operations.
21 Ipsilon can authtenticate a user by itself via it's own login
22 handlers (classes derived from `LoginManager`) or it can
23 capitalize on the authentication provided by the container Ipsilon
24 is running in (currently WSGI inside Apache). We refer to the
25 later as "external authentication" because it occurs outside of
26 Ipsilon. However in both cases there is a common need to execute
27 the same code irregardless of where the authntication
28 occurred. This class serves that purpose.
31 def get_external_auth_info(self):
32 """Return the username and auth type for external authentication.
34 If the container Ipsilon is running inside of has already
35 authenticated the user prior to reaching one of our endpoints
36 return the username and the name of authenticaion method
37 used. In Apache this will be REMOTE_USER and AUTH_TYPE.
39 The returned auth_type will be prefixed with the string
40 "external:" to clearly distinguish between the same method
41 being used internally by Ipsilon from the same method used by
42 the container hosting Ipsilon. The returned auth_type string
45 If there was no external authentication both username and
46 auth_type will be None. It is possible for a username to be
47 returned without knowing the auth_type.
49 :return: tuple of (username, auth_type)
53 username = cherrypy.request.login
55 auth_type = cherrypy.request.wsgi_environ.get('AUTH_TYPE')
57 auth_type = 'external:%s' % (auth_type.lower())
59 self.debug("get_external_auth_info: username=%s auth_type=%s" % (
62 return username, auth_type
64 def initialize_login_session(self, username, info=None,
65 auth_type=None, userdata=None):
66 """Establish a login session for a user.
68 Builds a `UserSession` object and bind attributes associated
69 with the user to the session.
71 User attributes derive from two sources, the `Info` object
72 passed as the info parameter and the userdata dict. The `Info`
73 object encapsulates the info plugins run by Ipsilon. The
74 userdata dict is additional information typically derived
75 during authentication.
77 The `Info` derived attributes are merged with the userdata
78 attributes to form one set of user attributes. The user
79 attributes are checked for consistenccy. Additional attrbutes
80 may be synthesized and added to the user attributes. The final
81 set of user attributes is then bound to the returned
84 :param username: The username bound to the identity principal
85 :param info: A `Info` object providing user attributes
86 :param auth_type: Authenication method name
87 :param userdata: Dict of additional user attributes
89 :return: `UserSession` object
92 session = UserSession()
94 # merge attributes from login plugin and info plugin
96 infoattrs = info.get_user_attrs(username)
103 if '_groups' in infoattrs:
104 userdata['_groups'] = list(set(userdata.get('_groups', []) +
105 infoattrs['_groups']))
106 del infoattrs['_groups']
108 if '_extras' in infoattrs:
109 userdata['_extras'] = userdata.get('_extras', {})
110 userdata['_extras'].update(infoattrs['_extras'])
111 del infoattrs['_extras']
113 userdata.update(infoattrs)
115 self.debug("User %s attributes: %s" % (username, repr(userdata)))
119 userdata.update({'_auth_type': auth_type})
121 userdata = {'_auth_type': auth_type}
123 # create session login including all the userdata just gathered
124 session.login(username, userdata)
129 class LoginManagerBase(ConfigHelper, PluginObject, LoginHelper):
131 def __init__(self, *args):
132 ConfigHelper.__init__(self)
133 PluginObject.__init__(self, *args)
139 def redirect_to_path(self, path, trans=None):
140 base = cherrypy.config.get('base.mount', "")
141 url = '%s/login/%s' % (base, path)
143 url += '?%s' % trans.get_GET_arg()
144 raise cherrypy.HTTPRedirect(url)
146 def auth_successful(self, trans, username, auth_type=None, userdata=None):
147 self.initialize_login_session(username, self.info, auth_type, userdata)
149 # save username into a cookie if parent was form base auth
150 if auth_type == 'password':
151 cookie = SecureCookie(USERNAME_COOKIE, username)
153 cookie.maxage = 1296000
156 transdata = trans.retrieve()
157 self.debug(transdata)
158 redirect = transdata.get('login_return',
159 cherrypy.config.get('base.mount', "") + '/')
160 self.debug('Redirecting back to: %s' % redirect)
162 # on direct login the UI (ie not redirected by a provider) we ned to
163 # remove the transaction cookie as it won't be needed anymore
164 if trans.provider == 'login':
165 self.debug('Wiping transaction data')
167 raise cherrypy.HTTPRedirect(redirect)
169 def auth_failed(self, trans, message=None):
170 # try with next module
171 next_login = self.next_login()
173 return self.redirect_to_path(next_login.path, trans)
175 # return to the caller if any
176 session = UserSession()
178 transdata = trans.retrieve()
180 # on direct login the UI (ie not redirected by a provider) we ned to
181 # remove the transaction cookie as it won't be needed anymore
182 if trans.provider == 'login':
185 # destroy session and return error
186 if 'login_return' not in transdata:
188 raise cherrypy.HTTPError(401, message)
190 raise cherrypy.HTTPRedirect(transdata['login_return'])
192 def set_auth_error(self):
193 cherrypy.response.status = 401
195 def get_tree(self, site):
196 raise NotImplementedError
198 def register(self, root, site):
202 def next_login(self):
203 plugins = self._site[FACILITY]
205 idx = plugins.enabled.index(self.name)
206 item = plugins.enabled[idx + 1]
207 return plugins.available[item]
208 except (ValueError, IndexError):
211 def other_login_stacks(self):
212 plugins = self._site[FACILITY]
215 idx = plugins.enabled.index(self.name)
216 except (ValueError, IndexError):
218 for i in range(0, len(plugins.enabled)):
221 stack.append(plugins.available[plugins.enabled[i]])
226 # and add self to the root
227 self._root.add_subtree(self.name, self.get_tree(self._site))
229 # Get handle of the info plugin
230 self.info = self._root.info
233 class LoginPageBase(Page):
235 def __init__(self, site, mgr):
236 super(LoginPageBase, self).__init__(site)
238 self._Transaction = None
240 def root(self, *args, **kwargs):
241 raise cherrypy.HTTPError(500)
244 class LoginFormBase(LoginPageBase):
246 def __init__(self, site, mgr, page, template=None):
247 super(LoginFormBase, self).__init__(site, mgr)
249 self.formtemplate = template or 'login/form.html'
252 def GET(self, *args, **kwargs):
253 context = self.create_tmpl_context()
254 return self._template(self.formtemplate, **context)
256 def root(self, *args, **kwargs):
257 self.trans = self.get_valid_transaction('login', **kwargs)
258 op = getattr(self, cherrypy.request.method, self.GET)
260 return op(*args, **kwargs)
262 def create_tmpl_context(self, **kwargs):
264 other_login_stacks = self.lm.other_login_stacks()
265 if other_login_stacks:
266 other_stacks = list()
267 for ls in other_login_stacks:
268 url = '%s/login/%s?%s' % (
269 self.basepath, ls.path, self.trans.get_GET_arg()
272 other_stacks.append({'url': url, 'name': name})
274 cookie = SecureCookie(USERNAME_COOKIE)
276 username = cookie.value
279 if self.trans is not None:
280 tid = self.trans.transaction_id
281 target = self.trans.retrieve().get('login_target')
282 username = self.trans.retrieve().get('login_username')
291 "action": '%s/%s' % (self.basepath, self.formpage),
292 "service_name": self.lm.service_name,
293 "username_text": self.lm.username_text,
294 "password_text": self.lm.password_text,
295 "description": self.lm.help_text,
296 "other_stacks": other_stacks,
297 "username": username,
298 "login_target": target,
299 "cancel_url": '%s/login/cancel?%s' % (self.basepath,
300 self.trans.get_GET_arg()),
302 context.update(kwargs)
303 if self.trans is not None:
304 t = self.trans.get_POST_tuple()
305 context.update({t[0]: t[1]})
310 FACILITY = 'login_config'
315 def __init__(self, *args, **kwargs):
316 super(Login, self).__init__(*args, **kwargs)
317 self.cancel = Cancel(*args, **kwargs)
318 self.info = Info(self._site)
320 plugins = PluginLoader(Login, FACILITY, 'LoginManager')
321 plugins.get_plugin_data()
322 self._site[FACILITY] = plugins
324 available = plugins.available.keys()
325 self.debug('Available login managers: %s' % str(available))
327 for item in plugins.available:
328 plugin = plugins.available[item]
329 plugin.register(self, self._site)
331 for item in plugins.enabled:
332 self.debug('Login plugin in enabled list: %s' % item)
333 if item not in plugins.available:
335 plugins.available[item].enable()
337 def add_subtree(self, name, page):
338 self.__dict__[name] = page
340 def get_first_login(self):
342 plugins = self._site[FACILITY]
344 first = plugins.enabled[0]
345 plugin = plugins.available[first]
348 def root(self, *args, **kwargs):
349 plugin = self.get_first_login()
351 trans = self.get_valid_transaction('login', **kwargs)
352 redirect = '%s/login/%s?%s' % (self.basepath,
355 raise cherrypy.HTTPRedirect(redirect)
356 return self._template('login/index.html', title='Login')
360 def __init__(self, *args, **kwargs):
361 super(Logout, self).__init__(*args, **kwargs)
364 def root(self, *args, **kwargs):
367 for provider in self.handlers:
368 self.debug("Calling logout for provider %s" % provider)
369 obj = self.handlers[provider]
373 return self._template('logout.html', title='Logout')
375 def add_handler(self, provider, handler):
377 Providers can register a logout handler here that is called
378 when the IdP logout link is accessed.
380 self.handlers[provider] = handler
385 def GET(self, *args, **kwargs):
387 session = UserSession()
390 # return to the caller if any
391 transdata = self.get_valid_transaction('login', **kwargs).retrieve()
392 if 'login_return' not in transdata:
393 raise cherrypy.HTTPError(401)
394 raise cherrypy.HTTPRedirect(transdata['login_return'])
396 def root(self, *args, **kwargs):
397 op = getattr(self, cherrypy.request.method, self.GET)
399 return op(*args, **kwargs)
402 class LoginManagerInstaller(object):
404 self.facility = FACILITY
408 def unconfigure(self, opts, changes):
411 def install_args(self, group):
412 raise NotImplementedError
414 def validate_args(self, args):
417 def configure(self, opts, changes):
418 raise NotImplementedError
421 class LoginMgrsInstall(object):
424 pi = PluginInstaller(LoginMgrsInstall, FACILITY)
425 self.plugins = pi.get_plugins()