Make availble a list of alternative aut methods
[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         # pylint: disable=star-args
189         return self._template(self.formtemplate, **context)
190
191     def root(self, *args, **kwargs):
192         self.trans = self.get_valid_transaction('login', **kwargs)
193         op = getattr(self, cherrypy.request.method, self.GET)
194         if callable(op):
195             return op(*args, **kwargs)
196
197     def create_tmpl_context(self, **kwargs):
198         other_stacks = None
199         other_login_stacks = self.lm.other_login_stacks()
200         if other_login_stacks:
201             other_stacks = list()
202             for ls in other_login_stacks:
203                 url = '%s?%s' % (ls.path, self.trans.get_GET_arg())
204                 name = ls.name
205                 other_stacks.append({'url': url, 'name': name})
206
207         cookie = SecureCookie(USERNAME_COOKIE)
208         cookie.receive()
209         username = cookie.value
210
211         target = None
212         if self.trans is not None:
213             tid = self.trans.transaction_id
214             target = self.trans.retrieve().get('login_target')
215             username = self.trans.retrieve().get('login_username')
216         if tid is None:
217             tid = ''
218
219         if username is None:
220             username = ''
221
222         context = {
223             "title": 'Login',
224             "action": '%s/%s' % (self.basepath, self.formpage),
225             "service_name": self.lm.service_name,
226             "username_text": self.lm.username_text,
227             "password_text": self.lm.password_text,
228             "description": self.lm.help_text,
229             "other_stacks": other_stacks,
230             "username": username,
231             "login_target": target,
232             "cancel_url": '%s/login/cancel?%s' % (self.basepath,
233                                                   self.trans.get_GET_arg()),
234         }
235         context.update(kwargs)
236         if self.trans is not None:
237             t = self.trans.get_POST_tuple()
238             context.update({t[0]: t[1]})
239
240         return context
241
242
243 FACILITY = 'login_config'
244
245
246 class Login(Page):
247
248     def __init__(self, *args, **kwargs):
249         super(Login, self).__init__(*args, **kwargs)
250         self.cancel = Cancel(*args, **kwargs)
251         self.info = Info(self._site)
252
253         plugins = PluginLoader(Login, FACILITY, 'LoginManager')
254         plugins.get_plugin_data()
255         self._site[FACILITY] = plugins
256
257         available = plugins.available.keys()
258         self._debug('Available login managers: %s' % str(available))
259
260         for item in plugins.available:
261             plugin = plugins.available[item]
262             plugin.register(self, self._site)
263
264         for item in plugins.enabled:
265             self._debug('Login plugin in enabled list: %s' % item)
266             if item not in plugins.available:
267                 continue
268             plugins.available[item].enable()
269
270     def add_subtree(self, name, page):
271         self.__dict__[name] = page
272
273     def get_first_login(self):
274         plugin = None
275         plugins = self._site[FACILITY]
276         if plugins.enabled:
277             first = plugins.enabled[0]
278             plugin = plugins.available[first]
279         return plugin
280
281     def root(self, *args, **kwargs):
282         plugin = self.get_first_login()
283         if plugin:
284             trans = self.get_valid_transaction('login', **kwargs)
285             redirect = '%s/login/%s?%s' % (self.basepath,
286                                            plugin.path,
287                                            trans.get_GET_arg())
288             raise cherrypy.HTTPRedirect(redirect)
289         return self._template('login/index.html', title='Login')
290
291
292 class Logout(Page):
293     def __init__(self, *args, **kwargs):
294         super(Logout, self).__init__(*args, **kwargs)
295         self.handlers = {}
296
297     def root(self, *args, **kwargs):
298         us = UserSession()
299
300         for provider in self.handlers:
301             self.debug("Calling logout for provider %s" % provider)
302             obj = self.handlers[provider]
303             obj()
304
305         us.logout(self.user)
306         return self._template('logout.html', title='Logout')
307
308     def add_handler(self, provider, handler):
309         """
310         Providers can register a logout handler here that is called
311         when the IdP logout link is accessed.
312         """
313         self.handlers[provider] = handler
314
315
316 class Cancel(Page):
317
318     def GET(self, *args, **kwargs):
319
320         session = UserSession()
321         session.logout(None)
322
323         # return to the caller if any
324         transdata = self.get_valid_transaction('login', **kwargs).retrieve()
325         if 'login_return' not in transdata:
326             raise cherrypy.HTTPError(401)
327         raise cherrypy.HTTPRedirect(transdata['login_return'])
328
329     def root(self, *args, **kwargs):
330         op = getattr(self, cherrypy.request.method, self.GET)
331         if callable(op):
332             return op(*args, **kwargs)
333
334
335 class LoginManagerInstaller(object):
336     def __init__(self):
337         self.facility = FACILITY
338         self.ptype = 'login'
339         self.name = None
340
341     def unconfigure(self, opts):
342         return
343
344     def install_args(self, group):
345         raise NotImplementedError
346
347     def validate_args(self, args):
348         return
349
350     def configure(self, opts):
351         raise NotImplementedError
352
353
354 class LoginMgrsInstall(object):
355
356     def __init__(self):
357         pi = PluginInstaller(LoginMgrsInstall, FACILITY)
358         self.plugins = pi.get_plugins()