Handle invalid/expired transactions gracefully
[cascardo/ipsilon.git] / ipsilon / login / common.py
1 #!/usr/bin/python
2 #
3 # Copyright (C) 2013  Simo Sorce <simo@redhat.com>
4 #
5 # see file 'COPYING' for use and warranty information
6 #
7 # This program is free software; you can redistribute it and/or modify
8 # it under the terms of the GNU General Public License as published by
9 # the Free Software Foundation, either version 3 of the License, or
10 # (at your option) any later version.
11 #
12 # This program is distributed in the hope that it will be useful,
13 # but WITHOUT ANY WARRANTY; without even the implied warranty of
14 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15 # GNU General Public License for more details.
16 #
17 # You should have received a copy of the GNU General Public License
18 # along with this program.  If not, see <http://www.gnu.org/licenses/>.
19
20 from ipsilon.util.log import Log
21 from ipsilon.util.page import Page
22 from ipsilon.util.user import UserSession
23 from ipsilon.util.plugin import PluginLoader, PluginObject
24 from ipsilon.util.plugin import PluginInstaller
25 from ipsilon.info.common import Info
26 from ipsilon.util.cookies import SecureCookie
27 import cherrypy
28
29
30 USERNAME_COOKIE = 'ipsilon_default_username'
31
32
33 class LoginManagerBase(PluginObject, Log):
34
35     def __init__(self):
36         super(LoginManagerBase, self).__init__()
37         self.path = '/'
38         self.next_login = None
39         self.info = None
40
41     def redirect_to_path(self, path):
42         base = cherrypy.config.get('base.mount', "")
43         raise cherrypy.HTTPRedirect('%s/login/%s' % (base, path))
44
45     def auth_successful(self, trans, username, auth_type=None, userdata=None):
46         session = UserSession()
47
48         if self.info:
49             userattrs = self.info.get_user_attrs(username)
50             if userdata:
51                 userdata.update(userattrs.get('userdata', {}))
52             else:
53                 userdata = userattrs.get('userdata', {})
54
55             # merge groups and extras from login plugin and info plugin
56             userdata['groups'] = list(set(userdata.get('groups', []) +
57                                           userattrs.get('groups', [])))
58
59             userdata['extras'] = userdata.get('extras', {})
60             userdata['extras'].update(userattrs.get('extras', {}))
61
62             self.debug("User %s attributes: %s" % (username, repr(userdata)))
63
64         if auth_type:
65             if userdata:
66                 userdata.update({'auth_type': auth_type})
67             else:
68                 userdata = {'auth_type': auth_type}
69
70         # create session login including all the userdata just gathered
71         session.login(username, userdata)
72
73         # save username into a cookie if parent was form base auth
74         if auth_type == 'password':
75             cookie = SecureCookie(USERNAME_COOKIE, username)
76             # 15 days
77             cookie.maxage = 1296000
78             cookie.send()
79
80         transdata = trans.retrieve()
81         self.debug(transdata)
82         redirect = transdata.get('login_return',
83                                  cherrypy.config.get('base.mount', "") + '/')
84         self.debug('Redirecting back to: %s' % redirect)
85
86         # on direct login the UI (ie not redirected by a provider) we ned to
87         # remove the transaction cookie as it won't be needed anymore
88         if trans.provider == 'login':
89             self.debug('Wiping transaction data')
90             trans.wipe()
91         raise cherrypy.HTTPRedirect(redirect)
92
93     def auth_failed(self, trans):
94         # try with next module
95         if self.next_login:
96             return self.redirect_to_path(self.next_login.path)
97
98         # return to the caller if any
99         session = UserSession()
100
101         transdata = trans.retrieve()
102
103         # on direct login the UI (ie not redirected by a provider) we ned to
104         # remove the transaction cookie as it won't be needed anymore
105         if trans.provider == 'login':
106             trans.wipe()
107
108         # destroy session and return error
109         if 'login_return' not in transdata:
110             session.logout(None)
111             raise cherrypy.HTTPError(401)
112
113         raise cherrypy.HTTPRedirect(transdata['login_return'])
114
115     def get_tree(self, site):
116         raise NotImplementedError
117
118     def enable(self, site):
119         plugins = site[FACILITY]
120         if self in plugins['enabled']:
121             return
122
123         # configure self
124         if self.name in plugins['config']:
125             self.set_config(plugins['config'][self.name])
126
127         # and add self to the root
128         root = plugins['root']
129         root.add_subtree(self.name, self.get_tree(site))
130
131         # finally add self in login chain
132         prev_obj = None
133         for prev_obj in plugins['enabled']:
134             if prev_obj.next_login:
135                 break
136         if prev_obj:
137             while prev_obj.next_login:
138                 prev_obj = prev_obj.next_login
139             prev_obj.next_login = self
140         if not root.first_login:
141             root.first_login = self
142
143         plugins['enabled'].append(self)
144         self._debug('Login plugin enabled: %s' % self.name)
145
146         # Get handle of the info plugin
147         self.info = root.info
148
149     def disable(self, site):
150         plugins = site[FACILITY]
151         if self not in plugins['enabled']:
152             return
153
154         # remove self from chain
155         root = plugins['root']
156         if root.first_login == self:
157             root.first_login = self.next_login
158         elif root.first_login:
159             prev_obj = root.first_login
160             while prev_obj.next_login != self:
161                 prev_obj = prev_obj.next_login
162             if prev_obj:
163                 prev_obj.next_login = self.next_login
164         self.next_login = None
165
166         plugins['enabled'].remove(self)
167         self._debug('Login plugin disabled: %s' % self.name)
168
169
170 class LoginPageBase(Page):
171
172     def __init__(self, site, mgr):
173         super(LoginPageBase, self).__init__(site)
174         self.lm = mgr
175         self._Transaction = None
176
177     def root(self, *args, **kwargs):
178         raise cherrypy.HTTPError(500)
179
180
181 class LoginFormBase(LoginPageBase):
182
183     def __init__(self, site, mgr, page, template=None):
184         super(LoginFormBase, self).__init__(site, mgr)
185         self.formpage = page
186         self.formtemplate = template or 'login/form.html'
187         self.trans = None
188
189     def GET(self, *args, **kwargs):
190         context = self.create_tmpl_context()
191         # pylint: disable=star-args
192         return self._template(self.formtemplate, **context)
193
194     def root(self, *args, **kwargs):
195         self.trans = self.get_valid_transaction('login', **kwargs)
196         op = getattr(self, cherrypy.request.method, self.GET)
197         if callable(op):
198             return op(*args, **kwargs)
199
200     def create_tmpl_context(self, **kwargs):
201         next_url = None
202         if self.lm.next_login is not None:
203             next_url = '%s?%s' % (self.lm.next_login.path,
204                                   self.trans.get_GET_arg())
205
206         cookie = SecureCookie(USERNAME_COOKIE)
207         cookie.receive()
208         username = cookie.value
209         if username is None:
210             username = ''
211
212         target = None
213         if self.trans is not None:
214             tid = self.trans.transaction_id
215             target = self.trans.retrieve().get('login_target')
216         if tid is None:
217             tid = ''
218
219         context = {
220             "title": 'Login',
221             "action": '%s/%s' % (self.basepath, self.formpage),
222             "service_name": self.lm.service_name,
223             "username_text": self.lm.username_text,
224             "password_text": self.lm.password_text,
225             "description": self.lm.help_text,
226             "next_url": next_url,
227             "username": username,
228             "login_target": target,
229         }
230         context.update(kwargs)
231         if self.trans is not None:
232             t = self.trans.get_POST_tuple()
233             context.update({t[0]: t[1]})
234
235         return context
236
237
238 FACILITY = 'login_config'
239
240
241 class Login(Page):
242
243     def __init__(self, *args, **kwargs):
244         super(Login, self).__init__(*args, **kwargs)
245         self.first_login = None
246         self.info = Info(self._site)
247
248         loader = PluginLoader(Login, FACILITY, 'LoginManager')
249         self._site[FACILITY] = loader.get_plugin_data()
250         plugins = self._site[FACILITY]
251
252         available = plugins['available'].keys()
253         self._debug('Available login managers: %s' % str(available))
254
255         plugins['root'] = self
256         for item in plugins['whitelist']:
257             self._debug('Login plugin in whitelist: %s' % item)
258             if item not in plugins['available']:
259                 continue
260             plugins['available'][item].enable(self._site)
261
262     def add_subtree(self, name, page):
263         self.__dict__[name] = page
264
265     def root(self, *args, **kwargs):
266         if self.first_login:
267             trans = self.get_valid_transaction('login', **kwargs)
268             redirect = '%s/login/%s?%s' % (self.basepath,
269                                            self.first_login.path,
270                                            trans.get_GET_arg())
271             raise cherrypy.HTTPRedirect(redirect)
272         return self._template('login/index.html', title='Login')
273
274
275 class Logout(Page):
276
277     def root(self, *args, **kwargs):
278         UserSession().logout(self.user)
279         return self._template('logout.html', title='Logout')
280
281
282 class LoginMgrsInstall(object):
283
284     def __init__(self):
285         pi = PluginInstaller(LoginMgrsInstall)
286         self.plugins = pi.get_plugins()