[IMP] auth_openid: forward port of patches made on 6.1 branch
[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 urllib
26
27 import werkzeug.urls
28 import werkzeug.exceptions
29
30 from openerp.modules.registry import RegistryManager
31 try:
32     import openerp.addons.web.common.http as openerpweb
33 except ImportError:
34     import web.common.http as openerpweb    # noqa
35
36 from openid import oidutil
37 from openid.store import filestore
38 from openid.consumer import consumer
39 from openid.cryptutil import randomString
40 from openid.extensions import ax, sreg
41
42 from .. import utils
43
44 _logger = logging.getLogger(__name__)
45 oidutil.log = _logger.debug
46
47 _storedir = os.path.join(tempfile.gettempdir(), 'openerp-auth_openid-store')
48
49 class GoogleAppsAwareConsumer(consumer.GenericConsumer):
50     def complete(self, message, endpoint, return_to):
51         if message.getOpenIDNamespace() == consumer.OPENID2_NS:
52             server_url = message.getArg(consumer.OPENID2_NS, 'op_endpoint', '')
53             if server_url.startswith('https://www.google.com/a/'):
54                 assoc_handle = message.getArg(consumer.OPENID_NS, 'assoc_handle')
55                 assoc = self.store.getAssociation(server_url, assoc_handle)
56                 if assoc:
57                     # update fields
58                     for attr in ['claimed_id', 'identity']:
59                         value = message.getArg(consumer.OPENID2_NS, attr, '')
60                         value = 'https://www.google.com/accounts/o8/user-xrds?uri=%s' % urllib.quote_plus(value)
61                         message.setArg(consumer.OPENID2_NS, attr, value)
62
63                     # now, resign the message
64                     message.delArg(consumer.OPENID2_NS, 'sig')
65                     message.delArg(consumer.OPENID2_NS, 'signed')
66                     message = assoc.signMessage(message)
67
68         return super(GoogleAppsAwareConsumer, self).complete(message, endpoint, return_to)
69
70
71 class OpenIDController(openerpweb.Controller):
72     _cp_path = '/auth_openid/login'
73
74     _store = filestore.FileOpenIDStore(_storedir)
75
76     _REQUIRED_ATTRIBUTES = ['email']
77     _OPTIONAL_ATTRIBUTES = 'nickname fullname postcode country language timezone'.split()
78
79     def _add_extensions(self, request):
80         """Add extensions to the request"""
81
82         sreg_request = sreg.SRegRequest(required=self._REQUIRED_ATTRIBUTES,
83                                         optional=self._OPTIONAL_ATTRIBUTES)
84         request.addExtension(sreg_request)
85
86         ax_request = ax.FetchRequest()
87         for alias in self._REQUIRED_ATTRIBUTES:
88             uri = utils.SREG2AX[alias]
89             ax_request.add(ax.AttrInfo(uri, required=True, alias=alias))
90         for alias in self._OPTIONAL_ATTRIBUTES:
91             uri = utils.SREG2AX[alias]
92             ax_request.add(ax.AttrInfo(uri, required=False, alias=alias))
93
94         request.addExtension(ax_request)
95
96     def _get_attributes_from_success_response(self, success_response):
97         attrs = {}
98
99         all_attrs = self._REQUIRED_ATTRIBUTES + self._OPTIONAL_ATTRIBUTES
100
101         sreg_resp = sreg.SRegResponse.fromSuccessResponse(success_response)
102         if sreg_resp:
103             for attr in all_attrs:
104                 value = sreg_resp.get(attr)
105                 if value is not None:
106                     attrs[attr] = value
107
108         ax_resp = ax.FetchResponse.fromSuccessResponse(success_response)
109         if ax_resp:
110             for attr in all_attrs:
111                 value = ax_resp.getSingle(utils.SREG2AX[attr])
112                 if value is not None:
113                     attrs[attr] = value
114         return attrs
115
116     def _get_realm(self, req):
117         return req.httprequest.host_url
118
119     @openerpweb.httprequest
120     def verify_direct(self, req, db, url):
121         result = self._verify(req, db, url)
122         if 'error' in result:
123             return werkzeug.exceptions.BadRequest(result['error'])
124         if result['action'] == 'redirect':
125             return werkzeug.utils.redirect(result['value'])
126         return result['value']
127
128     @openerpweb.jsonrequest
129     def verify(self, req, db, url):
130         return self._verify(req, db, url)
131
132     def _verify(self, req, db, url):
133         redirect_to = werkzeug.urls.Href(req.httprequest.host_url + 'auth_openid/login/process')(session_id=req.session_id)
134         realm = self._get_realm(req)
135
136         session = dict(dbname=db, openid_url=url)       # TODO add origin page ?
137         oidconsumer = consumer.Consumer(session, self._store)
138
139         try:
140             request = oidconsumer.begin(url)
141         except consumer.DiscoveryFailure, exc:
142             fetch_error_string = 'Error in discovery: %s' % (str(exc[0]),)
143             return {'error': fetch_error_string, 'title': 'OpenID Error'}
144
145         if request is None:
146             return {'error': 'No OpenID services found', 'title': 'OpenID Error'}
147
148         req.session.openid_session = session
149         self._add_extensions(request)
150
151         if request.shouldSendRedirect():
152             redirect_url = request.redirectURL(realm, redirect_to)
153             return {'action': 'redirect', 'value': redirect_url, 'session_id': req.session_id}
154         else:
155             form_html = request.htmlMarkup(realm, redirect_to)
156             return {'action': 'post', 'value': form_html, 'session_id': req.session_id}
157
158     @openerpweb.httprequest
159     def process(self, req, **kw):
160         session = getattr(req.session, 'openid_session', None)
161         if not session:
162             return werkzeug.utils.redirect('/')
163
164         oidconsumer = consumer.Consumer(session, self._store, consumer_class=GoogleAppsAwareConsumer)
165
166         query = req.httprequest.args
167         info = oidconsumer.complete(query, req.httprequest.base_url)
168         display_identifier = info.getDisplayIdentifier()
169
170         session['status'] = info.status
171         user_id = None
172
173         if info.status == consumer.SUCCESS:
174             dbname = session['dbname']
175             with utils.cursor(dbname) as cr:
176                 registry = RegistryManager.get(dbname)
177                 Modules = registry.get('ir.module.module')
178
179                 installed = Modules.search_count(cr, 1, ['&', ('name', '=', 'auth_openid'), ('state', '=', 'installed')]) == 1
180                 if installed:
181
182                     Users = registry.get('res.users')
183
184                     #openid_url = info.endpoint.canonicalID or display_identifier
185                     openid_url = session['openid_url']
186
187                     attrs = self._get_attributes_from_success_response(info)
188                     attrs['openid_url'] = openid_url
189                     session['attributes'] = attrs
190                     openid_email = attrs.get('email', False)
191
192                     domain = []
193                     if openid_email:
194                         domain += ['|', ('openid_email', '=', False)]
195                     domain += [('openid_email', '=', openid_email)]
196
197                     domain += [('openid_url', '=', openid_url), ('active', '=', True)]
198
199                     ids = Users.search(cr, 1, domain)
200                     assert len(ids) < 2
201                     if ids:
202                         user_id = ids[0]
203                         login = Users.browse(cr, 1, user_id).login
204                         key = randomString(utils.KEY_LENGTH, '0123456789abcdef')
205                         Users.write(cr, 1, [user_id], {'openid_key': key})
206                         # TODO fill empty fields with the ones from sreg/ax
207                         cr.commit()
208
209                         req.session.authenticate(dbname, login, key, {})
210
211             if not user_id:
212                 session['message'] = 'This OpenID identifier is not associated to any active users'
213
214         elif info.status == consumer.SETUP_NEEDED:
215             session['message'] = info.setup_url
216         elif info.status == consumer.FAILURE and display_identifier:
217             fmt = "Verification of %s failed: %s"
218             session['message'] = fmt % (display_identifier, info.message)
219         else:   # FAILURE
220             # Either we don't understand the code or there is no
221             # openid_url included with the error. Give a generic
222             # failure message. The library should supply debug
223             # information in a log.
224             session['message'] = 'Verification failed.'
225
226         fragment = '#loginerror' if not user_id else ''
227         return werkzeug.utils.redirect('/' + fragment)
228
229     @openerpweb.jsonrequest
230     def status(self, req):
231         session = getattr(req.session, 'openid_session', {})
232         return {'status': session.get('status'), 'message': session.get('message')}
233
234
235 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: