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