7cf0c2ac739ebdbf972b24fb4f57af37b93c7211
[cascardo/ipsilon.git] / ipsilon / login / common.py
1 # Copyright (C) 2013 Ipsilon project Contributors, for license see COPYING
2
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 import cherrypy
11
12
13 USERNAME_COOKIE = 'ipsilon_default_username'
14
15
16 class LoginManagerBase(ConfigHelper, PluginObject):
17
18     def __init__(self, *args):
19         ConfigHelper.__init__(self)
20         PluginObject.__init__(self, *args)
21         self._root = None
22         self._site = None
23         self.path = '/'
24         self.info = None
25
26     def redirect_to_path(self, path, trans=None):
27         base = cherrypy.config.get('base.mount', "")
28         url = '%s/login/%s' % (base, path)
29         if trans:
30             url += '?%s' % trans.get_GET_arg()
31         raise cherrypy.HTTPRedirect(url)
32
33     def auth_successful(self, trans, username, auth_type=None, userdata=None):
34         session = UserSession()
35
36         # merge attributes from login plugin and info plugin
37         if self.info:
38             infoattrs = self.info.get_user_attrs(username)
39         else:
40             infoattrs = dict()
41
42         if userdata is None:
43             userdata = dict()
44
45         if '_groups' in infoattrs:
46             userdata['_groups'] = list(set(userdata.get('_groups', []) +
47                                            infoattrs['_groups']))
48             del infoattrs['_groups']
49
50         if '_extras' in infoattrs:
51             userdata['_extras'] = userdata.get('_extras', {})
52             userdata['_extras'].update(infoattrs['_extras'])
53             del infoattrs['_extras']
54
55         userdata.update(infoattrs)
56
57         self.debug("User %s attributes: %s" % (username, repr(userdata)))
58
59         if auth_type:
60             if userdata:
61                 userdata.update({'_auth_type': auth_type})
62             else:
63                 userdata = {'_auth_type': auth_type}
64
65         # create session login including all the userdata just gathered
66         session.login(username, userdata)
67
68         # save username into a cookie if parent was form base auth
69         if auth_type == 'password':
70             cookie = SecureCookie(USERNAME_COOKIE, username)
71             # 15 days
72             cookie.maxage = 1296000
73             cookie.send()
74
75         transdata = trans.retrieve()
76         self.debug(transdata)
77         redirect = transdata.get('login_return',
78                                  cherrypy.config.get('base.mount', "") + '/')
79         self.debug('Redirecting back to: %s' % redirect)
80
81         # on direct login the UI (ie not redirected by a provider) we ned to
82         # remove the transaction cookie as it won't be needed anymore
83         if trans.provider == 'login':
84             self.debug('Wiping transaction data')
85             trans.wipe()
86         raise cherrypy.HTTPRedirect(redirect)
87
88     def auth_failed(self, trans):
89         # try with next module
90         next_login = self.next_login()
91         if next_login:
92             return self.redirect_to_path(next_login.path, trans)
93
94         # return to the caller if any
95         session = UserSession()
96
97         transdata = trans.retrieve()
98
99         # on direct login the UI (ie not redirected by a provider) we ned to
100         # remove the transaction cookie as it won't be needed anymore
101         if trans.provider == 'login':
102             trans.wipe()
103
104         # destroy session and return error
105         if 'login_return' not in transdata:
106             session.logout(None)
107             raise cherrypy.HTTPError(401)
108
109         raise cherrypy.HTTPRedirect(transdata['login_return'])
110
111     def set_auth_error(self):
112         cherrypy.response.status = 401
113
114     def get_tree(self, site):
115         raise NotImplementedError
116
117     def register(self, root, site):
118         self._root = root
119         self._site = site
120
121     def next_login(self):
122         plugins = self._site[FACILITY]
123         try:
124             idx = plugins.enabled.index(self.name)
125             item = plugins.enabled[idx + 1]
126             return plugins.available[item]
127         except (ValueError, IndexError):
128             return None
129
130     def other_login_stacks(self):
131         plugins = self._site[FACILITY]
132         stack = list()
133         try:
134             idx = plugins.enabled.index(self.name)
135         except (ValueError, IndexError):
136             idx = None
137         for i in range(0, len(plugins.enabled)):
138             if i == idx:
139                 continue
140             stack.append(plugins.available[plugins.enabled[i]])
141         return stack
142
143     def on_enable(self):
144
145         # and add self to the root
146         self._root.add_subtree(self.name, self.get_tree(self._site))
147
148         # Get handle of the info plugin
149         self.info = self._root.info
150
151
152 class LoginPageBase(Page):
153
154     def __init__(self, site, mgr):
155         super(LoginPageBase, self).__init__(site)
156         self.lm = mgr
157         self._Transaction = None
158
159     def root(self, *args, **kwargs):
160         raise cherrypy.HTTPError(500)
161
162
163 class LoginFormBase(LoginPageBase):
164
165     def __init__(self, site, mgr, page, template=None):
166         super(LoginFormBase, self).__init__(site, mgr)
167         self.formpage = page
168         self.formtemplate = template or 'login/form.html'
169         self.trans = None
170
171     def GET(self, *args, **kwargs):
172         context = self.create_tmpl_context()
173         return self._template(self.formtemplate, **context)
174
175     def root(self, *args, **kwargs):
176         self.trans = self.get_valid_transaction('login', **kwargs)
177         op = getattr(self, cherrypy.request.method, self.GET)
178         if callable(op):
179             return op(*args, **kwargs)
180
181     def create_tmpl_context(self, **kwargs):
182         other_stacks = None
183         other_login_stacks = self.lm.other_login_stacks()
184         if other_login_stacks:
185             other_stacks = list()
186             for ls in other_login_stacks:
187                 url = '%s?%s' % (ls.path, self.trans.get_GET_arg())
188                 name = ls.name
189                 other_stacks.append({'url': url, 'name': name})
190
191         cookie = SecureCookie(USERNAME_COOKIE)
192         cookie.receive()
193         username = cookie.value
194
195         target = None
196         if self.trans is not None:
197             tid = self.trans.transaction_id
198             target = self.trans.retrieve().get('login_target')
199             username = self.trans.retrieve().get('login_username')
200         if tid is None:
201             tid = ''
202
203         if username is None:
204             username = ''
205
206         context = {
207             "title": 'Login',
208             "action": '%s/%s' % (self.basepath, self.formpage),
209             "service_name": self.lm.service_name,
210             "username_text": self.lm.username_text,
211             "password_text": self.lm.password_text,
212             "description": self.lm.help_text,
213             "other_stacks": other_stacks,
214             "username": username,
215             "login_target": target,
216             "cancel_url": '%s/login/cancel?%s' % (self.basepath,
217                                                   self.trans.get_GET_arg()),
218         }
219         context.update(kwargs)
220         if self.trans is not None:
221             t = self.trans.get_POST_tuple()
222             context.update({t[0]: t[1]})
223
224         return context
225
226
227 FACILITY = 'login_config'
228
229
230 class Login(Page):
231
232     def __init__(self, *args, **kwargs):
233         super(Login, self).__init__(*args, **kwargs)
234         self.cancel = Cancel(*args, **kwargs)
235         self.info = Info(self._site)
236
237         plugins = PluginLoader(Login, FACILITY, 'LoginManager')
238         plugins.get_plugin_data()
239         self._site[FACILITY] = plugins
240
241         available = plugins.available.keys()
242         self.debug('Available login managers: %s' % str(available))
243
244         for item in plugins.available:
245             plugin = plugins.available[item]
246             plugin.register(self, self._site)
247
248         for item in plugins.enabled:
249             self.debug('Login plugin in enabled list: %s' % item)
250             if item not in plugins.available:
251                 continue
252             plugins.available[item].enable()
253
254     def add_subtree(self, name, page):
255         self.__dict__[name] = page
256
257     def get_first_login(self):
258         plugin = None
259         plugins = self._site[FACILITY]
260         if plugins.enabled:
261             first = plugins.enabled[0]
262             plugin = plugins.available[first]
263         return plugin
264
265     def root(self, *args, **kwargs):
266         plugin = self.get_first_login()
267         if plugin:
268             trans = self.get_valid_transaction('login', **kwargs)
269             redirect = '%s/login/%s?%s' % (self.basepath,
270                                            plugin.path,
271                                            trans.get_GET_arg())
272             raise cherrypy.HTTPRedirect(redirect)
273         return self._template('login/index.html', title='Login')
274
275
276 class Logout(Page):
277     def __init__(self, *args, **kwargs):
278         super(Logout, self).__init__(*args, **kwargs)
279         self.handlers = {}
280
281     def root(self, *args, **kwargs):
282         us = UserSession()
283
284         for provider in self.handlers:
285             self.debug("Calling logout for provider %s" % provider)
286             obj = self.handlers[provider]
287             obj()
288
289         us.logout(self.user)
290         return self._template('logout.html', title='Logout')
291
292     def add_handler(self, provider, handler):
293         """
294         Providers can register a logout handler here that is called
295         when the IdP logout link is accessed.
296         """
297         self.handlers[provider] = handler
298
299
300 class Cancel(Page):
301
302     def GET(self, *args, **kwargs):
303
304         session = UserSession()
305         session.logout(None)
306
307         # return to the caller if any
308         transdata = self.get_valid_transaction('login', **kwargs).retrieve()
309         if 'login_return' not in transdata:
310             raise cherrypy.HTTPError(401)
311         raise cherrypy.HTTPRedirect(transdata['login_return'])
312
313     def root(self, *args, **kwargs):
314         op = getattr(self, cherrypy.request.method, self.GET)
315         if callable(op):
316             return op(*args, **kwargs)
317
318
319 class LoginManagerInstaller(object):
320     def __init__(self):
321         self.facility = FACILITY
322         self.ptype = 'login'
323         self.name = None
324
325     def unconfigure(self, opts):
326         return
327
328     def install_args(self, group):
329         raise NotImplementedError
330
331     def validate_args(self, args):
332         return
333
334     def configure(self, opts):
335         raise NotImplementedError
336
337
338 class LoginMgrsInstall(object):
339
340     def __init__(self):
341         pi = PluginInstaller(LoginMgrsInstall, FACILITY)
342         self.plugins = pi.get_plugins()