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