[FIX] account_followup: merged the branch of jco fixing the account_followup module...
[odoo/odoo.git] / addons / auth_openid / controllers / main.py
1 # -*- coding: utf-8 -*-
2 ##############################################################################
3 #
4 #    OpenERP, Open Source Management Solution
5 #    Copyright (C) 2010-2012 OpenERP s.a. (<http://openerp.com>).
6 #
7 #    This program is free software: you can redistribute it and/or modify
8 #    it under the terms of the GNU Affero General Public License as
9 #    published by the Free Software Foundation, either version 3 of the
10 #    License, or (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 Affero General Public License for more details.
16 #
17 #    You should have received a copy of the GNU Affero General Public License
18 #    along with this program.  If not, see <http://www.gnu.org/licenses/>.
19 #
20 ##############################################################################
21
22 import logging
23 import os
24 import tempfile
25 import getpass
26 import urllib
27
28 import werkzeug.urls
29 import werkzeug.exceptions
30
31 from openid import oidutil
32 from openid.store import filestore
33 from openid.consumer import consumer
34 from openid.cryptutil import randomString
35 from openid.extensions import ax, sreg
36
37 import openerp
38 from openerp import SUPERUSER_ID
39 from openerp.modules.registry import RegistryManager
40 from openerp.addons.web.controllers.main import login_and_redirect, set_cookie_and_redirect
41
42 from .. import utils
43
44 _logger = logging.getLogger(__name__)
45 oidutil.log = _logger.debug
46
47 def get_system_user():
48     """Return system user info string, such as USERNAME-EUID"""
49     try:
50         info = getpass.getuser()
51     except ImportError:
52         if os.name == 'nt':
53             # when there is no 'USERNAME' in environment, getpass.getuser()
54             # fail when trying to import 'pwd' module - which is unix only.
55             # In that case we have to fallback to real win32 API.
56             import win32api
57             info = win32api.GetUserName()
58         else:
59             raise
60
61     euid = getattr(os, 'geteuid', None) # Non available on some platforms
62     if euid is not None:
63         info = '%s-%d' % (info, euid())
64     return info
65
66 _storedir = os.path.join(tempfile.gettempdir(), 
67                          'openerp-auth_openid-%s-store' % get_system_user())
68
69 class GoogleAppsAwareConsumer(consumer.GenericConsumer):
70     def complete(self, message, endpoint, return_to):
71         if message.getOpenIDNamespace() == consumer.OPENID2_NS:
72             server_url = message.getArg(consumer.OPENID2_NS, 'op_endpoint', '')
73             if server_url.startswith('https://www.google.com/a/'):
74                 assoc_handle = message.getArg(consumer.OPENID_NS, 'assoc_handle')
75                 assoc = self.store.getAssociation(server_url, assoc_handle)
76                 if assoc:
77                     # update fields
78                     for attr in ['claimed_id', 'identity']:
79                         value = message.getArg(consumer.OPENID2_NS, attr, '')
80                         value = 'https://www.google.com/accounts/o8/user-xrds?uri=%s' % urllib.quote_plus(value)
81                         message.setArg(consumer.OPENID2_NS, attr, value)
82
83                     # now, resign the message
84                     message.delArg(consumer.OPENID2_NS, 'sig')
85                     message.delArg(consumer.OPENID2_NS, 'signed')
86                     message = assoc.signMessage(message)
87
88         return super(GoogleAppsAwareConsumer, self).complete(message, endpoint, return_to)
89
90
91 class OpenIDController(openerp.addons.web.http.Controller):
92     _cp_path = '/auth_openid/login'
93
94     _store = filestore.FileOpenIDStore(_storedir)
95
96     _REQUIRED_ATTRIBUTES = ['email']
97     _OPTIONAL_ATTRIBUTES = 'nickname fullname postcode country language timezone'.split()
98
99     def _add_extensions(self, request):
100         """Add extensions to the request"""
101
102         sreg_request = sreg.SRegRequest(required=self._REQUIRED_ATTRIBUTES,
103                                         optional=self._OPTIONAL_ATTRIBUTES)
104         request.addExtension(sreg_request)
105
106         ax_request = ax.FetchRequest()
107         for alias in self._REQUIRED_ATTRIBUTES:
108             uri = utils.SREG2AX[alias]
109             ax_request.add(ax.AttrInfo(uri, required=True, alias=alias))
110         for alias in self._OPTIONAL_ATTRIBUTES:
111             uri = utils.SREG2AX[alias]
112             ax_request.add(ax.AttrInfo(uri, required=False, alias=alias))
113
114         request.addExtension(ax_request)
115
116     def _get_attributes_from_success_response(self, success_response):
117         attrs = {}
118
119         all_attrs = self._REQUIRED_ATTRIBUTES + self._OPTIONAL_ATTRIBUTES
120
121         sreg_resp = sreg.SRegResponse.fromSuccessResponse(success_response)
122         if sreg_resp:
123             for attr in all_attrs:
124                 value = sreg_resp.get(attr)
125                 if value is not None:
126                     attrs[attr] = value
127
128         ax_resp = ax.FetchResponse.fromSuccessResponse(success_response)
129         if ax_resp:
130             for attr in all_attrs:
131                 value = ax_resp.getSingle(utils.SREG2AX[attr])
132                 if value is not None:
133                     attrs[attr] = value
134         return attrs
135
136     def _get_realm(self, req):
137         return req.httprequest.host_url
138
139     @openerp.addons.web.http.httprequest
140     def verify_direct(self, req, db, url):
141         result = self._verify(req, db, url)
142         if 'error' in result:
143             return werkzeug.exceptions.BadRequest(result['error'])
144         if result['action'] == 'redirect':
145             return werkzeug.utils.redirect(result['value'])
146         return result['value']
147
148     @openerp.addons.web.http.jsonrequest
149     def verify(self, req, db, url):
150         return self._verify(req, db, url)
151
152     def _verify(self, req, db, url):
153         redirect_to = werkzeug.urls.Href(req.httprequest.host_url + 'auth_openid/login/process')(session_id=req.session_id)
154         realm = self._get_realm(req)
155
156         session = dict(dbname=db, openid_url=url)       # TODO add origin page ?
157         oidconsumer = consumer.Consumer(session, self._store)
158
159         try:
160             request = oidconsumer.begin(url)
161         except consumer.DiscoveryFailure, exc:
162             fetch_error_string = 'Error in discovery: %s' % (str(exc[0]),)
163             return {'error': fetch_error_string, 'title': 'OpenID Error'}
164
165         if request is None:
166             return {'error': 'No OpenID services found', 'title': 'OpenID Error'}
167
168         req.session.openid_session = session
169         self._add_extensions(request)
170
171         if request.shouldSendRedirect():
172             redirect_url = request.redirectURL(realm, redirect_to)
173             return {'action': 'redirect', 'value': redirect_url, 'session_id': req.session_id}
174         else:
175             form_html = request.htmlMarkup(realm, redirect_to)
176             return {'action': 'post', 'value': form_html, 'session_id': req.session_id}
177
178     @openerp.addons.web.http.httprequest
179     def process(self, req, **kw):
180         session = getattr(req.session, 'openid_session', None)
181         if not session:
182             return set_cookie_and_redirect(req, '/')
183
184         oidconsumer = consumer.Consumer(session, self._store, consumer_class=GoogleAppsAwareConsumer)
185
186         query = req.httprequest.args
187         info = oidconsumer.complete(query, req.httprequest.base_url)
188         display_identifier = info.getDisplayIdentifier()
189
190         session['status'] = info.status
191
192         if info.status == consumer.SUCCESS:
193             dbname = session['dbname']
194             registry = RegistryManager.get(dbname)
195             with registry.cursor() as cr:
196                 Modules = registry.get('ir.module.module')
197
198                 installed = Modules.search_count(cr, SUPERUSER_ID, ['&', ('name', '=', 'auth_openid'), ('state', '=', 'installed')]) == 1
199                 if installed:
200
201                     Users = registry.get('res.users')
202
203                     #openid_url = info.endpoint.canonicalID or display_identifier
204                     openid_url = session['openid_url']
205
206                     attrs = self._get_attributes_from_success_response(info)
207                     attrs['openid_url'] = openid_url
208                     session['attributes'] = attrs
209                     openid_email = attrs.get('email', False)
210
211                     domain = []
212                     if openid_email:
213                         domain += ['|', ('openid_email', '=', False)]
214                     domain += [('openid_email', '=', openid_email)]
215
216                     domain += [('openid_url', '=', openid_url), ('active', '=', True)]
217
218                     ids = Users.search(cr, SUPERUSER_ID, domain)
219                     assert len(ids) < 2
220                     if ids:
221                         user_id = ids[0]
222                         login = Users.browse(cr, SUPERUSER_ID, user_id).login
223                         key = randomString(utils.KEY_LENGTH, '0123456789abcdef')
224                         Users.write(cr, SUPERUSER_ID, [user_id], {'openid_key': key})
225                         # TODO fill empty fields with the ones from sreg/ax
226                         cr.commit()
227
228                         return login_and_redirect(req, dbname, login, key)
229
230             session['message'] = 'This OpenID identifier is not associated to any active users'
231
232         elif info.status == consumer.SETUP_NEEDED:
233             session['message'] = info.setup_url
234         elif info.status == consumer.FAILURE and display_identifier:
235             fmt = "Verification of %s failed: %s"
236             session['message'] = fmt % (display_identifier, info.message)
237         else:   # FAILURE
238             # Either we don't understand the code or there is no
239             # openid_url included with the error. Give a generic
240             # failure message. The library should supply debug
241             # information in a log.
242             session['message'] = 'Verification failed.'
243
244         return set_cookie_and_redirect(req, '/#action=login&loginerror=1')
245
246     @openerp.addons.web.http.jsonrequest
247     def status(self, req):
248         session = getattr(req.session, 'openid_session', {})
249         return {'status': session.get('status'), 'message': session.get('message')}
250
251
252 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: