cd4f166ff10ddf21c5c7cc04836149c8d614f682
[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, message=None):
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, message)
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/login/%s?%s' % (
188                     self.basepath, ls.path, self.trans.get_GET_arg()
189                 )
190                 name = ls.name
191                 other_stacks.append({'url': url, 'name': name})
192
193         cookie = SecureCookie(USERNAME_COOKIE)
194         cookie.receive()
195         username = cookie.value
196
197         target = None
198         if self.trans is not None:
199             tid = self.trans.transaction_id
200             target = self.trans.retrieve().get('login_target')
201             username = self.trans.retrieve().get('login_username')
202         if tid is None:
203             tid = ''
204
205         if username is None:
206             username = ''
207
208         context = {
209             "title": 'Login',
210             "action": '%s/%s' % (self.basepath, self.formpage),
211             "service_name": self.lm.service_name,
212             "username_text": self.lm.username_text,
213             "password_text": self.lm.password_text,
214             "description": self.lm.help_text,
215             "other_stacks": other_stacks,
216             "username": username,
217             "login_target": target,
218             "cancel_url": '%s/login/cancel?%s' % (self.basepath,
219                                                   self.trans.get_GET_arg()),
220         }
221         context.update(kwargs)
222         if self.trans is not None:
223             t = self.trans.get_POST_tuple()
224             context.update({t[0]: t[1]})
225
226         return context
227
228
229 FACILITY = 'login_config'
230
231
232 class Login(Page):
233
234     def __init__(self, *args, **kwargs):
235         super(Login, self).__init__(*args, **kwargs)
236         self.cancel = Cancel(*args, **kwargs)
237         self.info = Info(self._site)
238
239         plugins = PluginLoader(Login, FACILITY, 'LoginManager')
240         plugins.get_plugin_data()
241         self._site[FACILITY] = plugins
242
243         available = plugins.available.keys()
244         self.debug('Available login managers: %s' % str(available))
245
246         for item in plugins.available:
247             plugin = plugins.available[item]
248             plugin.register(self, self._site)
249
250         for item in plugins.enabled:
251             self.debug('Login plugin in enabled list: %s' % item)
252             if item not in plugins.available:
253                 continue
254             plugins.available[item].enable()
255
256     def add_subtree(self, name, page):
257         self.__dict__[name] = page
258
259     def get_first_login(self):
260         plugin = None
261         plugins = self._site[FACILITY]
262         if plugins.enabled:
263             first = plugins.enabled[0]
264             plugin = plugins.available[first]
265         return plugin
266
267     def root(self, *args, **kwargs):
268         plugin = self.get_first_login()
269         if plugin:
270             trans = self.get_valid_transaction('login', **kwargs)
271             redirect = '%s/login/%s?%s' % (self.basepath,
272                                            plugin.path,
273                                            trans.get_GET_arg())
274             raise cherrypy.HTTPRedirect(redirect)
275         return self._template('login/index.html', title='Login')
276
277
278 class Logout(Page):
279     def __init__(self, *args, **kwargs):
280         super(Logout, self).__init__(*args, **kwargs)
281         self.handlers = {}
282
283     def root(self, *args, **kwargs):
284         us = UserSession()
285
286         for provider in self.handlers:
287             self.debug("Calling logout for provider %s" % provider)
288             obj = self.handlers[provider]
289             obj()
290
291         us.logout(self.user)
292         return self._template('logout.html', title='Logout')
293
294     def add_handler(self, provider, handler):
295         """
296         Providers can register a logout handler here that is called
297         when the IdP logout link is accessed.
298         """
299         self.handlers[provider] = handler
300
301
302 class Cancel(Page):
303
304     def GET(self, *args, **kwargs):
305
306         session = UserSession()
307         session.logout(None)
308
309         # return to the caller if any
310         transdata = self.get_valid_transaction('login', **kwargs).retrieve()
311         if 'login_return' not in transdata:
312             raise cherrypy.HTTPError(401)
313         raise cherrypy.HTTPRedirect(transdata['login_return'])
314
315     def root(self, *args, **kwargs):
316         op = getattr(self, cherrypy.request.method, self.GET)
317         if callable(op):
318             return op(*args, **kwargs)
319
320
321 class LoginManagerInstaller(object):
322     def __init__(self):
323         self.facility = FACILITY
324         self.ptype = 'login'
325         self.name = None
326
327     def unconfigure(self, opts, changes):
328         return
329
330     def install_args(self, group):
331         raise NotImplementedError
332
333     def validate_args(self, args):
334         return
335
336     def configure(self, opts, changes):
337         raise NotImplementedError
338
339
340 class LoginMgrsInstall(object):
341
342     def __init__(self):
343         pi = PluginInstaller(LoginMgrsInstall, FACILITY)
344         self.plugins = pi.get_plugins()