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