ce921c5196a94e32e744d5961a844f023470f85a
[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.page import Page
21 from ipsilon.util.user import UserSession
22 from ipsilon.util.plugin import PluginInstaller, PluginLoader
23 from ipsilon.util.plugin import PluginObject, PluginConfig
24 from ipsilon.info.common import Info
25 from ipsilon.util.cookies import SecureCookie
26 import cherrypy
27
28
29 USERNAME_COOKIE = 'ipsilon_default_username'
30
31
32 class LoginManagerBase(PluginConfig, PluginObject):
33
34     def __init__(self, *args):
35         PluginConfig.__init__(self)
36         PluginObject.__init__(self, *args)
37         self._root = None
38         self._site = None
39         self.path = '/'
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         next_login = self.next_login()
97         if next_login:
98             return self.redirect_to_path(next_login.path)
99
100         # return to the caller if any
101         session = UserSession()
102
103         transdata = trans.retrieve()
104
105         # on direct login the UI (ie not redirected by a provider) we ned to
106         # remove the transaction cookie as it won't be needed anymore
107         if trans.provider == 'login':
108             trans.wipe()
109
110         # destroy session and return error
111         if 'login_return' not in transdata:
112             session.logout(None)
113             raise cherrypy.HTTPError(401)
114
115         raise cherrypy.HTTPRedirect(transdata['login_return'])
116
117     def get_tree(self, site):
118         raise NotImplementedError
119
120     def register(self, root, site):
121         self._root = root
122         self._site = site
123
124     def next_login(self):
125         plugins = self._site[FACILITY]
126         try:
127             idx = plugins.enabled.index(self.name)
128             item = plugins.enabled[idx + 1]
129             return plugins.available[item]
130         except (ValueError, IndexError):
131             return None
132
133     def on_enable(self):
134
135         # and add self to the root
136         self._root.add_subtree(self.name, self.get_tree(self._site))
137
138         # Get handle of the info plugin
139         self.info = self._root.info
140
141
142 class LoginPageBase(Page):
143
144     def __init__(self, site, mgr):
145         super(LoginPageBase, self).__init__(site)
146         self.lm = mgr
147         self._Transaction = None
148
149     def root(self, *args, **kwargs):
150         raise cherrypy.HTTPError(500)
151
152
153 class LoginFormBase(LoginPageBase):
154
155     def __init__(self, site, mgr, page, template=None):
156         super(LoginFormBase, self).__init__(site, mgr)
157         self.formpage = page
158         self.formtemplate = template or 'login/form.html'
159         self.trans = None
160
161     def GET(self, *args, **kwargs):
162         context = self.create_tmpl_context()
163         # pylint: disable=star-args
164         return self._template(self.formtemplate, **context)
165
166     def root(self, *args, **kwargs):
167         self.trans = self.get_valid_transaction('login', **kwargs)
168         op = getattr(self, cherrypy.request.method, self.GET)
169         if callable(op):
170             return op(*args, **kwargs)
171
172     def create_tmpl_context(self, **kwargs):
173         next_url = None
174         next_login = self.lm.next_login()
175         if next_login:
176             next_url = '%s?%s' % (next_login.path,
177                                   self.trans.get_GET_arg())
178
179         cookie = SecureCookie(USERNAME_COOKIE)
180         cookie.receive()
181         username = cookie.value
182
183         target = None
184         if self.trans is not None:
185             tid = self.trans.transaction_id
186             target = self.trans.retrieve().get('login_target')
187             username = self.trans.retrieve().get('login_username')
188         if tid is None:
189             tid = ''
190
191         if username is None:
192             username = ''
193
194         context = {
195             "title": 'Login',
196             "action": '%s/%s' % (self.basepath, self.formpage),
197             "service_name": self.lm.service_name,
198             "username_text": self.lm.username_text,
199             "password_text": self.lm.password_text,
200             "description": self.lm.help_text,
201             "next_url": next_url,
202             "username": username,
203             "login_target": target,
204             "cancel_url": '%s/login/cancel?%s' % (self.basepath,
205                                                   self.trans.get_GET_arg()),
206         }
207         context.update(kwargs)
208         if self.trans is not None:
209             t = self.trans.get_POST_tuple()
210             context.update({t[0]: t[1]})
211
212         return context
213
214
215 FACILITY = 'login_config'
216
217
218 class Login(Page):
219
220     def __init__(self, *args, **kwargs):
221         super(Login, self).__init__(*args, **kwargs)
222         self.cancel = Cancel(*args, **kwargs)
223         self.info = Info(self._site)
224
225         plugins = PluginLoader(Login, FACILITY, 'LoginManager')
226         plugins.get_plugin_data()
227         self._site[FACILITY] = plugins
228
229         available = plugins.available.keys()
230         self._debug('Available login managers: %s' % str(available))
231
232         for item in plugins.available:
233             plugin = plugins.available[item]
234             plugin.register(self, self._site)
235
236         for item in plugins.enabled:
237             self._debug('Login plugin in enabled list: %s' % item)
238             if item not in plugins.available:
239                 continue
240             plugins.available[item].enable()
241
242     def add_subtree(self, name, page):
243         self.__dict__[name] = page
244
245     def get_first_login(self):
246         plugin = None
247         plugins = self._site[FACILITY]
248         if plugins.enabled:
249             first = plugins.enabled[0]
250             plugin = plugins.available[first]
251         return plugin
252
253     def root(self, *args, **kwargs):
254         plugin = self.get_first_login()
255         if plugin:
256             trans = self.get_valid_transaction('login', **kwargs)
257             redirect = '%s/login/%s?%s' % (self.basepath,
258                                            plugin.path,
259                                            trans.get_GET_arg())
260             raise cherrypy.HTTPRedirect(redirect)
261         return self._template('login/index.html', title='Login')
262
263
264 class Logout(Page):
265
266     def root(self, *args, **kwargs):
267         UserSession().logout(self.user)
268         return self._template('logout.html', title='Logout')
269
270
271 class Cancel(Page):
272
273     def GET(self, *args, **kwargs):
274
275         session = UserSession()
276         session.logout(None)
277
278         # return to the caller if any
279         transdata = self.get_valid_transaction('login', **kwargs).retrieve()
280         if 'login_return' not in transdata:
281             raise cherrypy.HTTPError(401)
282         raise cherrypy.HTTPRedirect(transdata['login_return'])
283
284     def root(self, *args, **kwargs):
285         op = getattr(self, cherrypy.request.method, self.GET)
286         if callable(op):
287             return op(*args, **kwargs)
288
289
290 class LoginMgrsInstall(object):
291
292     def __init__(self):
293         pi = PluginInstaller(LoginMgrsInstall, FACILITY)
294         self.plugins = pi.get_plugins()