Refactor plugin initialization and enablement
[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         if username is None:
183             username = ''
184
185         target = None
186         if self.trans is not None:
187             tid = self.trans.transaction_id
188             target = self.trans.retrieve().get('login_target')
189         if tid is None:
190             tid = ''
191
192         context = {
193             "title": 'Login',
194             "action": '%s/%s' % (self.basepath, self.formpage),
195             "service_name": self.lm.service_name,
196             "username_text": self.lm.username_text,
197             "password_text": self.lm.password_text,
198             "description": self.lm.help_text,
199             "next_url": next_url,
200             "username": username,
201             "login_target": target,
202             "cancel_url": '%s/login/cancel?%s' % (self.basepath,
203                                                   self.trans.get_GET_arg()),
204         }
205         context.update(kwargs)
206         if self.trans is not None:
207             t = self.trans.get_POST_tuple()
208             context.update({t[0]: t[1]})
209
210         return context
211
212
213 FACILITY = 'login_config'
214
215
216 class Login(Page):
217
218     def __init__(self, *args, **kwargs):
219         super(Login, self).__init__(*args, **kwargs)
220         self.cancel = Cancel(*args, **kwargs)
221         self.info = Info(self._site)
222
223         plugins = PluginLoader(Login, FACILITY, 'LoginManager')
224         plugins.get_plugin_data()
225         self._site[FACILITY] = plugins
226
227         available = plugins.available.keys()
228         self._debug('Available login managers: %s' % str(available))
229
230         for item in plugins.available:
231             plugin = plugins.available[item]
232             plugin.register(self, self._site)
233
234         for item in plugins.enabled:
235             self._debug('Login plugin in enabled list: %s' % item)
236             if item not in plugins.available:
237                 continue
238             plugins.available[item].enable()
239
240     def add_subtree(self, name, page):
241         self.__dict__[name] = page
242
243     def get_first_login(self):
244         plugin = None
245         plugins = self._site[FACILITY]
246         if plugins.enabled:
247             first = plugins.enabled[0]
248             plugin = plugins.available[first]
249         return plugin
250
251     def root(self, *args, **kwargs):
252         plugin = self.get_first_login()
253         if plugin:
254             trans = self.get_valid_transaction('login', **kwargs)
255             redirect = '%s/login/%s?%s' % (self.basepath,
256                                            plugin.path,
257                                            trans.get_GET_arg())
258             raise cherrypy.HTTPRedirect(redirect)
259         return self._template('login/index.html', title='Login')
260
261
262 class Logout(Page):
263
264     def root(self, *args, **kwargs):
265         UserSession().logout(self.user)
266         return self._template('logout.html', title='Logout')
267
268
269 class Cancel(Page):
270
271     def GET(self, *args, **kwargs):
272
273         session = UserSession()
274         session.logout(None)
275
276         # return to the caller if any
277         transdata = self.get_valid_transaction('login', **kwargs).retrieve()
278         if 'login_return' not in transdata:
279             raise cherrypy.HTTPError(401)
280         raise cherrypy.HTTPRedirect(transdata['login_return'])
281
282     def root(self, *args, **kwargs):
283         op = getattr(self, cherrypy.request.method, self.GET)
284         if callable(op):
285             return op(*args, **kwargs)
286
287
288 class LoginMgrsInstall(object):
289
290     def __init__(self):
291         pi = PluginInstaller(LoginMgrsInstall, FACILITY)
292         self.plugins = pi.get_plugins()