1 # -*- coding: utf-8 -*-
2 ##############################################################################
4 # OpenERP, Open Source Management Solution
5 # Copyright (C) 2010-2012 OpenERP s.a. (<http://openerp.com>).
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.
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.
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/>.
20 ##############################################################################
29 import werkzeug.exceptions
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
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
44 _logger = logging.getLogger(__name__)
45 oidutil.log = _logger.debug
47 def get_system_user():
48 """Return system user info string, such as USERNAME-EUID"""
50 info = getpass.getuser()
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.
57 info = win32api.GetUserName()
61 euid = getattr(os, 'geteuid', None) # Non available on some platforms
63 info = '%s-%d' % (info, euid())
66 _storedir = os.path.join(tempfile.gettempdir(),
67 'openerp-auth_openid-%s-store' % get_system_user())
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)
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)
83 # now, resign the message
84 message.delArg(consumer.OPENID2_NS, 'sig')
85 message.delArg(consumer.OPENID2_NS, 'signed')
86 message = assoc.signMessage(message)
88 return super(GoogleAppsAwareConsumer, self).complete(message, endpoint, return_to)
91 class OpenIDController(openerp.addons.web.http.Controller):
92 _cp_path = '/auth_openid/login'
94 _store = filestore.FileOpenIDStore(_storedir)
96 _REQUIRED_ATTRIBUTES = ['email']
97 _OPTIONAL_ATTRIBUTES = 'nickname fullname postcode country language timezone'.split()
99 def _add_extensions(self, request):
100 """Add extensions to the request"""
102 sreg_request = sreg.SRegRequest(required=self._REQUIRED_ATTRIBUTES,
103 optional=self._OPTIONAL_ATTRIBUTES)
104 request.addExtension(sreg_request)
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))
114 request.addExtension(ax_request)
116 def _get_attributes_from_success_response(self, success_response):
119 all_attrs = self._REQUIRED_ATTRIBUTES + self._OPTIONAL_ATTRIBUTES
121 sreg_resp = sreg.SRegResponse.fromSuccessResponse(success_response)
123 for attr in all_attrs:
124 value = sreg_resp.get(attr)
125 if value is not None:
128 ax_resp = ax.FetchResponse.fromSuccessResponse(success_response)
130 for attr in all_attrs:
131 value = ax_resp.getSingle(utils.SREG2AX[attr])
132 if value is not None:
136 def _get_realm(self, req):
137 return req.httprequest.host_url
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']
148 @openerp.addons.web.http.jsonrequest
149 def verify(self, req, db, url):
150 return self._verify(req, db, url)
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)
156 session = dict(dbname=db, openid_url=url) # TODO add origin page ?
157 oidconsumer = consumer.Consumer(session, self._store)
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'}
166 return {'error': 'No OpenID services found', 'title': 'OpenID Error'}
168 req.session.openid_session = session
169 self._add_extensions(request)
171 if request.shouldSendRedirect():
172 redirect_url = request.redirectURL(realm, redirect_to)
173 return {'action': 'redirect', 'value': redirect_url, 'session_id': req.session_id}
175 form_html = request.htmlMarkup(realm, redirect_to)
176 return {'action': 'post', 'value': form_html, 'session_id': req.session_id}
178 @openerp.addons.web.http.httprequest
179 def process(self, req, **kw):
180 session = getattr(req.session, 'openid_session', None)
182 return set_cookie_and_redirect(req, '/')
184 oidconsumer = consumer.Consumer(session, self._store, consumer_class=GoogleAppsAwareConsumer)
186 query = req.httprequest.args
187 info = oidconsumer.complete(query, req.httprequest.base_url)
188 display_identifier = info.getDisplayIdentifier()
190 session['status'] = info.status
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')
198 installed = Modules.search_count(cr, SUPERUSER_ID, ['&', ('name', '=', 'auth_openid'), ('state', '=', 'installed')]) == 1
201 Users = registry.get('res.users')
203 #openid_url = info.endpoint.canonicalID or display_identifier
204 openid_url = session['openid_url']
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)
213 domain += ['|', ('openid_email', '=', False)]
214 domain += [('openid_email', '=', openid_email)]
216 domain += [('openid_url', '=', openid_url), ('active', '=', True)]
218 ids = Users.search(cr, SUPERUSER_ID, domain)
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
228 return login_and_redirect(req, dbname, login, key)
230 session['message'] = 'This OpenID identifier is not associated to any active users'
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)
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.'
244 return set_cookie_and_redirect(req, '/#action=login&loginerror=1')
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')}
252 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: