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