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