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