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