c7c8050f3e2d00736433677158f3866ecbd9e9e4
[cascardo/ipsilon.git] / ipsilon / login / common.py
1 # Copyright (C) 2013  Simo Sorce <simo@redhat.com>
2 #
3 # see file 'COPYING' for use and warranty information
4 #
5 # This program is free software; you can redistribute it and/or modify
6 # it under the terms of the GNU General Public License as published by
7 # the Free Software Foundation, either version 3 of the License, or
8 # (at your option) any later version.
9 #
10 # This program is distributed in the hope that it will be useful,
11 # but WITHOUT ANY WARRANTY; without even the implied warranty of
12 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13 # GNU General Public License for more details.
14 #
15 # You should have received a copy of the GNU General Public License
16 # along with this program.  If not, see <http://www.gnu.org/licenses/>.
17
18 from ipsilon.util.page import Page
19 from ipsilon.util.user import UserSession
20 from ipsilon.util.plugin import PluginInstaller, PluginLoader
21 from ipsilon.util.plugin import PluginObject
22 from ipsilon.util.config import ConfigHelper
23 from ipsilon.info.common import Info
24 from ipsilon.util.cookies import SecureCookie
25 import cherrypy
26
27
28 USERNAME_COOKIE = 'ipsilon_default_username'
29
30
31 class LoginManagerBase(ConfigHelper, PluginObject):
32
33     def __init__(self, *args):
34         ConfigHelper.__init__(self)
35         PluginObject.__init__(self, *args)
36         self._root = None
37         self._site = None
38         self.path = '/'
39         self.info = None
40
41     def redirect_to_path(self, path, trans=None):
42         base = cherrypy.config.get('base.mount', "")
43         url = '%s/login/%s' % (base, path)
44         if trans:
45             url += '?%s' % trans.get_GET_arg()
46         raise cherrypy.HTTPRedirect(url)
47
48     def auth_successful(self, trans, username, auth_type=None, userdata=None):
49         session = UserSession()
50
51         # merge attributes from login plugin and info plugin
52         if self.info:
53             infoattrs = self.info.get_user_attrs(username)
54         else:
55             infoattrs = dict()
56
57         if userdata is None:
58             userdata = dict()
59
60         if '_groups' in infoattrs:
61             userdata['_groups'] = list(set(userdata.get('_groups', []) +
62                                            infoattrs['_groups']))
63             del infoattrs['_groups']
64
65         if '_extras' in infoattrs:
66             userdata['_extras'] = userdata.get('_extras', {})
67             userdata['_extras'].update(infoattrs['_extras'])
68             del infoattrs['_extras']
69
70         userdata.update(infoattrs)
71
72         self.debug("User %s attributes: %s" % (username, repr(userdata)))
73
74         if auth_type:
75             if userdata:
76                 userdata.update({'_auth_type': auth_type})
77             else:
78                 userdata = {'_auth_type': auth_type}
79
80         # create session login including all the userdata just gathered
81         session.login(username, userdata)
82
83         # save username into a cookie if parent was form base auth
84         if auth_type == 'password':
85             cookie = SecureCookie(USERNAME_COOKIE, username)
86             # 15 days
87             cookie.maxage = 1296000
88             cookie.send()
89
90         transdata = trans.retrieve()
91         self.debug(transdata)
92         redirect = transdata.get('login_return',
93                                  cherrypy.config.get('base.mount', "") + '/')
94         self.debug('Redirecting back to: %s' % redirect)
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             self.debug('Wiping transaction data')
100             trans.wipe()
101         raise cherrypy.HTTPRedirect(redirect)
102
103     def auth_failed(self, trans):
104         # try with next module
105         next_login = self.next_login()
106         if next_login:
107             return self.redirect_to_path(next_login.path, trans)
108
109         # return to the caller if any
110         session = UserSession()
111
112         transdata = trans.retrieve()
113
114         # on direct login the UI (ie not redirected by a provider) we ned to
115         # remove the transaction cookie as it won't be needed anymore
116         if trans.provider == 'login':
117             trans.wipe()
118
119         # destroy session and return error
120         if 'login_return' not in transdata:
121             session.logout(None)
122             raise cherrypy.HTTPError(401)
123
124         raise cherrypy.HTTPRedirect(transdata['login_return'])
125
126     def set_auth_error(self):
127         cherrypy.response.status = 401
128
129     def get_tree(self, site):
130         raise NotImplementedError
131
132     def register(self, root, site):
133         self._root = root
134         self._site = site
135
136     def next_login(self):
137         plugins = self._site[FACILITY]
138         try:
139             idx = plugins.enabled.index(self.name)
140             item = plugins.enabled[idx + 1]
141             return plugins.available[item]
142         except (ValueError, IndexError):
143             return None
144
145     def other_login_stacks(self):
146         plugins = self._site[FACILITY]
147         stack = list()
148         try:
149             idx = plugins.enabled.index(self.name)
150         except (ValueError, IndexError):
151             idx = None
152         for i in range(0, len(plugins.enabled)):
153             if i == idx:
154                 continue
155             stack.append(plugins.available[plugins.enabled[i]])
156         return stack
157
158     def on_enable(self):
159
160         # and add self to the root
161         self._root.add_subtree(self.name, self.get_tree(self._site))
162
163         # Get handle of the info plugin
164         self.info = self._root.info
165
166
167 class LoginPageBase(Page):
168
169     def __init__(self, site, mgr):
170         super(LoginPageBase, self).__init__(site)
171         self.lm = mgr
172         self._Transaction = None
173
174     def root(self, *args, **kwargs):
175         raise cherrypy.HTTPError(500)
176
177
178 class LoginFormBase(LoginPageBase):
179
180     def __init__(self, site, mgr, page, template=None):
181         super(LoginFormBase, self).__init__(site, mgr)
182         self.formpage = page
183         self.formtemplate = template or 'login/form.html'
184         self.trans = None
185
186     def GET(self, *args, **kwargs):
187         context = self.create_tmpl_context()
188         return self._template(self.formtemplate, **context)
189
190     def root(self, *args, **kwargs):
191         self.trans = self.get_valid_transaction('login', **kwargs)
192         op = getattr(self, cherrypy.request.method, self.GET)
193         if callable(op):
194             return op(*args, **kwargs)
195
196     def create_tmpl_context(self, **kwargs):
197         other_stacks = None
198         other_login_stacks = self.lm.other_login_stacks()
199         if other_login_stacks:
200             other_stacks = list()
201             for ls in other_login_stacks:
202                 url = '%s?%s' % (ls.path, self.trans.get_GET_arg())
203                 name = ls.name
204                 other_stacks.append({'url': url, 'name': name})
205
206         cookie = SecureCookie(USERNAME_COOKIE)
207         cookie.receive()
208         username = cookie.value
209
210         target = None
211         if self.trans is not None:
212             tid = self.trans.transaction_id
213             target = self.trans.retrieve().get('login_target')
214             username = self.trans.retrieve().get('login_username')
215         if tid is None:
216             tid = ''
217
218         if username is None:
219             username = ''
220
221         context = {
222             "title": 'Login',
223             "action": '%s/%s' % (self.basepath, self.formpage),
224             "service_name": self.lm.service_name,
225             "username_text": self.lm.username_text,
226             "password_text": self.lm.password_text,
227             "description": self.lm.help_text,
228             "other_stacks": other_stacks,
229             "username": username,
230             "login_target": target,
231             "cancel_url": '%s/login/cancel?%s' % (self.basepath,
232                                                   self.trans.get_GET_arg()),
233         }
234         context.update(kwargs)
235         if self.trans is not None:
236             t = self.trans.get_POST_tuple()
237             context.update({t[0]: t[1]})
238
239         return context
240
241
242 FACILITY = 'login_config'
243
244
245 class Login(Page):
246
247     def __init__(self, *args, **kwargs):
248         super(Login, self).__init__(*args, **kwargs)
249         self.cancel = Cancel(*args, **kwargs)
250         self.info = Info(self._site)
251
252         plugins = PluginLoader(Login, FACILITY, 'LoginManager')
253         plugins.get_plugin_data()
254         self._site[FACILITY] = plugins
255
256         available = plugins.available.keys()
257         self.debug('Available login managers: %s' % str(available))
258
259         for item in plugins.available:
260             plugin = plugins.available[item]
261             plugin.register(self, self._site)
262
263         for item in plugins.enabled:
264             self.debug('Login plugin in enabled list: %s' % item)
265             if item not in plugins.available:
266                 continue
267             plugins.available[item].enable()
268
269     def add_subtree(self, name, page):
270         self.__dict__[name] = page
271
272     def get_first_login(self):
273         plugin = None
274         plugins = self._site[FACILITY]
275         if plugins.enabled:
276             first = plugins.enabled[0]
277             plugin = plugins.available[first]
278         return plugin
279
280     def root(self, *args, **kwargs):
281         plugin = self.get_first_login()
282         if plugin:
283             trans = self.get_valid_transaction('login', **kwargs)
284             redirect = '%s/login/%s?%s' % (self.basepath,
285                                            plugin.path,
286                                            trans.get_GET_arg())
287             raise cherrypy.HTTPRedirect(redirect)
288         return self._template('login/index.html', title='Login')
289
290
291 class Logout(Page):
292     def __init__(self, *args, **kwargs):
293         super(Logout, self).__init__(*args, **kwargs)
294         self.handlers = {}
295
296     def root(self, *args, **kwargs):
297         us = UserSession()
298
299         for provider in self.handlers:
300             self.debug("Calling logout for provider %s" % provider)
301             obj = self.handlers[provider]
302             obj()
303
304         us.logout(self.user)
305         return self._template('logout.html', title='Logout')
306
307     def add_handler(self, provider, handler):
308         """
309         Providers can register a logout handler here that is called
310         when the IdP logout link is accessed.
311         """
312         self.handlers[provider] = handler
313
314
315 class Cancel(Page):
316
317     def GET(self, *args, **kwargs):
318
319         session = UserSession()
320         session.logout(None)
321
322         # return to the caller if any
323         transdata = self.get_valid_transaction('login', **kwargs).retrieve()
324         if 'login_return' not in transdata:
325             raise cherrypy.HTTPError(401)
326         raise cherrypy.HTTPRedirect(transdata['login_return'])
327
328     def root(self, *args, **kwargs):
329         op = getattr(self, cherrypy.request.method, self.GET)
330         if callable(op):
331             return op(*args, **kwargs)
332
333
334 class LoginManagerInstaller(object):
335     def __init__(self):
336         self.facility = FACILITY
337         self.ptype = 'login'
338         self.name = None
339
340     def unconfigure(self, opts):
341         return
342
343     def install_args(self, group):
344         raise NotImplementedError
345
346     def validate_args(self, args):
347         return
348
349     def configure(self, opts):
350         raise NotImplementedError
351
352
353 class LoginMgrsInstall(object):
354
355     def __init__(self):
356         pi = PluginInstaller(LoginMgrsInstall, FACILITY)
357         self.plugins = pi.get_plugins()