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