Return to provider if user cancels login
[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             "cancel_url": '%s/login/cancel?%s' % (self.basepath,
230                                                   self.trans.get_GET_arg()),
231         }
232         context.update(kwargs)
233         if self.trans is not None:
234             t = self.trans.get_POST_tuple()
235             context.update({t[0]: t[1]})
236
237         return context
238
239
240 FACILITY = 'login_config'
241
242
243 class Login(Page):
244
245     def __init__(self, *args, **kwargs):
246         super(Login, self).__init__(*args, **kwargs)
247         self.cancel = Cancel(*args, **kwargs)
248         self.first_login = None
249         self.info = Info(self._site)
250
251         loader = PluginLoader(Login, FACILITY, 'LoginManager')
252         self._site[FACILITY] = loader.get_plugin_data()
253         plugins = self._site[FACILITY]
254
255         available = plugins['available'].keys()
256         self._debug('Available login managers: %s' % str(available))
257
258         plugins['root'] = self
259         for item in plugins['whitelist']:
260             self._debug('Login plugin in whitelist: %s' % item)
261             if item not in plugins['available']:
262                 continue
263             plugins['available'][item].enable(self._site)
264
265     def add_subtree(self, name, page):
266         self.__dict__[name] = page
267
268     def root(self, *args, **kwargs):
269         if self.first_login:
270             trans = self.get_valid_transaction('login', **kwargs)
271             redirect = '%s/login/%s?%s' % (self.basepath,
272                                            self.first_login.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
280     def root(self, *args, **kwargs):
281         UserSession().logout(self.user)
282         return self._template('logout.html', title='Logout')
283
284
285 class Cancel(Page):
286
287     def GET(self, *args, **kwargs):
288
289         session = UserSession()
290         session.logout(None)
291
292         # return to the caller if any
293         transdata = self.get_valid_transaction('login', **kwargs).retrieve()
294         if 'login_return' not in transdata:
295             raise cherrypy.HTTPError(401)
296         raise cherrypy.HTTPRedirect(transdata['login_return'])
297
298     def root(self, *args, **kwargs):
299         op = getattr(self, cherrypy.request.method, self.GET)
300         if callable(op):
301             return op(*args, **kwargs)
302
303
304 class LoginMgrsInstall(object):
305
306     def __init__(self):
307         pi = PluginInstaller(LoginMgrsInstall)
308         self.plugins = pi.get_plugins()