9496a4bb8662509176109cbe587a182e39f1c7f2
[cascardo/ipsilon.git] / ipsilon / login / common.py
1 # Copyright (C) 2013  Simo Sorce <simo@redhat.com>
2 #
3 # see file 'COPYING' for use and warranty information
4 #
5 # This program is free software; you can redistribute it and/or modify
6 # it under the terms of the GNU General Public License as published by
7 # the Free Software Foundation, either version 3 of the License, or
8 # (at your option) any later version.
9 #
10 # This program is distributed in the hope that it will be useful,
11 # but WITHOUT ANY WARRANTY; without even the implied warranty of
12 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13 # GNU General Public License for more details.
14 #
15 # You should have received a copy of the GNU General Public License
16 # along with this program.  If not, see <http://www.gnu.org/licenses/>.
17
18 from ipsilon.util.page import Page
19 from ipsilon.util.user import UserSession
20 from ipsilon.util.plugin import PluginInstaller, PluginLoader
21 from ipsilon.util.plugin import PluginObject
22 from ipsilon.util.config import ConfigHelper
23 from ipsilon.info.common import Info
24 from ipsilon.util.cookies import SecureCookie
25 import cherrypy
26
27
28 USERNAME_COOKIE = 'ipsilon_default_username'
29
30
31 class LoginManagerBase(ConfigHelper, PluginObject):
32
33     def __init__(self, *args):
34         ConfigHelper.__init__(self)
35         PluginObject.__init__(self, *args)
36         self._root = None
37         self._site = None
38         self.path = '/'
39         self.info = None
40
41     def redirect_to_path(self, path, trans=None):
42         base = cherrypy.config.get('base.mount', "")
43         url = '%s/login/%s' % (base, path)
44         if trans:
45             url += '?%s' % trans.get_GET_arg()
46         raise cherrypy.HTTPRedirect(url)
47
48     def auth_successful(self, trans, username, auth_type=None, userdata=None):
49         session = UserSession()
50
51         # merge attributes from login plugin and info plugin
52         if self.info:
53             infoattrs = self.info.get_user_attrs(username)
54         else:
55             infoattrs = dict()
56
57         if userdata is None:
58             userdata = dict()
59
60         if '_groups' in infoattrs:
61             userdata['_groups'] = list(set(userdata.get('_groups', []) +
62                                            infoattrs['_groups']))
63             del infoattrs['_groups']
64
65         if '_extras' in infoattrs:
66             userdata['_extras'] = userdata.get('_extras', {})
67             userdata['_extras'].update(infoattrs['_extras'])
68             del infoattrs['_extras']
69
70         userdata.update(infoattrs)
71
72         self.debug("User %s attributes: %s" % (username, repr(userdata)))
73
74         if auth_type:
75             if userdata:
76                 userdata.update({'_auth_type': auth_type})
77             else:
78                 userdata = {'_auth_type': auth_type}
79
80         # create session login including all the userdata just gathered
81         session.login(username, userdata)
82
83         # save username into a cookie if parent was form base auth
84         if auth_type == 'password':
85             cookie = SecureCookie(USERNAME_COOKIE, username)
86             # 15 days
87             cookie.maxage = 1296000
88             cookie.send()
89
90         transdata = trans.retrieve()
91         self.debug(transdata)
92         redirect = transdata.get('login_return',
93                                  cherrypy.config.get('base.mount', "") + '/')
94         self.debug('Redirecting back to: %s' % redirect)
95
96         # on direct login the UI (ie not redirected by a provider) we ned to
97         # remove the transaction cookie as it won't be needed anymore
98         if trans.provider == 'login':
99             self.debug('Wiping transaction data')
100             trans.wipe()
101         raise cherrypy.HTTPRedirect(redirect)
102
103     def auth_failed(self, trans):
104         # try with next module
105         next_login = self.next_login()
106         if next_login:
107             return self.redirect_to_path(next_login.path, trans)
108
109         # return to the caller if any
110         session = UserSession()
111
112         transdata = trans.retrieve()
113
114         # on direct login the UI (ie not redirected by a provider) we ned to
115         # remove the transaction cookie as it won't be needed anymore
116         if trans.provider == 'login':
117             trans.wipe()
118
119         # destroy session and return error
120         if 'login_return' not in transdata:
121             session.logout(None)
122             raise cherrypy.HTTPError(401)
123
124         raise cherrypy.HTTPRedirect(transdata['login_return'])
125
126     def set_auth_error(self):
127         cherrypy.response.status = 401
128
129     def get_tree(self, site):
130         raise NotImplementedError
131
132     def register(self, root, site):
133         self._root = root
134         self._site = site
135
136     def next_login(self):
137         plugins = self._site[FACILITY]
138         try:
139             idx = plugins.enabled.index(self.name)
140             item = plugins.enabled[idx + 1]
141             return plugins.available[item]
142         except (ValueError, IndexError):
143             return None
144
145     def on_enable(self):
146
147         # and add self to the root
148         self._root.add_subtree(self.name, self.get_tree(self._site))
149
150         # Get handle of the info plugin
151         self.info = self._root.info
152
153
154 class LoginPageBase(Page):
155
156     def __init__(self, site, mgr):
157         super(LoginPageBase, self).__init__(site)
158         self.lm = mgr
159         self._Transaction = None
160
161     def root(self, *args, **kwargs):
162         raise cherrypy.HTTPError(500)
163
164
165 class LoginFormBase(LoginPageBase):
166
167     def __init__(self, site, mgr, page, template=None):
168         super(LoginFormBase, self).__init__(site, mgr)
169         self.formpage = page
170         self.formtemplate = template or 'login/form.html'
171         self.trans = None
172
173     def GET(self, *args, **kwargs):
174         context = self.create_tmpl_context()
175         # pylint: disable=star-args
176         return self._template(self.formtemplate, **context)
177
178     def root(self, *args, **kwargs):
179         self.trans = self.get_valid_transaction('login', **kwargs)
180         op = getattr(self, cherrypy.request.method, self.GET)
181         if callable(op):
182             return op(*args, **kwargs)
183
184     def create_tmpl_context(self, **kwargs):
185         next_url = None
186         next_login = self.lm.next_login()
187         if next_login:
188             next_url = '%s?%s' % (next_login.path,
189                                   self.trans.get_GET_arg())
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             "next_url": next_url,
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()