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