a3020a424f5cec8f80ad3cf86b3690add32e0d20
[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 set_auth_error(self):
126         cherrypy.response.status = 401
127
128     def get_tree(self, site):
129         raise NotImplementedError
130
131     def register(self, root, site):
132         self._root = root
133         self._site = site
134
135     def next_login(self):
136         plugins = self._site[FACILITY]
137         try:
138             idx = plugins.enabled.index(self.name)
139             item = plugins.enabled[idx + 1]
140             return plugins.available[item]
141         except (ValueError, IndexError):
142             return None
143
144     def on_enable(self):
145
146         # and add self to the root
147         self._root.add_subtree(self.name, self.get_tree(self._site))
148
149         # Get handle of the info plugin
150         self.info = self._root.info
151
152
153 class LoginPageBase(Page):
154
155     def __init__(self, site, mgr):
156         super(LoginPageBase, self).__init__(site)
157         self.lm = mgr
158         self._Transaction = None
159
160     def root(self, *args, **kwargs):
161         raise cherrypy.HTTPError(500)
162
163
164 class LoginFormBase(LoginPageBase):
165
166     def __init__(self, site, mgr, page, template=None):
167         super(LoginFormBase, self).__init__(site, mgr)
168         self.formpage = page
169         self.formtemplate = template or 'login/form.html'
170         self.trans = None
171
172     def GET(self, *args, **kwargs):
173         context = self.create_tmpl_context()
174         # pylint: disable=star-args
175         return self._template(self.formtemplate, **context)
176
177     def root(self, *args, **kwargs):
178         self.trans = self.get_valid_transaction('login', **kwargs)
179         op = getattr(self, cherrypy.request.method, self.GET)
180         if callable(op):
181             return op(*args, **kwargs)
182
183     def create_tmpl_context(self, **kwargs):
184         next_url = None
185         next_login = self.lm.next_login()
186         if next_login:
187             next_url = '%s?%s' % (next_login.path,
188                                   self.trans.get_GET_arg())
189
190         cookie = SecureCookie(USERNAME_COOKIE)
191         cookie.receive()
192         username = cookie.value
193
194         target = None
195         if self.trans is not None:
196             tid = self.trans.transaction_id
197             target = self.trans.retrieve().get('login_target')
198             username = self.trans.retrieve().get('login_username')
199         if tid is None:
200             tid = ''
201
202         if username is None:
203             username = ''
204
205         context = {
206             "title": 'Login',
207             "action": '%s/%s' % (self.basepath, self.formpage),
208             "service_name": self.lm.service_name,
209             "username_text": self.lm.username_text,
210             "password_text": self.lm.password_text,
211             "description": self.lm.help_text,
212             "next_url": next_url,
213             "username": username,
214             "login_target": target,
215             "cancel_url": '%s/login/cancel?%s' % (self.basepath,
216                                                   self.trans.get_GET_arg()),
217         }
218         context.update(kwargs)
219         if self.trans is not None:
220             t = self.trans.get_POST_tuple()
221             context.update({t[0]: t[1]})
222
223         return context
224
225
226 FACILITY = 'login_config'
227
228
229 class Login(Page):
230
231     def __init__(self, *args, **kwargs):
232         super(Login, self).__init__(*args, **kwargs)
233         self.cancel = Cancel(*args, **kwargs)
234         self.info = Info(self._site)
235
236         plugins = PluginLoader(Login, FACILITY, 'LoginManager')
237         plugins.get_plugin_data()
238         self._site[FACILITY] = plugins
239
240         available = plugins.available.keys()
241         self._debug('Available login managers: %s' % str(available))
242
243         for item in plugins.available:
244             plugin = plugins.available[item]
245             plugin.register(self, self._site)
246
247         for item in plugins.enabled:
248             self._debug('Login plugin in enabled list: %s' % item)
249             if item not in plugins.available:
250                 continue
251             plugins.available[item].enable()
252
253     def add_subtree(self, name, page):
254         self.__dict__[name] = page
255
256     def get_first_login(self):
257         plugin = None
258         plugins = self._site[FACILITY]
259         if plugins.enabled:
260             first = plugins.enabled[0]
261             plugin = plugins.available[first]
262         return plugin
263
264     def root(self, *args, **kwargs):
265         plugin = self.get_first_login()
266         if plugin:
267             trans = self.get_valid_transaction('login', **kwargs)
268             redirect = '%s/login/%s?%s' % (self.basepath,
269                                            plugin.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 Cancel(Page):
283
284     def GET(self, *args, **kwargs):
285
286         session = UserSession()
287         session.logout(None)
288
289         # return to the caller if any
290         transdata = self.get_valid_transaction('login', **kwargs).retrieve()
291         if 'login_return' not in transdata:
292             raise cherrypy.HTTPError(401)
293         raise cherrypy.HTTPRedirect(transdata['login_return'])
294
295     def root(self, *args, **kwargs):
296         op = getattr(self, cherrypy.request.method, self.GET)
297         if callable(op):
298             return op(*args, **kwargs)
299
300
301 class LoginManagerInstaller(object):
302     def __init__(self):
303         self.facility = FACILITY
304         self.ptype = 'login'
305         self.name = None
306
307     def unconfigure(self, opts):
308         return
309
310     def install_args(self, group):
311         raise NotImplementedError
312
313     def configure(self, opts):
314         raise NotImplementedError
315
316
317 class LoginMgrsInstall(object):
318
319     def __init__(self):
320         pi = PluginInstaller(LoginMgrsInstall, FACILITY)
321         self.plugins = pi.get_plugins()