From 37265511172a0afe89a585fd4055e24e25a2fa52 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Nowak?= <luke@nexedi.com> Date: Wed, 4 Jul 2012 15:35:52 +0200 Subject: [PATCH] Implement Facebook based authentication. --- .../facebook_token_cache_factory.xml | 76 ++++++ .../volatile_cache_plugin.xml | 20 ++ .../portal_skins/erp5_credential_facebook.xml | 26 ++ .../Base_createFacebookUser.xml | 67 +++++ bt5/erp5_credential_facebook/bt/change_log | 2 + .../bt/copyright_list | 1 + bt5/erp5_credential_facebook/bt/description | 1 + bt5/erp5_credential_facebook/bt/license | 1 + bt5/erp5_credential_facebook/bt/revision | 1 + .../bt/template_format_version | 1 + .../bt/template_path_list | 2 + .../bt/template_skin_id_list | 1 + bt5/erp5_credential_facebook/bt/title | 1 + bt5/erp5_credential_facebook/bt/version | 1 + .../ERP5FacebookExtractionPlugin.py | 235 ++++++++++++++++++ product/ERP5Security/__init__.py | 11 + ...curity_addERP5FacebookExtractionPlugin.zpt | 36 +++ 17 files changed, 483 insertions(+) create mode 100644 bt5/erp5_credential_facebook/PathTemplateItem/portal_caches/facebook_token_cache_factory.xml create mode 100644 bt5/erp5_credential_facebook/PathTemplateItem/portal_caches/facebook_token_cache_factory/volatile_cache_plugin.xml create mode 100644 bt5/erp5_credential_facebook/SkinTemplateItem/portal_skins/erp5_credential_facebook.xml create mode 100644 bt5/erp5_credential_facebook/SkinTemplateItem/portal_skins/erp5_credential_facebook/Base_createFacebookUser.xml create mode 100644 bt5/erp5_credential_facebook/bt/change_log create mode 100644 bt5/erp5_credential_facebook/bt/copyright_list create mode 100644 bt5/erp5_credential_facebook/bt/description create mode 100644 bt5/erp5_credential_facebook/bt/license create mode 100644 bt5/erp5_credential_facebook/bt/revision create mode 100644 bt5/erp5_credential_facebook/bt/template_format_version create mode 100644 bt5/erp5_credential_facebook/bt/template_path_list create mode 100644 bt5/erp5_credential_facebook/bt/template_skin_id_list create mode 100644 bt5/erp5_credential_facebook/bt/title create mode 100644 bt5/erp5_credential_facebook/bt/version create mode 100644 product/ERP5Security/ERP5FacebookExtractionPlugin.py create mode 100644 product/ERP5Security/www/ERP5Security_addERP5FacebookExtractionPlugin.zpt diff --git a/bt5/erp5_credential_facebook/PathTemplateItem/portal_caches/facebook_token_cache_factory.xml b/bt5/erp5_credential_facebook/PathTemplateItem/portal_caches/facebook_token_cache_factory.xml new file mode 100644 index 0000000000..6a946ca13f --- /dev/null +++ b/bt5/erp5_credential_facebook/PathTemplateItem/portal_caches/facebook_token_cache_factory.xml @@ -0,0 +1,76 @@ +<?xml version="1.0"?> +<ZopeData> + <record id="1" aka="AAAAAAAAAAE="> + <pickle> + <global name="Cache Factory" module="erp5.portal_type"/> + </pickle> + <pickle> + <dictionary> + <item> + <key> <string>_count</string> </key> + <value> + <persistent> <string encoding="base64">AAAAAAAAAAI=</string> </persistent> + </value> + </item> + <item> + <key> <string>_mt_index</string> </key> + <value> + <persistent> <string encoding="base64">AAAAAAAAAAM=</string> </persistent> + </value> + </item> + <item> + <key> <string>_tree</string> </key> + <value> + <persistent> <string encoding="base64">AAAAAAAAAAQ=</string> </persistent> + </value> + </item> + <item> + <key> <string>cache_duration</string> </key> + <value> <int>3600</int> </value> + </item> + <item> + <key> <string>description</string> </key> + <value> + <none/> + </value> + </item> + <item> + <key> <string>id</string> </key> + <value> <string>facebook_token_cache_factory</string> </value> + </item> + <item> + <key> <string>portal_type</string> </key> + <value> <string>Cache Factory</string> </value> + </item> + <item> + <key> <string>title</string> </key> + <value> + <none/> + </value> + </item> + </dictionary> + </pickle> + </record> + <record id="2" aka="AAAAAAAAAAI="> + <pickle> + <global name="Length" module="BTrees.Length"/> + </pickle> + <pickle> <int>0</int> </pickle> + </record> + <record id="3" aka="AAAAAAAAAAM="> + <pickle> + <global name="OOBTree" module="BTrees.OOBTree"/> + </pickle> + <pickle> + <none/> + </pickle> + </record> + <record id="4" aka="AAAAAAAAAAQ="> + <pickle> + <global name="OOBTree" module="BTrees.OOBTree"/> + </pickle> + <pickle> + <none/> + </pickle> + </record> +</ZopeData> diff --git a/bt5/erp5_credential_facebook/PathTemplateItem/portal_caches/facebook_token_cache_factory/volatile_cache_plugin.xml b/bt5/erp5_credential_facebook/PathTemplateItem/portal_caches/facebook_token_cache_factory/volatile_cache_plugin.xml new file mode 100644 index 0000000000..3455556ba0 --- /dev/null +++ b/bt5/erp5_credential_facebook/PathTemplateItem/portal_caches/facebook_token_cache_factory/volatile_cache_plugin.xml @@ -0,0 +1,20 @@ +<?xml version="1.0"?> +<ZopeData> + <record id="1" aka="AAAAAAAAAAE="> + <pickle> + <global name="Ram Cache" module="erp5.portal_type"/> + </pickle> + <pickle> + <dictionary> + <item> + <key> <string>id</string> </key> + <value> <string>volatile_cache_plugin</string> </value> + </item> + <item> + <key> <string>portal_type</string> </key> + <value> <string>Ram Cache</string> </value> + </item> + </dictionary> + </pickle> + </record> +</ZopeData> diff --git a/bt5/erp5_credential_facebook/SkinTemplateItem/portal_skins/erp5_credential_facebook.xml b/bt5/erp5_credential_facebook/SkinTemplateItem/portal_skins/erp5_credential_facebook.xml new file mode 100644 index 0000000000..dc3f5fcad5 --- /dev/null +++ b/bt5/erp5_credential_facebook/SkinTemplateItem/portal_skins/erp5_credential_facebook.xml @@ -0,0 +1,26 @@ +<?xml version="1.0"?> +<ZopeData> + <record id="1" aka="AAAAAAAAAAE="> + <pickle> + <global name="Folder" module="OFS.Folder"/> + </pickle> + <pickle> + <dictionary> + <item> + <key> <string>_objects</string> </key> + <value> + <tuple/> + </value> + </item> + <item> + <key> <string>id</string> </key> + <value> <string>erp5_credential_facebook</string> </value> + </item> + <item> + <key> <string>title</string> </key> + <value> <string></string> </value> + </item> + </dictionary> + </pickle> + </record> +</ZopeData> diff --git a/bt5/erp5_credential_facebook/SkinTemplateItem/portal_skins/erp5_credential_facebook/Base_createFacebookUser.xml b/bt5/erp5_credential_facebook/SkinTemplateItem/portal_skins/erp5_credential_facebook/Base_createFacebookUser.xml new file mode 100644 index 0000000000..8bc75f0afa --- /dev/null +++ b/bt5/erp5_credential_facebook/SkinTemplateItem/portal_skins/erp5_credential_facebook/Base_createFacebookUser.xml @@ -0,0 +1,67 @@ +<?xml version="1.0"?> +<ZopeData> + <record id="1" aka="AAAAAAAAAAE="> + <pickle> + <global name="PythonScript" module="Products.PythonScripts.PythonScript"/> + </pickle> + <pickle> + <dictionary> + <item> + <key> <string>Script_magic</string> </key> + <value> <int>3</int> </value> + </item> + <item> + <key> <string>_bind_names</string> </key> + <value> + <object> + <klass> + <global name="NameAssignments" module="Shared.DC.Scripts.Bindings"/> + </klass> + <tuple/> + <state> + <dictionary> + <item> + <key> <string>_asgns</string> </key> + <value> + <dictionary> + <item> + <key> <string>name_container</string> </key> + <value> <string>container</string> </value> + </item> + <item> + <key> <string>name_context</string> </key> + <value> <string>context</string> </value> + </item> + <item> + <key> <string>name_m_self</string> </key> + <value> <string>script</string> </value> + </item> + <item> + <key> <string>name_subpath</string> </key> + <value> <string>traverse_subpath</string> </value> + </item> + </dictionary> + </value> + </item> + </dictionary> + </state> + </object> + </value> + </item> + <item> + <key> <string>_body</string> </key> + <value> <string>raise NotImplementedError(\'This script shall be overlodad, as user creation is project specific\')\n +</string> </value> + </item> + <item> + <key> <string>_params</string> </key> + <value> <string>tag, first_name, last_name, reference, email</string> </value> + </item> + <item> + <key> <string>id</string> </key> + <value> <string>Base_createFacebookUser</string> </value> + </item> + </dictionary> + </pickle> + </record> +</ZopeData> diff --git a/bt5/erp5_credential_facebook/bt/change_log b/bt5/erp5_credential_facebook/bt/change_log new file mode 100644 index 0000000000..055bbce55f --- /dev/null +++ b/bt5/erp5_credential_facebook/bt/change_log @@ -0,0 +1,2 @@ +2012/07/04 Åukasz Nowak +* Initial version \ No newline at end of file diff --git a/bt5/erp5_credential_facebook/bt/copyright_list b/bt5/erp5_credential_facebook/bt/copyright_list new file mode 100644 index 0000000000..8dfa82e635 --- /dev/null +++ b/bt5/erp5_credential_facebook/bt/copyright_list @@ -0,0 +1 @@ +Nexedi \ No newline at end of file diff --git a/bt5/erp5_credential_facebook/bt/description b/bt5/erp5_credential_facebook/bt/description new file mode 100644 index 0000000000..039c4845f2 --- /dev/null +++ b/bt5/erp5_credential_facebook/bt/description @@ -0,0 +1 @@ +Facebook based credential system. \ No newline at end of file diff --git a/bt5/erp5_credential_facebook/bt/license b/bt5/erp5_credential_facebook/bt/license new file mode 100644 index 0000000000..3a3e12bcad --- /dev/null +++ b/bt5/erp5_credential_facebook/bt/license @@ -0,0 +1 @@ +GPL \ No newline at end of file diff --git a/bt5/erp5_credential_facebook/bt/revision b/bt5/erp5_credential_facebook/bt/revision new file mode 100644 index 0000000000..56a6051ca2 --- /dev/null +++ b/bt5/erp5_credential_facebook/bt/revision @@ -0,0 +1 @@ +1 \ No newline at end of file diff --git a/bt5/erp5_credential_facebook/bt/template_format_version b/bt5/erp5_credential_facebook/bt/template_format_version new file mode 100644 index 0000000000..56a6051ca2 --- /dev/null +++ b/bt5/erp5_credential_facebook/bt/template_format_version @@ -0,0 +1 @@ +1 \ No newline at end of file diff --git a/bt5/erp5_credential_facebook/bt/template_path_list b/bt5/erp5_credential_facebook/bt/template_path_list new file mode 100644 index 0000000000..6091d5ff7a --- /dev/null +++ b/bt5/erp5_credential_facebook/bt/template_path_list @@ -0,0 +1,2 @@ +portal_caches/facebook_token_cache_factory +portal_caches/facebook_token_cache_factory/volatile_cache_plugin \ No newline at end of file diff --git a/bt5/erp5_credential_facebook/bt/template_skin_id_list b/bt5/erp5_credential_facebook/bt/template_skin_id_list new file mode 100644 index 0000000000..09ca68d954 --- /dev/null +++ b/bt5/erp5_credential_facebook/bt/template_skin_id_list @@ -0,0 +1 @@ +erp5_credential_facebook \ No newline at end of file diff --git a/bt5/erp5_credential_facebook/bt/title b/bt5/erp5_credential_facebook/bt/title new file mode 100644 index 0000000000..09ca68d954 --- /dev/null +++ b/bt5/erp5_credential_facebook/bt/title @@ -0,0 +1 @@ +erp5_credential_facebook \ No newline at end of file diff --git a/bt5/erp5_credential_facebook/bt/version b/bt5/erp5_credential_facebook/bt/version new file mode 100644 index 0000000000..ceab6e11ec --- /dev/null +++ b/bt5/erp5_credential_facebook/bt/version @@ -0,0 +1 @@ +0.1 \ No newline at end of file diff --git a/product/ERP5Security/ERP5FacebookExtractionPlugin.py b/product/ERP5Security/ERP5FacebookExtractionPlugin.py new file mode 100644 index 0000000000..56b39e671d --- /dev/null +++ b/product/ERP5Security/ERP5FacebookExtractionPlugin.py @@ -0,0 +1,235 @@ +# -*- 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.ERP5UserManager import SUPER_USER +from Products.PluggableAuthService.PluggableAuthService import DumbHTTPExtractor +from AccessControl.SecurityManagement import getSecurityManager,\ + setSecurityManager, newSecurityManager +from Products.ERP5Type.Cache import DEFAULT_CACHE_SCOPE +import socket +from Products.ERP5Security.ERP5UserManager import getUserByLogin +from zLOG import LOG, ERROR, INFO + +try: + import facebook +except ImportError: + facebook = None + +#Form for new plugin in ZMI +manage_addERP5FacebookExtractionPluginForm = PageTemplateFile( + 'www/ERP5Security_addERP5FacebookExtractionPlugin', globals(), + __name__='manage_addERP5FacebookExtractionPluginForm') + +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()) + +class ERP5FacebookExtractionPlugin(BasePlugin): + """ + Plugin to authenicate as machines. + """ + + meta_type = "ERP5 Facebook Extraction Plugin" + # cache_fatory_name proposal to begin configurable + cache_factory_name = 'facebook_token_cache_factory' + reference_prefix = 'fb_' + 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 + return cache_factory + + def setFacebookToken(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 getFacebookToken(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: + return cache_entry.getValue() + raise KeyError('Key %r not found' % key) + + def getFacebookEntry(self, token): + timeout = socket.getdefaulttimeout() + try: + # require really fast interaction + socket.setdefaulttimeout(5) + facebook_entry = facebook.GraphAPI(token).get_object("me") + except Exception: + facebook_entry = None + finally: + socket.setdefaulttimeout(timeout) + + user_entry = {} + if facebook_entry is not None: + # sanitise value + try: + for k in ('first_name', 'last_name', 'id', 'email'): + if k == 'id': + user_entry['reference'] = self.reference_prefix + facebook_entry[k].encode( + 'utf-8') + else: + user_entry[k] = facebook_entry[k].encode('utf-8') + except KeyError: + user_entry = None + return user_entry + + #################################### + #ILoginPasswordHostExtractionPlugin# + #################################### + security.declarePrivate('extractCredentials') + def extractCredentials(self, request): + """ Extract facebook credentials from the request header. """ + Base_createFacebookUser = getattr(self.getPortalObject(), + 'Base_createFacebookUser', None) + if facebook is None or Base_createFacebookUser is None: + # no facebook library available or not configured + if facebook is None: + LOG('ERP5FacebookExtractionPlugin', INFO, + 'No facebook module available, disabled authentication.') + if Base_createFacebookUser is None: + LOG('ERP5FacebookExtractionPlugin', INFO, + 'No Base_createFacebookUser script available, install ' + 'erp5_credential_facebook, disabled authentication.') + return DumbHTTPExtractor().extractCredentials(request) + + creds = {} + token = None + if request._auth is not None: + # 1st - try to fetch from Authorization header + if 'facebook' in request._auth.lower(): + l = request._auth.split() + if len(l) == 2: + token = l[1] + + if token is None: + # no token + return DumbHTTPExtractor().extractCredentials(request) + + # token is available + user = None + facebook_entry = None + try: + user = self.getFacebookToken(token) + except KeyError: + facebook_entry = self.getFacebookEntry(token) + if facebook_entry is not None: + user = facebook_entry['reference'] + + if user is None: + # fallback to default way + return DumbHTTPExtractor().extractCredentials(request) + + tag = '%s_user_creation_in_progress' + + if self.getPortalObject().portal_activities.countMessageWithTag(tag) > 0: + self.REQUEST['USER_CREATION_IN_PROGRESS'] = user + else: + # create the user if not found + person_list = getUserByLogin(self.getPortalObject(), user) + if len(person_list) == 0: + sm = getSecurityManager() + if sm.getUser().getId() != SUPER_USER: + newSecurityManager(self, self.getUser(SUPER_USER)) + try: + self.REQUEST['USER_CREATION_IN_PROGRESS'] = user + if facebook_entry is None: + facebook_entry = self.getFacebookEntry(token) + try: + self.Base_createFacebookUser(tag, **facebook_entry) + except Exception: + LOG('ERP5FacebookExtractionPlugin', ERROR, + 'Issue while calling creation script:', error=True) + raise + finally: + setSecurityManager(sm) + try: + self.setFacebookToken(token, user) + except KeyError: + # allow to work w/o cache + pass + creds['external_login'] = user + creds['remote_host'] = request.get('REMOTE_HOST', '') + try: + creds['remote_address'] = request.getClientAddr() + except AttributeError: + creds['remote_address'] = request.get('REMOTE_ADDR', '') + return creds + + manage_editERP5FacebookExtractionPluginForm = PageTemplateFile( + 'www/ERP5Security_editERP5FacebookExtractionPlugin', + globals(), + __name__='manage_editERP5FacebookExtractionPluginForm') + +#List implementation of class +classImplements( ERP5FacebookExtractionPlugin, + plugins.ILoginPasswordHostExtractionPlugin + ) +InitializeClass(ERP5FacebookExtractionPlugin) + diff --git a/product/ERP5Security/__init__.py b/product/ERP5Security/__init__.py index ea30b2e7dd..efde32ddf0 100644 --- a/product/ERP5Security/__init__.py +++ b/product/ERP5Security/__init__.py @@ -28,6 +28,7 @@ import ERP5UserFactory import ERP5KeyAuthPlugin import ERP5ExternalAuthenticationPlugin import ERP5BearerExtractionPlugin +import ERP5FacebookExtractionPlugin def mergedLocalRoles(object): """Returns a merging of object and its ancestors' @@ -64,6 +65,7 @@ registerMultiPlugin(ERP5UserFactory.ERP5UserFactory.meta_type) registerMultiPlugin(ERP5KeyAuthPlugin.ERP5KeyAuthPlugin.meta_type) registerMultiPlugin(ERP5ExternalAuthenticationPlugin.ERP5ExternalAuthenticationPlugin.meta_type) registerMultiPlugin(ERP5BearerExtractionPlugin.ERP5BearerExtractionPlugin.meta_type) +registerMultiPlugin(ERP5FacebookExtractionPlugin.ERP5FacebookExtractionPlugin.meta_type) def initialize(context): @@ -130,6 +132,15 @@ def initialize(context): , icon='www/portal.gif' ) + context.registerClass( ERP5FacebookExtractionPlugin.ERP5FacebookExtractionPlugin + , permission=ManageUsers + , constructors=( + ERP5FacebookExtractionPlugin.manage_addERP5FacebookExtractionPluginForm, + ERP5FacebookExtractionPlugin.addERP5FacebookExtractionPlugin, ) + , visibility=None + , icon='www/portal.gif' + ) + from AccessControl.SecurityInfo import ModuleSecurityInfo ModuleSecurityInfo('Products.ERP5Security.ERP5UserManager').declarePublic( 'getUserByLogin') diff --git a/product/ERP5Security/www/ERP5Security_addERP5FacebookExtractionPlugin.zpt b/product/ERP5Security/www/ERP5Security_addERP5FacebookExtractionPlugin.zpt new file mode 100644 index 0000000000..4fb943e6b3 --- /dev/null +++ b/product/ERP5Security/www/ERP5Security_addERP5FacebookExtractionPlugin.zpt @@ -0,0 +1,36 @@ +<h1 tal:replace="structure context/manage_page_header">PAGE HEADER</h1> +<h2 tal:define="form_title string:Add ERP5 Facebook Extraction Plugin" + tal:replace="structure context/manage_form_title">FORM TITLE</h2> + +<p class="form-help">Please input the configuration</p> + +<form action="addERP5FacebookExtractionPlugin" method="POST"> +<table cellspacing="0" cellpadding="2" border="0"> + <tr> + <td align="left" valign="top"> + <div class="form-label"> + Id + </div> + </td> + <td align="left" valign="top"> + <input type="text" name="id" size="40" /> + </td> + </tr> + <tr> + <td align="left" valign="top"> + <div class="form-label"> + Title + </div> + </td> + <td align="left" valign="top"> + <input type="text" name="title" size="40" /> + </td> + </tr> + <tr> + <td colspan="2"> <input type="submit" value="add plugin"/> + </td> + </tr> +</table> +</form> + +<h1 tal:replace="structure context/manage_page_footer">PAGE FOOTER</h1> -- 2.30.9