# -*- coding: utf-8 -*- ############################################################################## # # Copyright (c) 2012 Nexedi SA and Contributors. All Rights Reserved. # # WARNING: This program as such is intended to be used by professional # programmers who take the whole responsibility of assessing all potential # consequences resulting from its eventual inadequacies and bugs # End users who are looking for a ready-to-use solution with commercial # guarantees and support are strongly advised to contract a Free Software # Service Company # # This program is Free Software; you can redistribute it and/or # modify it under the terms of the GNU General Public License # as published by the Free Software Foundation; either version 2 # of the License, or (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. # ############################################################################## from Products.ERP5Type.Globals import InitializeClass from AccessControl import ClassSecurityInfo from Products.PageTemplates.PageTemplateFile import PageTemplateFile from Products.PluggableAuthService.interfaces import plugins from Products.PluggableAuthService.utils import classImplements from Products.PluggableAuthService.plugins.BasePlugin import BasePlugin from Products.ERP5Security import _setUserNameForAccessLog from AccessControl.SecurityManagement import getSecurityManager, \ setSecurityManager, newSecurityManager from Products.ERP5Type.Cache import DEFAULT_CACHE_SCOPE import time from six.moves import urllib import json from zLOG import LOG, ERROR, INFO try: import facebook except ImportError: facebook = None try: import apiclient.discovery import httplib2 import oauth2client.client except ImportError: httplib2 = None #Form for new plugin in ZMI manage_addERP5FacebookExtractionPluginForm = PageTemplateFile( 'www/ERP5Security_addERP5FacebookExtractionPlugin', globals(), __name__='manage_addERP5FacebookExtractionPluginForm') def getGoogleUserEntry(token): if httplib2 is None: LOG('ERP5GoogleExtractionPlugin', INFO, 'No Google modules available, please install google-api-python-client ' 'package. Authentication disabled..') return None http = oauth2client.client.AccessTokenCredentials(token, 'ERP5 Client' ).authorize(httplib2.Http(timeout=5)) service = apiclient.discovery.build("oauth2", "v1", http=http) google_entry = service.userinfo().get().execute() user_entry = {} if google_entry is not None: # sanitise value for k in (('first_name', 'given_name'), ('last_name', 'family_name'), ('email', 'email'), ('reference', 'email'),): value = google_entry.get(k[1], '').encode('utf-8') user_entry[k[0]] = value return user_entry def addERP5FacebookExtractionPlugin(dispatcher, id, title=None, REQUEST=None): """ Add a ERP5FacebookExtractionPlugin to a Pluggable Auth Service. """ plugin = ERP5FacebookExtractionPlugin(id, title) dispatcher._setObject(plugin.getId(), plugin) if REQUEST is not None: REQUEST['RESPONSE'].redirect( '%s/manage_workspace' '?manage_tabs_message=' 'ERP5FacebookExtractionPlugin+added.' % dispatcher.absolute_url()) #Form for new plugin in ZMI manage_addERP5GoogleExtractionPluginForm = PageTemplateFile( 'www/ERP5Security_addERP5GoogleExtractionPlugin', globals(), __name__='manage_addERP5GoogleExtractionPluginForm') def addERP5GoogleExtractionPlugin(dispatcher, id, title=None, REQUEST=None): """ Add a ERP5GoogleExtractionPlugin to a Pluggable Auth Service. """ plugin = ERP5GoogleExtractionPlugin(id, title) dispatcher._setObject(plugin.getId(), plugin) if REQUEST is not None: REQUEST['RESPONSE'].redirect( '%s/manage_workspace' '?manage_tabs_message=' 'ERP5GoogleExtractionPlugin+added.' % dispatcher.absolute_url()) class ERP5ExternalOauth2ExtractionPlugin: cache_factory_name = 'external_oauth2_token_cache_factory' security = ClassSecurityInfo() def __init__(self, id, title=None): #Register value self._setId(id) self.title = title ##################### # memcached helpers # ##################### def _getCacheFactory(self): portal = self.getPortalObject() cache_tool = portal.portal_caches cache_factory = cache_tool.getRamCacheRoot().get(self.cache_factory_name) #XXX This conditional statement should be remove as soon as #Broadcasting will be enable among all zeo clients. #Interaction which update portal_caches should interact with all nodes. if cache_factory is None \ and getattr(cache_tool, self.cache_factory_name, None) is not None: #ram_cache_root is not up to date for current node cache_tool.updateCache() cache_factory = cache_tool.getRamCacheRoot().get(self.cache_factory_name) if cache_factory is None: raise KeyError("Cache factory %s not found" % self.cache_factory_name) return cache_factory def setToken(self, key, body): cache_factory = self._getCacheFactory() cache_duration = cache_factory.cache_duration for cache_plugin in cache_factory.getCachePluginList(): cache_plugin.set(key, DEFAULT_CACHE_SCOPE, body, cache_duration=cache_duration) def getToken(self, key): cache_factory = self._getCacheFactory() for cache_plugin in cache_factory.getCachePluginList(): cache_entry = cache_plugin.get(key, DEFAULT_CACHE_SCOPE) if cache_entry is not None: # Avoid errors if the plugin don't have the funcionality of refresh token refreshTokenIfExpired = getattr(self, "refreshTokenIfExpired", None) cache_value = cache_entry.getValue() if refreshTokenIfExpired is not None: return refreshTokenIfExpired(key, cache_value) else: return cache_value raise KeyError('Key %r not found' % key) #################################### #ILoginPasswordHostExtractionPlugin# #################################### security.declarePrivate('extractCredentials') def extractCredentials(self, request): """ Extract Oauth2 credentials from the request header. """ user_dict = {} cookie_hash = request.get(self.cookie_name) if cookie_hash is not None: try: user_dict = self.getToken(cookie_hash) except KeyError: LOG(self.getId(), INFO, 'Hash %s not found' % cookie_hash) return {} token = None if "access_token" in user_dict: token = user_dict["access_token"] if token is None: # no token, then no credentials return {} user_entry = None try: user_entry = self.getToken(token) except KeyError: user_entry = self.getUserEntry(token) if user_entry is not None: # Reduce data size because, we don't need more than reference user_entry = {"reference": user_entry["reference"]} if user_entry is None: # no user, then no credentials return {} try: # Every request will update cache to postpone the cache expiration # to keep the user logged in self.setToken(token, user_entry) except KeyError as error: # allow to work w/o cache LOG(self.getId(), INFO, error) pass # Credentials returned here will be used by ERP5LoginUserManager to find the login document # having reference `user`. creds = { "login_portal_type": self.login_portal_type, "external_login": user_entry["reference"] } # PAS wants remote_host / remote_address creds['remote_host'] = request.get('REMOTE_HOST', '') try: creds['remote_address'] = request.getClientAddr() except AttributeError: creds['remote_address'] = request.get('REMOTE_ADDR', '') _setUserNameForAccessLog('%s=%s' % (self.getId(), creds['external_login']) , request) return creds def getFacebookUserEntry(token): if facebook is None: LOG('ERP5FacebookExtractionPlugin', INFO, 'No facebook module, install facebook-sdk package. ' 'Authentication disabled.') return None args = {'fields' : 'id,name,email', } facebook_entry = facebook.GraphAPI(token, timeout=5).get_object("me", **args) user_entry = {} if facebook_entry is not None: # sanitise value for k in ('name', 'id'): try: if k == 'id': user_entry['reference'] = facebook_entry[k].encode('utf-8') else: user_entry[k] = facebook_entry[k].encode('utf-8') except KeyError: raise ValueError(facebook_entry) return user_entry class ERP5FacebookExtractionPlugin(ERP5ExternalOauth2ExtractionPlugin, BasePlugin): """ Plugin to authenicate as machines. """ meta_type = "ERP5 Facebook Extraction Plugin" login_portal_type = "Facebook Login" cookie_name = "__ac_facebook_hash" cache_factory_name = "facebook_server_auth_token_cache_factory" def refreshTokenIfExpired(self, key, cache_value): return cache_value def getUserEntry(self, token): return getFacebookUserDict(token) class ERP5GoogleExtractionPlugin(ERP5ExternalOauth2ExtractionPlugin, BasePlugin): """ Plugin to authenicate as machines. """ meta_type = "ERP5 Google Extraction Plugin" login_portal_type = "Google Login" cookie_name = "__ac_google_hash" cache_factory_name = "google_server_auth_token_cache_factory" def refreshTokenIfExpired(self, key, cache_value): expires_in = cache_value.get("token_response", {}).get("expires_in") refresh_token = cache_value.get("refresh_token") if expires_in and refresh_token: if (time.time() - cache_value["response_timestamp"]) >= float(expires_in): credential = oauth2client.client.OAuth2Credentials( cache_value["access_token"], cache_value["client_id"], cache_value["client_secret"], refresh_token, cache_value["token_expiry"], cache_value["token_uri"], cache_value["user_agent"]) credential.refresh(httplib2.Http(timeout=5)) cache_value = json.loads(credential.to_json()) cache_value["response_timestamp"] = time.time() self.setToken(key, cache_value) return cache_value def getUserEntry(self, token): return getGoogleUserEntry(token) #List implementation of class classImplements( ERP5FacebookExtractionPlugin, plugins.ILoginPasswordHostExtractionPlugin ) InitializeClass(ERP5FacebookExtractionPlugin) classImplements( ERP5GoogleExtractionPlugin, plugins.ILoginPasswordHostExtractionPlugin ) InitializeClass(ERP5GoogleExtractionPlugin)