Commit 042c5ec7 authored by Gabriel Monnerat's avatar Gabriel Monnerat Committed by Eteri

erp5_oauth_google_login: Implementation of login in ERP5 with Google Account

Google Login follow the same implementation of ERP5 Login(subobject of Person) and with an action in preferences, the user can add Google Login to his person.

- A link was add to login page in ERP5 with Google Account and zocial.min.css is used to display it nicely

- logout was extended to remove cookie __ac_google_hash if authentication with Google account is enabled

- login_form is using ERP5Site_getAvailableOAuthLoginList to know if google login is supported or not. With this, we can extend to other oauth easily.

- ERP5ExternalOauth2ExtractionPlugin don't have the responsability of create user in extraction plugin. A more apporpriate place would be a dedicated "signup using oauth" page, relying on erp5_credential for the actual user creation.

- portal_oauth is used to store secret_key and client_id from Google

- enable PAS plugin through upgrader
parent 930c028e
...@@ -3,6 +3,9 @@ ...@@ -3,6 +3,9 @@
xmlns:i18n="http://xml.zope.org/namespaces/i18n"> xmlns:i18n="http://xml.zope.org/namespaces/i18n">
<tal:block tal:define="form_action string:logged_in; <tal:block tal:define="form_action string:logged_in;
global form_id string:login_form; global form_id string:login_form;
available_oauth_login_list python: context.getPortalObject().ERP5Site_getAvailableOAuthLoginList();
enable_google_login python: 'google' in available_oauth_login_list;
css_list python: enable_google_login and ['%s/zocial.min.css' % here.portal_url()] or [];
js_list python: ['%s/login_form.js' % (here.portal_url(), ), '%s/erp5.js' % (here.portal_url(), )]"> js_list python: ['%s/login_form.js' % (here.portal_url(), ), '%s/erp5.js' % (here.portal_url(), )]">
<tal:block metal:use-macro="here/main_template/macros/master"> <tal:block metal:use-macro="here/main_template/macros/master">
<tal:block metal:fill-slot="main"> <tal:block metal:fill-slot="main">
...@@ -45,7 +48,17 @@ ...@@ -45,7 +48,17 @@
<a tal:attributes="href string:${here/portal_url}/ERP5Site_viewCredentialRecoveryLoginDialog" <a tal:attributes="href string:${here/portal_url}/ERP5Site_viewCredentialRecoveryLoginDialog"
i18n:translate="" i18n:domain="ui">Can't access your account ?</a> i18n:translate="" i18n:domain="ui">Can't access your account ?</a>
</div> </div>
<p class="clear"></p>
</div> </div>
<tal:block tal:condition="enable_google_login">
<div class="field">
<label>&nbsp;</label>
<div class="input">
<a tal:attributes="href string:${here/portal_url}/ERP5Site_redirectToGoogleLoginPage"
i18n:translate="" i18n:domain="ui" class="zocial google">Login with Google</a>
</div>
</div>
</tal:block>
</fieldset> </fieldset>
<script type="text/javascript">setFocus()</script> <script type="text/javascript">setFocus()</script>
<p i18n:translate="" i18n:domain="ui">Having trouble logging in? Make sure to enable cookies in your web browser.</p> <p i18n:translate="" i18n:domain="ui">Having trouble logging in? Make sure to enable cookies in your web browser.</p>
......
<?xml version="1.0"?>
<ZopeData>
<record id="1" aka="AAAAAAAAAAE=">
<pickle>
<global name="Property Sheet" 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>description</string> </key>
<value>
<none/>
</value>
</item>
<item>
<key> <string>id</string> </key>
<value> <string>OAuthClient</string> </value>
</item>
<item>
<key> <string>portal_type</string> </key>
<value> <string>Property Sheet</string> </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>
<?xml version="1.0"?>
<ZopeData>
<record id="1" aka="AAAAAAAAAAE=">
<pickle>
<global name="Standard Property" module="erp5.portal_type"/>
</pickle>
<pickle>
<dictionary>
<item>
<key> <string>categories</string> </key>
<value>
<tuple>
<string>elementary_type/string</string>
</tuple>
</value>
</item>
<item>
<key> <string>description</string> </key>
<value>
<none/>
</value>
</item>
<item>
<key> <string>id</string> </key>
<value> <string>client_id_property</string> </value>
</item>
<item>
<key> <string>portal_type</string> </key>
<value> <string>Standard Property</string> </value>
</item>
</dictionary>
</pickle>
</record>
</ZopeData>
<?xml version="1.0"?>
<ZopeData>
<record id="1" aka="AAAAAAAAAAE=">
<pickle>
<global name="Standard Property" module="erp5.portal_type"/>
</pickle>
<pickle>
<dictionary>
<item>
<key> <string>categories</string> </key>
<value>
<tuple>
<string>elementary_type/string</string>
</tuple>
</value>
</item>
<item>
<key> <string>description</string> </key>
<value>
<none/>
</value>
</item>
<item>
<key> <string>id</string> </key>
<value> <string>reference_property</string> </value>
</item>
<item>
<key> <string>portal_type</string> </key>
<value> <string>Standard Property</string> </value>
</item>
</dictionary>
</pickle>
</record>
</ZopeData>
<?xml version="1.0"?>
<ZopeData>
<record id="1" aka="AAAAAAAAAAE=">
<pickle>
<global name="Standard Property" module="erp5.portal_type"/>
</pickle>
<pickle>
<dictionary>
<item>
<key> <string>categories</string> </key>
<value>
<tuple>
<string>elementary_type/string</string>
</tuple>
</value>
</item>
<item>
<key> <string>description</string> </key>
<value>
<none/>
</value>
</item>
<item>
<key> <string>id</string> </key>
<value> <string>secret_key_property</string> </value>
</item>
<item>
<key> <string>portal_type</string> </key>
<value> <string>Standard Property</string> </value>
</item>
</dictionary>
</pickle>
</record>
</ZopeData>
<?xml version="1.0"?>
<ZopeData>
<record id="1" aka="AAAAAAAAAAE=">
<pickle>
<global name="ERP5 Form" module="erp5.portal_type"/>
</pickle>
<pickle>
<dictionary>
<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/>
</value>
</item>
</dictionary>
</state>
</object>
</value>
</item>
<item>
<key> <string>_objects</string> </key>
<value>
<tuple/>
</value>
</item>
<item>
<key> <string>action</string> </key>
<value> <string>Base_edit</string> </value>
</item>
<item>
<key> <string>description</string> </key>
<value> <string></string> </value>
</item>
<item>
<key> <string>edit_order</string> </key>
<value>
<list/>
</value>
</item>
<item>
<key> <string>encoding</string> </key>
<value> <string>UTF-8</string> </value>
</item>
<item>
<key> <string>enctype</string> </key>
<value> <string></string> </value>
</item>
<item>
<key> <string>group_list</string> </key>
<value>
<list>
<string>left</string>
<string>right</string>
<string>center</string>
<string>bottom</string>
<string>hidden</string>
</list>
</value>
</item>
<item>
<key> <string>groups</string> </key>
<value>
<dictionary>
<item>
<key> <string>bottom</string> </key>
<value>
<list/>
</value>
</item>
<item>
<key> <string>center</string> </key>
<value>
<list/>
</value>
</item>
<item>
<key> <string>hidden</string> </key>
<value>
<list/>
</value>
</item>
<item>
<key> <string>left</string> </key>
<value>
<list>
<string>my_reference</string>
</list>
</value>
</item>
<item>
<key> <string>right</string> </key>
<value>
<list>
<string>my_translated_portal_type</string>
<string>my_translated_validation_state_title</string>
</list>
</value>
</item>
</dictionary>
</value>
</item>
<item>
<key> <string>id</string> </key>
<value> <string>ExternalLogin_view</string> </value>
</item>
<item>
<key> <string>method</string> </key>
<value> <string>POST</string> </value>
</item>
<item>
<key> <string>name</string> </key>
<value> <string>Login_view</string> </value>
</item>
<item>
<key> <string>pt</string> </key>
<value> <string>form_view</string> </value>
</item>
<item>
<key> <string>row_length</string> </key>
<value> <int>4</int> </value>
</item>
<item>
<key> <string>stored_encoding</string> </key>
<value> <string>UTF-8</string> </value>
</item>
<item>
<key> <string>title</string> </key>
<value> <string>Login</string> </value>
</item>
<item>
<key> <string>unicode_mode</string> </key>
<value> <int>0</int> </value>
</item>
<item>
<key> <string>update_action</string> </key>
<value> <string></string> </value>
</item>
<item>
<key> <string>update_action_title</string> </key>
<value> <string></string> </value>
</item>
</dictionary>
</pickle>
</record>
</ZopeData>
<?xml version="1.0"?>
<ZopeData>
<record id="1" aka="AAAAAAAAAAE=">
<pickle>
<global name="ProxyField" module="Products.ERP5Form.ProxyField"/>
</pickle>
<pickle>
<dictionary>
<item>
<key> <string>delegated_list</string> </key>
<value>
<list>
<string>description</string>
<string>title</string>
</list>
</value>
</item>
<item>
<key> <string>id</string> </key>
<value> <string>my_reference</string> </value>
</item>
<item>
<key> <string>message_values</string> </key>
<value>
<dictionary>
<item>
<key> <string>external_validator_failed</string> </key>
<value> <string>The input failed the external validator.</string> </value>
</item>
</dictionary>
</value>
</item>
<item>
<key> <string>overrides</string> </key>
<value>
<dictionary>
<item>
<key> <string>field_id</string> </key>
<value> <string></string> </value>
</item>
<item>
<key> <string>form_id</string> </key>
<value> <string></string> </value>
</item>
<item>
<key> <string>target</string> </key>
<value> <string></string> </value>
</item>
</dictionary>
</value>
</item>
<item>
<key> <string>tales</string> </key>
<value>
<dictionary>
<item>
<key> <string>field_id</string> </key>
<value> <string></string> </value>
</item>
<item>
<key> <string>form_id</string> </key>
<value> <string></string> </value>
</item>
<item>
<key> <string>target</string> </key>
<value> <string></string> </value>
</item>
</dictionary>
</value>
</item>
<item>
<key> <string>values</string> </key>
<value>
<dictionary>
<item>
<key> <string>description</string> </key>
<value> <string>The username this person will use to log in the system. The system will check that there isn\'t another user with the same username.</string> </value>
</item>
<item>
<key> <string>field_id</string> </key>
<value> <string>my_string_field</string> </value>
</item>
<item>
<key> <string>form_id</string> </key>
<value> <string>Base_viewFieldLibrary</string> </value>
</item>
<item>
<key> <string>target</string> </key>
<value> <string>Click to edit the target</string> </value>
</item>
<item>
<key> <string>title</string> </key>
<value> <string>User Login</string> </value>
</item>
</dictionary>
</value>
</item>
</dictionary>
</pickle>
</record>
</ZopeData>
<?xml version="1.0"?>
<ZopeData>
<record id="1" aka="AAAAAAAAAAE=">
<pickle>
<global name="ProxyField" module="Products.ERP5Form.ProxyField"/>
</pickle>
<pickle>
<dictionary>
<item>
<key> <string>id</string> </key>
<value> <string>my_translated_portal_type</string> </value>
</item>
<item>
<key> <string>message_values</string> </key>
<value>
<dictionary>
<item>
<key> <string>external_validator_failed</string> </key>
<value> <string>The input failed the external validator.</string> </value>
</item>
</dictionary>
</value>
</item>
<item>
<key> <string>overrides</string> </key>
<value>
<dictionary>
<item>
<key> <string>field_id</string> </key>
<value> <string></string> </value>
</item>
<item>
<key> <string>form_id</string> </key>
<value> <string></string> </value>
</item>
<item>
<key> <string>target</string> </key>
<value> <string></string> </value>
</item>
</dictionary>
</value>
</item>
<item>
<key> <string>tales</string> </key>
<value>
<dictionary>
<item>
<key> <string>field_id</string> </key>
<value> <string></string> </value>
</item>
<item>
<key> <string>form_id</string> </key>
<value> <string></string> </value>
</item>
<item>
<key> <string>target</string> </key>
<value> <string></string> </value>
</item>
</dictionary>
</value>
</item>
<item>
<key> <string>values</string> </key>
<value>
<dictionary>
<item>
<key> <string>field_id</string> </key>
<value> <string>my_view_mode_translated_portal_type</string> </value>
</item>
<item>
<key> <string>form_id</string> </key>
<value> <string>Base_viewBaseFieldLibrary</string> </value>
</item>
<item>
<key> <string>target</string> </key>
<value> <string>Click to edit the target</string> </value>
</item>
</dictionary>
</value>
</item>
</dictionary>
</pickle>
</record>
</ZopeData>
<?xml version="1.0"?>
<ZopeData>
<record id="1" aka="AAAAAAAAAAE=">
<pickle>
<global name="ProxyField" module="Products.ERP5Form.ProxyField"/>
</pickle>
<pickle>
<dictionary>
<item>
<key> <string>delegated_list</string> </key>
<value>
<list/>
</value>
</item>
<item>
<key> <string>id</string> </key>
<value> <string>my_translated_validation_state_title</string> </value>
</item>
<item>
<key> <string>message_values</string> </key>
<value>
<dictionary>
<item>
<key> <string>external_validator_failed</string> </key>
<value> <string>The input failed the external validator.</string> </value>
</item>
</dictionary>
</value>
</item>
<item>
<key> <string>overrides</string> </key>
<value>
<dictionary>
<item>
<key> <string>field_id</string> </key>
<value> <string></string> </value>
</item>
<item>
<key> <string>form_id</string> </key>
<value> <string></string> </value>
</item>
<item>
<key> <string>target</string> </key>
<value> <string></string> </value>
</item>
</dictionary>
</value>
</item>
<item>
<key> <string>tales</string> </key>
<value>
<dictionary>
<item>
<key> <string>field_id</string> </key>
<value> <string></string> </value>
</item>
<item>
<key> <string>form_id</string> </key>
<value> <string></string> </value>
</item>
<item>
<key> <string>target</string> </key>
<value> <string></string> </value>
</item>
</dictionary>
</value>
</item>
<item>
<key> <string>values</string> </key>
<value>
<dictionary>
<item>
<key> <string>field_id</string> </key>
<value> <string>my_translated_workflow_state_title</string> </value>
</item>
<item>
<key> <string>form_id</string> </key>
<value> <string>Base_viewFieldLibrary</string> </value>
</item>
<item>
<key> <string>target</string> </key>
<value> <string>Click to edit the target</string> </value>
</item>
</dictionary>
</value>
</item>
</dictionary>
</pickle>
</record>
</ZopeData>
...@@ -10,6 +10,7 @@ ...@@ -10,6 +10,7 @@
<key> <string>delegated_list</string> </key> <key> <string>delegated_list</string> </key>
<value> <value>
<list> <list>
<string>all_columns</string>
<string>columns</string> <string>columns</string>
<string>count_method</string> <string>count_method</string>
<string>selection_name</string> <string>selection_name</string>
...@@ -78,6 +79,17 @@ ...@@ -78,6 +79,17 @@
<key> <string>Base_viewSearchResultList</string> </key> <key> <string>Base_viewSearchResultList</string> </key>
<value> <int>1</int> </value> <value> <int>1</int> </value>
</item> </item>
<item>
<key> <string>all_columns</string> </key>
<value>
<list>
<tuple>
<string>modification_date</string>
<string>Modification Date</string>
</tuple>
</list>
</value>
</item>
<item> <item>
<key> <string>all_editable_columns</string> </key> <key> <string>all_editable_columns</string> </key>
<value> <value>
...@@ -93,8 +105,8 @@ ...@@ -93,8 +105,8 @@
<string>Reference</string> <string>Reference</string>
</tuple> </tuple>
<tuple> <tuple>
<string>modification_date</string> <string>translated_validation_state_title</string>
<string>Modification Date</string> <string>State</string>
</tuple> </tuple>
</list> </list>
</value> </value>
......
OAuthClient
\ No newline at end of file
<?xml version="1.0"?>
<ZopeData>
<record id="1" aka="AAAAAAAAAAE=">
<pickle>
<global name="ActionInformation" module="Products.CMFCore.ActionInformation"/>
</pickle>
<pickle>
<dictionary>
<item>
<key> <string>action</string> </key>
<value>
<persistent> <string encoding="base64">AAAAAAAAAAI=</string> </persistent>
</value>
</item>
<item>
<key> <string>categories</string> </key>
<value>
<tuple>
<string>action_type/object_view</string>
</tuple>
</value>
</item>
<item>
<key> <string>category</string> </key>
<value> <string>object_view</string> </value>
</item>
<item>
<key> <string>condition</string> </key>
<value> <string></string> </value>
</item>
<item>
<key> <string>description</string> </key>
<value>
<none/>
</value>
</item>
<item>
<key> <string>icon</string> </key>
<value> <string></string> </value>
</item>
<item>
<key> <string>id</string> </key>
<value> <string>view</string> </value>
</item>
<item>
<key> <string>permissions</string> </key>
<value>
<tuple>
<string>View</string>
</tuple>
</value>
</item>
<item>
<key> <string>portal_type</string> </key>
<value> <string>Action Information</string> </value>
</item>
<item>
<key> <string>priority</string> </key>
<value> <float>1.0</float> </value>
</item>
<item>
<key> <string>title</string> </key>
<value> <string>View</string> </value>
</item>
<item>
<key> <string>visible</string> </key>
<value> <int>1</int> </value>
</item>
</dictionary>
</pickle>
</record>
<record id="2" aka="AAAAAAAAAAI=">
<pickle>
<global name="Expression" module="Products.CMFCore.Expression"/>
</pickle>
<pickle>
<dictionary>
<item>
<key> <string>text</string> </key>
<value> <string>string: ${object_url}/GoogleConnector_view</string> </value>
</item>
</dictionary>
</pickle>
</record>
</ZopeData>
<?xml version="1.0"?>
<ZopeData>
<record id="1" aka="AAAAAAAAAAE=">
<pickle>
<global name="ActionInformation" module="Products.CMFCore.ActionInformation"/>
</pickle>
<pickle>
<dictionary>
<item>
<key> <string>action</string> </key>
<value>
<persistent> <string encoding="base64">AAAAAAAAAAI=</string> </persistent>
</value>
</item>
<item>
<key> <string>categories</string> </key>
<value>
<tuple>
<string>action_type/object_view</string>
</tuple>
</value>
</item>
<item>
<key> <string>category</string> </key>
<value> <string>object_view</string> </value>
</item>
<item>
<key> <string>condition</string> </key>
<value> <string></string> </value>
</item>
<item>
<key> <string>description</string> </key>
<value>
<none/>
</value>
</item>
<item>
<key> <string>icon</string> </key>
<value> <string></string> </value>
</item>
<item>
<key> <string>id</string> </key>
<value> <string>view</string> </value>
</item>
<item>
<key> <string>permissions</string> </key>
<value>
<tuple>
<string>View</string>
</tuple>
</value>
</item>
<item>
<key> <string>portal_type</string> </key>
<value> <string>Action Information</string> </value>
</item>
<item>
<key> <string>priority</string> </key>
<value> <float>1.0</float> </value>
</item>
<item>
<key> <string>title</string> </key>
<value> <string>View</string> </value>
</item>
<item>
<key> <string>visible</string> </key>
<value> <int>1</int> </value>
</item>
</dictionary>
</pickle>
</record>
<record id="2" aka="AAAAAAAAAAI=">
<pickle>
<global name="Expression" module="Products.CMFCore.Expression"/>
</pickle>
<pickle>
<dictionary>
<item>
<key> <string>text</string> </key>
<value> <string>string:${object_url}/ExternalLogin_view</string> </value>
</item>
</dictionary>
</pickle>
</record>
</ZopeData>
import json
import oauth2client.client
from Products.ERP5Security.ERP5ExternalOauth2ExtractionPlugin import getGoogleUserEntry
SCOPE_LIST = ['https://www.googleapis.com/auth/userinfo.profile',
'https://www.googleapis.com/auth/userinfo.email']
def redirectToGoogleLoginPage(self):
client_id, secret_key = self.ERP5Site_getGoogleClientIdAndSecretKey()
flow = oauth2client.client.OAuth2WebServerFlow(
client_id=client_id,
client_secret=secret_key,
scope=SCOPE_LIST,
redirect_uri="{0}/ERP5Site_receiveGoogleCallback".format(self.absolute_url()),
access_type="offline",
prompt="consent",
include_granted_scopes="true")
self.REQUEST.RESPONSE.redirect(flow.step1_get_authorize_url())
def getAccessTokenFromCode(self, code, redirect_uri):
portal = self.getPortalObject()
client_id, secret_key = portal.ERP5Site_getGoogleClientIdAndSecretKey()
flow = oauth2client.client.OAuth2WebServerFlow(
client_id=client_id,
client_secret=secret_key,
scope=SCOPE_LIST,
redirect_uri=redirect_uri,
access_type="offline",
include_granted_scopes="true")
credential = flow.step2_exchange(code)
credential_data = json.loads(credential.to_json())
return credential_data
def getUserEntry(access_token):
return getGoogleUserEntry(access_token)
\ No newline at end of file
<?xml version="1.0"?>
<ZopeData>
<record id="1" aka="AAAAAAAAAAE=">
<pickle>
<global name="Extension Component" module="erp5.portal_type"/>
</pickle>
<pickle>
<dictionary>
<item>
<key> <string>_recorded_property_dict</string> </key>
<value>
<persistent> <string encoding="base64">AAAAAAAAAAI=</string> </persistent>
</value>
</item>
<item>
<key> <string>default_reference</string> </key>
<value> <string>GoogleLoginUtility</string> </value>
</item>
<item>
<key> <string>description</string> </key>
<value>
<none/>
</value>
</item>
<item>
<key> <string>id</string> </key>
<value> <string>extension.erp5.GoogleLoginUtility</string> </value>
</item>
<item>
<key> <string>portal_type</string> </key>
<value> <string>Extension Component</string> </value>
</item>
<item>
<key> <string>sid</string> </key>
<value>
<none/>
</value>
</item>
<item>
<key> <string>text_content_error_message</string> </key>
<value>
<tuple/>
</value>
</item>
<item>
<key> <string>text_content_warning_message</string> </key>
<value>
<tuple/>
</value>
</item>
<item>
<key> <string>version</string> </key>
<value> <string>erp5</string> </value>
</item>
<item>
<key> <string>workflow_history</string> </key>
<value>
<persistent> <string encoding="base64">AAAAAAAAAAM=</string> </persistent>
</value>
</item>
</dictionary>
</pickle>
</record>
<record id="2" aka="AAAAAAAAAAI=">
<pickle>
<global name="PersistentMapping" module="Persistence.mapping"/>
</pickle>
<pickle>
<dictionary>
<item>
<key> <string>data</string> </key>
<value>
<dictionary/>
</value>
</item>
</dictionary>
</pickle>
</record>
<record id="3" aka="AAAAAAAAAAM=">
<pickle>
<global name="PersistentMapping" module="Persistence.mapping"/>
</pickle>
<pickle>
<dictionary>
<item>
<key> <string>data</string> </key>
<value>
<dictionary>
<item>
<key> <string>component_validation_workflow</string> </key>
<value>
<persistent> <string encoding="base64">AAAAAAAAAAQ=</string> </persistent>
</value>
</item>
</dictionary>
</value>
</item>
</dictionary>
</pickle>
</record>
<record id="4" aka="AAAAAAAAAAQ=">
<pickle>
<global name="WorkflowHistoryList" module="Products.ERP5Type.patches.WorkflowTool"/>
</pickle>
<pickle>
<tuple>
<none/>
<list>
<dictionary>
<item>
<key> <string>action</string> </key>
<value> <string>validate</string> </value>
</item>
<item>
<key> <string>validation_state</string> </key>
<value> <string>validated</string> </value>
</item>
</dictionary>
</list>
</tuple>
</pickle>
</record>
</ZopeData>
<?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>86400</int> </value>
</item>
<item>
<key> <string>description</string> </key>
<value>
<none/>
</value>
</item>
<item>
<key> <string>id</string> </key>
<value> <string>google_server_auth_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> <string>google_server_auth_token_cache_factory</string> </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>
<?xml version="1.0"?>
<ZopeData>
<record id="1" aka="AAAAAAAAAAE=">
<pickle>
<global name="Distributed Ram Cache" module="erp5.portal_type"/>
</pickle>
<pickle>
<dictionary>
<item>
<key> <string>categories</string> </key>
<value>
<tuple>
<string>specialise/portal_memcached/persistent_memcached_plugin</string>
</tuple>
</value>
</item>
<item>
<key> <string>id</string> </key>
<value> <string>persistent_cache_plugin</string> </value>
</item>
<item>
<key> <string>int_index</string> </key>
<value> <int>1</int> </value>
</item>
<item>
<key> <string>portal_type</string> </key>
<value> <string>Distributed Ram Cache</string> </value>
</item>
<item>
<key> <string>title</string> </key>
<value> <string>persistent_cache</string> </value>
</item>
</dictionary>
</pickle>
</record>
</ZopeData>
<allowed_content_type_list>
<portal_type id="OAuth Tool">
<item>Google Connector</item>
</portal_type>
<portal_type id="Person">
<item>Google Login</item>
</portal_type>
</allowed_content_type_list>
\ No newline at end of file
<property_sheet_list>
<portal_type id="Google Connector">
<item>OAuthClient</item>
</portal_type>
<portal_type id="Template Tool">
<item>TemplateToolERP5GoogleExtractionPluginConstraint</item>
</portal_type>
</property_sheet_list>
<?xml version="1.0"?>
<ZopeData>
<record id="1" aka="AAAAAAAAAAE=">
<pickle>
<global name="Base Type" module="erp5.portal_type"/>
</pickle>
<pickle>
<dictionary>
<item>
<key> <string>content_icon</string> </key>
<value>
<none/>
</value>
</item>
<item>
<key> <string>description</string> </key>
<value>
<none/>
</value>
</item>
<item>
<key> <string>id</string> </key>
<value> <string>Google Connector</string> </value>
</item>
<item>
<key> <string>init_script</string> </key>
<value>
<none/>
</value>
</item>
<item>
<key> <string>permission</string> </key>
<value>
<none/>
</value>
</item>
<item>
<key> <string>portal_type</string> </key>
<value> <string>Base Type</string> </value>
</item>
<item>
<key> <string>type_class</string> </key>
<value> <string>XMLObject</string> </value>
</item>
<item>
<key> <string>type_interface</string> </key>
<value>
<tuple/>
</value>
</item>
<item>
<key> <string>type_mixin</string> </key>
<value>
<tuple/>
</value>
</item>
</dictionary>
</pickle>
</record>
</ZopeData>
<?xml version="1.0"?>
<ZopeData>
<record id="1" aka="AAAAAAAAAAE=">
<pickle>
<global name="Base Type" module="erp5.portal_type"/>
</pickle>
<pickle>
<dictionary>
<item>
<key> <string>content_icon</string> </key>
<value>
<none/>
</value>
</item>
<item>
<key> <string>description</string> </key>
<value>
<none/>
</value>
</item>
<item>
<key> <string>group_list</string> </key>
<value>
<tuple>
<string>login</string>
</tuple>
</value>
</item>
<item>
<key> <string>id</string> </key>
<value> <string>Google Login</string> </value>
</item>
<item>
<key> <string>init_script</string> </key>
<value>
<none/>
</value>
</item>
<item>
<key> <string>permission</string> </key>
<value>
<none/>
</value>
</item>
<item>
<key> <string>portal_type</string> </key>
<value> <string>Base Type</string> </value>
</item>
<item>
<key> <string>searchable_text_property_id</string> </key>
<value>
<tuple>
<string>reference</string>
</tuple>
</value>
</item>
<item>
<key> <string>type_class</string> </key>
<value> <string>Login</string> </value>
</item>
<item>
<key> <string>type_interface</string> </key>
<value>
<tuple/>
</value>
</item>
<item>
<key> <string>type_mixin</string> </key>
<value>
<tuple/>
</value>
</item>
</dictionary>
</pickle>
</record>
</ZopeData>
<workflow_chain>
<chain>
<type>Google Connector</type>
<workflow>edit_workflow, validation_workflow</workflow>
</chain>
<chain>
<type>Google Login</type>
<workflow>edit_workflow, validation_workflow</workflow>
</chain>
</workflow_chain>
\ No newline at end of file
<?xml version="1.0"?>
<ZopeData>
<record id="1" aka="AAAAAAAAAAE=">
<pickle>
<global name="Property Sheet" 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>description</string> </key>
<value>
<none/>
</value>
</item>
<item>
<key> <string>id</string> </key>
<value> <string>TemplateToolERP5GoogleExtractionPluginConstraint</string> </value>
</item>
<item>
<key> <string>portal_type</string> </key>
<value> <string>Property Sheet</string> </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>
<?xml version="1.0"?>
<ZopeData>
<record id="1" aka="AAAAAAAAAAE=">
<pickle>
<global name="Script Constraint" module="erp5.portal_type"/>
</pickle>
<pickle>
<dictionary>
<item>
<key> <string>_identity_criterion</string> </key>
<value>
<persistent> <string encoding="base64">AAAAAAAAAAI=</string> </persistent>
</value>
</item>
<item>
<key> <string>_range_criterion</string> </key>
<value>
<persistent> <string encoding="base64">AAAAAAAAAAM=</string> </persistent>
</value>
</item>
<item>
<key> <string>categories</string> </key>
<value>
<tuple>
<string>constraint_type/post_upgrade</string>
</tuple>
</value>
</item>
<item>
<key> <string>description</string> </key>
<value>
<none/>
</value>
</item>
<item>
<key> <string>id</string> </key>
<value> <string>ERP5GoogleExtractionPlugin_existence_constraint</string> </value>
</item>
<item>
<key> <string>portal_type</string> </key>
<value> <string>Script Constraint</string> </value>
</item>
<item>
<key> <string>script_id</string> </key>
<value> <string>TemplateTool_checkGoogleExtractionPluginExistenceConsistency</string> </value>
</item>
</dictionary>
</pickle>
</record>
<record id="2" aka="AAAAAAAAAAI=">
<pickle>
<global name="PersistentMapping" module="Persistence.mapping"/>
</pickle>
<pickle>
<dictionary>
<item>
<key> <string>data</string> </key>
<value>
<dictionary/>
</value>
</item>
</dictionary>
</pickle>
</record>
<record id="3" aka="AAAAAAAAAAM=">
<pickle>
<global name="PersistentMapping" module="Persistence.mapping"/>
</pickle>
<pickle>
<dictionary>
<item>
<key> <string>data</string> </key>
<value>
<dictionary/>
</value>
</item>
</dictionary>
</pickle>
</record>
</ZopeData>
<?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_oauth_google_login</string> </value>
</item>
<item>
<key> <string>title</string> </key>
<value> <string></string> </value>
</item>
</dictionary>
</pickle>
</record>
</ZopeData>
<?xml version="1.0"?>
<ZopeData>
<record id="1" aka="AAAAAAAAAAE=">
<pickle>
<global name="ExternalMethod" module="Products.ExternalMethod.ExternalMethod"/>
</pickle>
<pickle>
<dictionary>
<item>
<key> <string>_function</string> </key>
<value> <string>getAccessTokenFromCode</string> </value>
</item>
<item>
<key> <string>_module</string> </key>
<value> <string>GoogleLoginUtility</string> </value>
</item>
<item>
<key> <string>id</string> </key>
<value> <string>ERP5Site_getAccessTokenFromCode</string> </value>
</item>
<item>
<key> <string>title</string> </key>
<value> <string></string> </value>
</item>
</dictionary>
</pickle>
</record>
</ZopeData>
if REQUEST is not None:
raise ValueError("This script can't be called in the URL")
result_list = context.getPortalObject().portal_catalog(
portal_type="Google Connector",
reference=reference,
validation_state="validated",
limit=2,
)
assert result_list, "Google Connector not found"
if len(result_list) == 2:
raise ValueError("Impossible to select one Google Connector")
google_connector = result_list[0].getObject()
return google_connector.getClientId(), google_connector.getSecretKey()
<?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>_params</string> </key>
<value> <string>reference="default", REQUEST=None</string> </value>
</item>
<item>
<key> <string>_proxy_roles</string> </key>
<value>
<tuple>
<string>Manager</string>
</tuple>
</value>
</item>
<item>
<key> <string>id</string> </key>
<value> <string>ERP5Site_getGoogleClientIdAndSecretKey</string> </value>
</item>
</dictionary>
</pickle>
</record>
</ZopeData>
<?xml version="1.0"?>
<ZopeData>
<record id="1" aka="AAAAAAAAAAE=">
<pickle>
<global name="ExternalMethod" module="Products.ExternalMethod.ExternalMethod"/>
</pickle>
<pickle>
<dictionary>
<item>
<key> <string>_function</string> </key>
<value> <string>getUserEntry</string> </value>
</item>
<item>
<key> <string>_module</string> </key>
<value> <string>GoogleLoginUtility</string> </value>
</item>
<item>
<key> <string>id</string> </key>
<value> <string>ERP5Site_getGoogleUserEntry</string> </value>
</item>
<item>
<key> <string>title</string> </key>
<value> <string></string> </value>
</item>
</dictionary>
</pickle>
</record>
</ZopeData>
import time
def handleError(error):
context.Base_redirect(
'login_form',
keep_items={"portal_status_message":
context.Base_translateString(
"There was problem with Google login: ${error}. Please try again later.",
mapping={"error": error})
})
if error is not None:
return handleError(error)
elif code is not None:
response_dict = context.ERP5Site_getAccessTokenFromCode(
code,
"{0}/ERP5Site_receiveGoogleCallback".format(context.absolute_url()))
if response_dict is not None:
access_token = response_dict['access_token'].encode('utf-8')
hash_str = context.Base_getHMAC(access_token, access_token)
context.REQUEST.RESPONSE.setCookie('__ac_google_hash', hash_str, path='/')
# store timestamp in second since the epoch in UTC is enough
response_dict["response_timestamp"] = time.time()
context.Base_setBearerToken(hash_str,
response_dict,
"google_server_auth_token_cache_factory")
user_dict = context.ERP5Site_getGoogleUserEntry(access_token)
user_reference = user_dict["email"]
context.Base_setBearerToken(access_token,
{"reference": user_reference},
"google_server_auth_token_cache_factory")
method = getattr(context, "ERP5Site_createGoogleUserToOAuth", None)
if method is not None:
method(user_reference, user_dict)
return context.REQUEST.RESPONSE.redirect(
context.REQUEST.get("came_from") or context.absolute_url())
return handleError('')
<?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>_params</string> </key>
<value> <string>code=None, error=None</string> </value>
</item>
<item>
<key> <string>id</string> </key>
<value> <string>ERP5Site_receiveGoogleCallback</string> </value>
</item>
</dictionary>
</pickle>
</record>
</ZopeData>
<?xml version="1.0"?>
<ZopeData>
<record id="1" aka="AAAAAAAAAAE=">
<pickle>
<global name="ExternalMethod" module="Products.ExternalMethod.ExternalMethod"/>
</pickle>
<pickle>
<dictionary>
<item>
<key> <string>_function</string> </key>
<value> <string>redirectToGoogleLoginPage</string> </value>
</item>
<item>
<key> <string>_module</string> </key>
<value> <string>GoogleLoginUtility</string> </value>
</item>
<item>
<key> <string>id</string> </key>
<value> <string>ERP5Site_redirectToGoogleLoginPage</string> </value>
</item>
<item>
<key> <string>title</string> </key>
<value> <string></string> </value>
</item>
</dictionary>
</pickle>
</record>
</ZopeData>
<?xml version="1.0"?>
<ZopeData>
<record id="1" aka="AAAAAAAAAAE=">
<pickle>
<global name="ERP5 Form" module="erp5.portal_type"/>
</pickle>
<pickle>
<dictionary>
<item>
<key> <string>_objects</string> </key>
<value>
<tuple/>
</value>
</item>
<item>
<key> <string>action</string> </key>
<value> <string>Base_edit</string> </value>
</item>
<item>
<key> <string>description</string> </key>
<value> <string></string> </value>
</item>
<item>
<key> <string>edit_order</string> </key>
<value>
<list/>
</value>
</item>
<item>
<key> <string>encoding</string> </key>
<value> <string>UTF-8</string> </value>
</item>
<item>
<key> <string>enctype</string> </key>
<value> <string></string> </value>
</item>
<item>
<key> <string>group_list</string> </key>
<value>
<list>
<string>left</string>
<string>right</string>
<string>center</string>
<string>bottom</string>
<string>hidden</string>
</list>
</value>
</item>
<item>
<key> <string>groups</string> </key>
<value>
<dictionary>
<item>
<key> <string>bottom</string> </key>
<value>
<list/>
</value>
</item>
<item>
<key> <string>center</string> </key>
<value>
<list/>
</value>
</item>
<item>
<key> <string>hidden</string> </key>
<value>
<list/>
</value>
</item>
<item>
<key> <string>left</string> </key>
<value>
<list>
<string>my_client_id</string>
<string>my_secret_key</string>
</list>
</value>
</item>
<item>
<key> <string>right</string> </key>
<value>
<list>
<string>my_reference</string>
<string>my_translated_validation_state_title</string>
</list>
</value>
</item>
</dictionary>
</value>
</item>
<item>
<key> <string>id</string> </key>
<value> <string>GoogleConnector_view</string> </value>
</item>
<item>
<key> <string>method</string> </key>
<value> <string>POST</string> </value>
</item>
<item>
<key> <string>name</string> </key>
<value> <string>GoogleConnector_view</string> </value>
</item>
<item>
<key> <string>pt</string> </key>
<value> <string>form_view</string> </value>
</item>
<item>
<key> <string>row_length</string> </key>
<value> <int>4</int> </value>
</item>
<item>
<key> <string>stored_encoding</string> </key>
<value> <string>UTF-8</string> </value>
</item>
<item>
<key> <string>title</string> </key>
<value> <string>Google Connector</string> </value>
</item>
<item>
<key> <string>unicode_mode</string> </key>
<value> <int>0</int> </value>
</item>
<item>
<key> <string>update_action</string> </key>
<value> <string></string> </value>
</item>
<item>
<key> <string>update_action_title</string> </key>
<value> <string></string> </value>
</item>
</dictionary>
</pickle>
</record>
</ZopeData>
<?xml version="1.0"?>
<ZopeData>
<record id="1" aka="AAAAAAAAAAE=">
<pickle>
<global name="ProxyField" module="Products.ERP5Form.ProxyField"/>
</pickle>
<pickle>
<dictionary>
<item>
<key> <string>delegated_list</string> </key>
<value>
<list>
<string>title</string>
</list>
</value>
</item>
<item>
<key> <string>id</string> </key>
<value> <string>my_client_id</string> </value>
</item>
<item>
<key> <string>message_values</string> </key>
<value>
<dictionary>
<item>
<key> <string>external_validator_failed</string> </key>
<value> <string>The input failed the external validator.</string> </value>
</item>
</dictionary>
</value>
</item>
<item>
<key> <string>overrides</string> </key>
<value>
<dictionary>
<item>
<key> <string>field_id</string> </key>
<value> <string></string> </value>
</item>
<item>
<key> <string>form_id</string> </key>
<value> <string></string> </value>
</item>
</dictionary>
</value>
</item>
<item>
<key> <string>tales</string> </key>
<value>
<dictionary>
<item>
<key> <string>field_id</string> </key>
<value> <string></string> </value>
</item>
<item>
<key> <string>form_id</string> </key>
<value> <string></string> </value>
</item>
</dictionary>
</value>
</item>
<item>
<key> <string>values</string> </key>
<value>
<dictionary>
<item>
<key> <string>field_id</string> </key>
<value> <string>my_reference</string> </value>
</item>
<item>
<key> <string>form_id</string> </key>
<value> <string>Base_viewFieldLibrary</string> </value>
</item>
<item>
<key> <string>title</string> </key>
<value> <string>Client Id</string> </value>
</item>
</dictionary>
</value>
</item>
</dictionary>
</pickle>
</record>
</ZopeData>
<?xml version="1.0"?>
<ZopeData>
<record id="1" aka="AAAAAAAAAAE=">
<pickle>
<global name="ProxyField" module="Products.ERP5Form.ProxyField"/>
</pickle>
<pickle>
<dictionary>
<item>
<key> <string>id</string> </key>
<value> <string>my_reference</string> </value>
</item>
<item>
<key> <string>message_values</string> </key>
<value>
<dictionary>
<item>
<key> <string>external_validator_failed</string> </key>
<value> <string>The input failed the external validator.</string> </value>
</item>
</dictionary>
</value>
</item>
<item>
<key> <string>overrides</string> </key>
<value>
<dictionary>
<item>
<key> <string>field_id</string> </key>
<value> <string></string> </value>
</item>
<item>
<key> <string>form_id</string> </key>
<value> <string></string> </value>
</item>
</dictionary>
</value>
</item>
<item>
<key> <string>tales</string> </key>
<value>
<dictionary>
<item>
<key> <string>field_id</string> </key>
<value> <string></string> </value>
</item>
<item>
<key> <string>form_id</string> </key>
<value> <string></string> </value>
</item>
</dictionary>
</value>
</item>
<item>
<key> <string>values</string> </key>
<value>
<dictionary>
<item>
<key> <string>field_id</string> </key>
<value> <string>my_reference</string> </value>
</item>
<item>
<key> <string>form_id</string> </key>
<value> <string>Base_viewFieldLibrary</string> </value>
</item>
</dictionary>
</value>
</item>
</dictionary>
</pickle>
</record>
</ZopeData>
<?xml version="1.0"?>
<ZopeData>
<record id="1" aka="AAAAAAAAAAE=">
<pickle>
<global name="ProxyField" module="Products.ERP5Form.ProxyField"/>
</pickle>
<pickle>
<dictionary>
<item>
<key> <string>delegated_list</string> </key>
<value>
<list>
<string>title</string>
</list>
</value>
</item>
<item>
<key> <string>id</string> </key>
<value> <string>my_secret_key</string> </value>
</item>
<item>
<key> <string>message_values</string> </key>
<value>
<dictionary>
<item>
<key> <string>external_validator_failed</string> </key>
<value> <string>The input failed the external validator.</string> </value>
</item>
</dictionary>
</value>
</item>
<item>
<key> <string>overrides</string> </key>
<value>
<dictionary>
<item>
<key> <string>field_id</string> </key>
<value> <string></string> </value>
</item>
<item>
<key> <string>form_id</string> </key>
<value> <string></string> </value>
</item>
</dictionary>
</value>
</item>
<item>
<key> <string>tales</string> </key>
<value>
<dictionary>
<item>
<key> <string>field_id</string> </key>
<value> <string></string> </value>
</item>
<item>
<key> <string>form_id</string> </key>
<value> <string></string> </value>
</item>
</dictionary>
</value>
</item>
<item>
<key> <string>values</string> </key>
<value>
<dictionary>
<item>
<key> <string>field_id</string> </key>
<value> <string>my_reference</string> </value>
</item>
<item>
<key> <string>form_id</string> </key>
<value> <string>Base_viewFieldLibrary</string> </value>
</item>
<item>
<key> <string>title</string> </key>
<value> <string>Secret Key</string> </value>
</item>
</dictionary>
</value>
</item>
</dictionary>
</pickle>
</record>
</ZopeData>
<?xml version="1.0"?>
<ZopeData>
<record id="1" aka="AAAAAAAAAAE=">
<pickle>
<global name="ProxyField" module="Products.ERP5Form.ProxyField"/>
</pickle>
<pickle>
<dictionary>
<item>
<key> <string>delegated_list</string> </key>
<value>
<list/>
</value>
</item>
<item>
<key> <string>id</string> </key>
<value> <string>my_translated_validation_state_title</string> </value>
</item>
<item>
<key> <string>message_values</string> </key>
<value>
<dictionary>
<item>
<key> <string>external_validator_failed</string> </key>
<value> <string>The input failed the external validator.</string> </value>
</item>
</dictionary>
</value>
</item>
<item>
<key> <string>overrides</string> </key>
<value>
<dictionary>
<item>
<key> <string>field_id</string> </key>
<value> <string></string> </value>
</item>
<item>
<key> <string>form_id</string> </key>
<value> <string></string> </value>
</item>
</dictionary>
</value>
</item>
<item>
<key> <string>tales</string> </key>
<value>
<dictionary>
<item>
<key> <string>field_id</string> </key>
<value> <string></string> </value>
</item>
<item>
<key> <string>form_id</string> </key>
<value> <string></string> </value>
</item>
</dictionary>
</value>
</item>
<item>
<key> <string>values</string> </key>
<value>
<dictionary>
<item>
<key> <string>field_id</string> </key>
<value> <string>my_translated_workflow_state_title</string> </value>
</item>
<item>
<key> <string>form_id</string> </key>
<value> <string>Base_viewFieldLibrary</string> </value>
</item>
</dictionary>
</value>
</item>
</dictionary>
</pickle>
</record>
</ZopeData>
acl_users = context.getPortalObject().acl_users
plugin_id = 'erp5_google_extraction'
error_list = []
if plugin_id not in acl_users.objectIds():
error_list.append(
'ERP5 Google Extraction Plugin does not exist as %s/%s' % (acl_users.getPath(), plugin_id))
if fixit:
acl_users.manage_addProduct['ERP5Security'].addERP5GoogleExtractionPlugin(plugin_id)
getattr(acl_users, plugin_id).manage_activateInterfaces([
'IExtractionPlugin',
])
return error_list
<?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>_params</string> </key>
<value> <string>fixit=False</string> </value>
</item>
<item>
<key> <string>id</string> </key>
<value> <string>TemplateTool_checkGoogleExtractionPluginExistenceConsistency</string> </value>
</item>
</dictionary>
</pickle>
</record>
</ZopeData>
##############################################################################
#
# Copyright (c) 2002-2016 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 adviced 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.
#
##############################################################################
import uuid
from Products.ERP5Type.tests.ERP5TypeTestCase import ERP5TypeTestCase
from erp5.component.extension import GoogleLoginUtility
from Products.ERP5Type.tests.utils import createZODBPythonScript
CLIENT_ID = "a1b2c3"
SECRET_KEY = "3c2ba1"
ACCESS_TOKEN = "T1234"
CODE = "1234"
def getUserId(access_token):
return "dummy@example.com"
def getAccessTokenFromCode(code, redirect_uri):
assert code == CODE, "Invalid code"
# This is an example of a Google response
return {'_module': 'oauth2client.client',
'scopes': ['https://www.googleapis.com/auth/userinfo.email', 'https://www.googleapis.com/auth/userinfo.profile'],
'revoke_uri': 'https://accounts.google.com/o/oauth2/revoke',
'access_token': ACCESS_TOKEN,
'token_uri': 'https://www.googleapis.com/oauth2/v4/token',
'token_info_uri': 'https://www.googleapis.com/oauth2/v3/tokeninfo',
'invalid': False,
'token_response': {
'access_token': ACCESS_TOKEN,
'token_type': 'Bearer',
'expires_in': 3600,
'refresh_token': "111",
'id_token': '222'
},
'client_id': CLIENT_ID,
'id_token': {
'picture': '',
'sub': '',
'aud': '',
'family_name': 'D',
'iss': 'https://accounts.google.com',
'email_verified': True,
'at_hash': 'p3vPYQkVuqByBA',
'given_name': 'John',
'exp': 123,
'azp': '123.apps.googleusercontent.com',
'iat': 455,
'locale': 'pt',
'email': getUserId(None),
'name': 'John D'
},
'client_secret': 'secret',
'token_expiry': '2017-03-31T16:06:28Z',
'_class': 'OAuth2Credentials',
'refresh_token': '111',
'user_agent': None
}
def getUserEntry(access_token):
return {
"first_name": "John",
"last_name": "Doe",
"email": getUserId(None),
"reference": getUserId(None)
}
GoogleLoginUtility_getAccessTokenFromCode = GoogleLoginUtility.getAccessTokenFromCode
GoogleLoginUtility_getUserEntry = GoogleLoginUtility.getUserEntry
class TestGoogleLogin(ERP5TypeTestCase):
def getTitle(self):
return "Test Google Login"
def beforeTearDown(self):
GoogleLoginUtility.getAccessTokenFromCode = GoogleLoginUtility_getAccessTokenFromCode
GoogleLoginUtility.getUserEntry = GoogleLoginUtility_getUserEntry
def afterSetUp(self):
"""
This is ran before anything, used to set the environment
"""
# Patch extension to avoid external connection
GoogleLoginUtility.getUserId = getUserId
GoogleLoginUtility.getAccessTokenFromCode = getAccessTokenFromCode
GoogleLoginUtility.getUserEntry = getUserEntry
self.dummy_user_id = "dummy"
self.dummy_connector_id = "test_google_connector"
person_module = self.portal.person_module
if getattr(person_module, self.dummy_user_id, None) is None:
person = person_module.newContent(first_name="Dummy",
id=self.dummy_user_id,
reference=self.dummy_user_id,
user_id=self.dummy_user_id
)
assignment = person.newContent(portal_type="Assignment")
assignment.open()
login = person.newContent(portal_type="ERP5 Login", reference=self.dummy_user_id)
login.validate()
person.validate()
self.tic()
portal_catalog = self.portal.portal_catalog
for obj in portal_catalog(portal_type=["Google Login", "Person"],
reference=getUserId(None),
validation_state="validated"):
obj.getObject().invalidate()
uuid_str = uuid.uuid4().hex
obj.setReference(uuid_str)
obj.setUserId(uuid_str)
for connector in portal_catalog(portal_type="Google Connector",
validation_state="validated",
id="NOT %s" % self.dummy_connector_id,
reference="default"):
connector.invalidate()
if getattr(self.portal.portal_oauth, self.dummy_connector_id, None) is None:
connector = self.portal.portal_oauth.newContent(id=self.dummy_connector_id,
portal_type="Google Connector",
reference="default",
client_id=CLIENT_ID,
secret_key=SECRET_KEY)
connector.validate()
self.tic()
def test_redirect(self):
"""
Check URL generate to redirect to Google
"""
self.logout()
self.portal.ERP5Site_redirectToGoogleLoginPage()
location = self.portal.REQUEST.RESPONSE.getHeader("Location")
self.assertIn("https://accounts.google.com/o/oauth2/", location)
self.assertIn("response_type=code", location)
self.assertIn("client_id=%s" % CLIENT_ID, location)
self.assertNotIn("secret_key=", location)
self.assertIn("ERP5Site_receiveGoogleCallback", location)
def test_create_user_in_ERP5Site_createGoogleUserToOAuth(self):
"""
Check if ERP5 set cookie properly after receive code from external service
"""
self.login()
id_list = []
for result in self.portal.portal_catalog(portal_type="Credential Request",
reference=getUserId(None)):
id_list.append(result.getObject().getId())
self.portal.credential_request_module.manage_delObjects(ids=id_list)
skin = self.portal.portal_skins.custom
createZODBPythonScript(skin, "CredentialRequest_createUser", "", """
person = context.getDestinationDecisionValue(portal_type="Person")
login_list = [x for x in person.objectValues(portal_type='Google Login') \
if x.getValidationState() == 'validated']
if len(login_list):
login = login_list[0]
else:
login = person.newContent(portal_type='Google Login')
reference = context.getReference()
if not login.hasReference():
if not reference:
raise ValueError("Impossible to create an account without login")
login.setReference(reference)
if not person.Person_getUserId():
person.setUserId(reference)
if login.getValidationState() == 'draft':
login.validate()
return reference, None
""")
createZODBPythonScript(skin, "ERP5Site_createGoogleUserToOAuth", "user_reference, user_dict", """
module = context.getPortalObject().getDefaultModule(portal_type='Credential Request')
credential_request = module.newContent(
portal_type="Credential Request",
first_name=user_dict["first_name"],
last_name=user_dict["last_name"],
reference=user_reference,
default_email_text=user_dict["email"],
)
credential_request.submit()
context.portal_alarms.accept_submitted_credentials.activeSense()
return credential_request
""")
self.logout()
response = self.portal.ERP5Site_receiveGoogleCallback(code=CODE)
google_hash = self.portal.REQUEST.RESPONSE.cookies.get("__ac_google_hash")["value"]
self.assertEqual("b01533abb684a658dc71c81da4e67546", google_hash)
self.assertEqual(self.portal.absolute_url(), response)
self.tic()
self.login()
credential_request = self.portal.portal_catalog(portal_type="Credential Request",
reference=getUserId(None))[0].getObject()
credential_request.accept()
person = credential_request.getDestinationDecisionValue()
google_login = person.objectValues(portal_types="Google Login")[0]
self.assertEqual(getUserId(None), google_login.getReference())
<?xml version="1.0"?>
<ZopeData>
<record id="1" aka="AAAAAAAAAAE=">
<pickle>
<global name="Test Component" module="erp5.portal_type"/>
</pickle>
<pickle>
<dictionary>
<item>
<key> <string>_recorded_property_dict</string> </key>
<value>
<persistent> <string encoding="base64">AAAAAAAAAAI=</string> </persistent>
</value>
</item>
<item>
<key> <string>default_reference</string> </key>
<value> <string>testGoogleLogin</string> </value>
</item>
<item>
<key> <string>description</string> </key>
<value>
<none/>
</value>
</item>
<item>
<key> <string>id</string> </key>
<value> <string>test.erp5.testGoogleLogin</string> </value>
</item>
<item>
<key> <string>portal_type</string> </key>
<value> <string>Test Component</string> </value>
</item>
<item>
<key> <string>sid</string> </key>
<value>
<none/>
</value>
</item>
<item>
<key> <string>text_content_error_message</string> </key>
<value>
<tuple/>
</value>
</item>
<item>
<key> <string>text_content_warning_message</string> </key>
<value>
<tuple/>
</value>
</item>
<item>
<key> <string>version</string> </key>
<value> <string>erp5</string> </value>
</item>
<item>
<key> <string>workflow_history</string> </key>
<value>
<persistent> <string encoding="base64">AAAAAAAAAAM=</string> </persistent>
</value>
</item>
</dictionary>
</pickle>
</record>
<record id="2" aka="AAAAAAAAAAI=">
<pickle>
<global name="PersistentMapping" module="Persistence.mapping"/>
</pickle>
<pickle>
<dictionary>
<item>
<key> <string>data</string> </key>
<value>
<dictionary/>
</value>
</item>
</dictionary>
</pickle>
</record>
<record id="3" aka="AAAAAAAAAAM=">
<pickle>
<global name="PersistentMapping" module="Persistence.mapping"/>
</pickle>
<pickle>
<dictionary>
<item>
<key> <string>data</string> </key>
<value>
<dictionary>
<item>
<key> <string>component_validation_workflow</string> </key>
<value>
<persistent> <string encoding="base64">AAAAAAAAAAQ=</string> </persistent>
</value>
</item>
</dictionary>
</value>
</item>
</dictionary>
</pickle>
</record>
<record id="4" aka="AAAAAAAAAAQ=">
<pickle>
<global name="WorkflowHistoryList" module="Products.ERP5Type.patches.WorkflowTool"/>
</pickle>
<pickle>
<tuple>
<none/>
<list>
<dictionary>
<item>
<key> <string>action</string> </key>
<value> <string>validate</string> </value>
</item>
<item>
<key> <string>validation_state</string> </key>
<value> <string>validated</string> </value>
</item>
</dictionary>
</list>
</tuple>
</pickle>
</record>
</ZopeData>
erp5_bearer_token
erp5_oauth
\ No newline at end of file
Google Connector | view
Google Login | view
extension.erp5.GoogleLoginUtility
\ No newline at end of file
portal_caches/google_server_auth_token_cache_factory
portal_caches/google_server_auth_token_cache_factory/**
\ No newline at end of file
OAuth Tool | Google Connector
Person | Google Login
\ No newline at end of file
Google Connector
Google Login
\ No newline at end of file
Google Connector | OAuthClient
Template Tool | TemplateToolERP5GoogleExtractionPluginConstraint
Google Connector | edit_workflow
Google Connector | validation_workflow
Google Login | edit_workflow
Google Login | validation_workflow
\ No newline at end of file
erp5_oauth_google_login
\ No newline at end of file
test.erp5.testGoogleLogin
\ No newline at end of file
erp5_full_text_myisam_catalog
erp5_credential
\ No newline at end of file
erp5_oauth_google_login
\ No newline at end of file
...@@ -11,4 +11,8 @@ REQUEST = portal.REQUEST ...@@ -11,4 +11,8 @@ REQUEST = portal.REQUEST
if REQUEST.has_key('portal_skin'): if REQUEST.has_key('portal_skin'):
portal.portal_skins.clearSkinCookie() portal.portal_skins.clearSkinCookie()
REQUEST.RESPONSE.expireCookie('__ac', path='/') REQUEST.RESPONSE.expireCookie('__ac', path='/')
if getattr(portal.portal_skins, "erp5_oauth_google_login", None):
REQUEST.RESPONSE.expireCookie('__ac_google_hash', path='/')
return REQUEST.RESPONSE.redirect(REQUEST.URL1 + '/logged_out') return REQUEST.RESPONSE.redirect(REQUEST.URL1 + '/logged_out')
oauth_login_list = []
portal_skin = context.getPortalObject().portal_skins
if getattr(portal_skin, "erp5_oauth_google_login", None) is not None:
oauth_login_list.append("google")
return oauth_login_list
<?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>_params</string> </key>
<value> <string></string> </value>
</item>
<item>
<key> <string>id</string> </key>
<value> <string>ERP5Site_getAvailableOAuthLoginList</string> </value>
</item>
</dictionary>
</pickle>
</record>
</ZopeData>
...@@ -1103,3 +1103,7 @@ div.pdf-preview-navigation img.last{ ...@@ -1103,3 +1103,7 @@ div.pdf-preview-navigation img.last{
fieldset > div.large-gadget { fieldset > div.large-gadget {
height: 85vh; height: 85vh;
} }
a.zocial {
margin-top: 10px;
}
...@@ -3,6 +3,9 @@ ...@@ -3,6 +3,9 @@
xmlns:i18n="http://xml.zope.org/namespaces/i18n"> xmlns:i18n="http://xml.zope.org/namespaces/i18n">
<tal:block tal:define="form_action string:logged_in; <tal:block tal:define="form_action string:logged_in;
global form_id string:login_form; global form_id string:login_form;
available_oauth_login_list python: context.getPortalObject().ERP5Site_getAvailableOAuthLoginList();
enable_google_login python: 'google' in available_oauth_login_list;
css_list python: enable_google_login and ['%s/zocial.min.css' % here.portal_url()] or [];
js_list python: ['%s/login_form.js' % (here.portal_url(), ), '%s/erp5.js' % (here.portal_url(), )]"> js_list python: ['%s/login_form.js' % (here.portal_url(), ), '%s/erp5.js' % (here.portal_url(), )]">
<tal:block metal:use-macro="here/main_template/macros/master"> <tal:block metal:use-macro="here/main_template/macros/master">
<tal:block metal:fill-slot="main"> <tal:block metal:fill-slot="main">
...@@ -46,6 +49,15 @@ ...@@ -46,6 +49,15 @@
i18n:translate="" i18n:domain="ui">I forgot my password!</a> i18n:translate="" i18n:domain="ui">I forgot my password!</a>
</div> </div>
</div> </div>
<tal:block tal:condition="enable_google_login">
<div class="field">
<label>&nbsp;</label>
<div class="input">
<a tal:attributes="href string:${here/portal_url}/ERP5Site_redirectToGoogleLoginPage"
i18n:translate="" i18n:domain="ui" class="zocial google">Login with Google</a>
</div>
</div>
</tal:block>
</fieldset> </fieldset>
<script type="text/javascript">setFocus()</script> <script type="text/javascript">setFocus()</script>
<p i18n:translate="" i18n:domain="ui">Having trouble logging in? Make sure to enable cookies in your web browser.</p> <p i18n:translate="" i18n:domain="ui">Having trouble logging in? Make sure to enable cookies in your web browser.</p>
......
@charset "UTF-8";/*!
Zocial Butons
http://zocial.smcllns.com
by Sam Collins (@smcllns)
License: http://opensource.org/licenses/mit-license.php
You are free to use and modify, as long as you keep this license comment intact or link back to zocial.smcllns.com on your site.
*/.zocial,a.zocial{border:1px solid #777;border-color:rgba(0,0,0,0.2);border-bottom-color:#333;border-bottom-color:rgba(0,0,0,0.4);color:#fff;-moz-box-shadow:inset 0 .08em 0 rgba(255,255,255,0.4),inset 0 0 .1em rgba(255,255,255,0.9);-webkit-box-shadow:inset 0 .08em 0 rgba(255,255,255,0.4),inset 0 0 .1em rgba(255,255,255,0.9);box-shadow:inset 0 .08em 0 rgba(255,255,255,0.4),inset 0 0 .1em rgba(255,255,255,0.9);cursor:pointer;display:inline-block;font:bold 100%/2.1 "Lucida Grande",Tahoma,sans-serif;padding:0 .95em 0 0;text-align:center;text-decoration:none;text-shadow:0 1px 0 rgba(0,0,0,0.5);white-space:nowrap;-moz-user-select:none;-webkit-user-select:none;user-select:none;position:relative;-moz-border-radius:.3em;-webkit-border-radius:.3em;border-radius:.3em}.zocial:before{content:"";border-right:.075em solid rgba(0,0,0,0.1);float:left;font:120%/1.65 zocial;font-style:normal;font-weight:normal;margin:0 .5em 0 0;padding:0 .5em;text-align:center;text-decoration:none;text-transform:none;-moz-box-shadow:.075em 0 0 rgba(255,255,255,0.25);-webkit-box-shadow:.075em 0 0 rgba(255,255,255,0.25);box-shadow:.075em 0 0 rgba(255,255,255,0.25);-moz-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;-webkit-font-smoothing:antialiased;font-smoothing:antialiased}.zocial:active{outline:0}.zocial:hover,.zocial:focus{color:#fff}.zocial.icon{overflow:hidden;max-width:2.4em;padding-left:0;padding-right:0;max-height:2.15em;white-space:nowrap}.zocial.icon:before{padding:0;width:2em;height:2em;box-shadow:none;border:0}.zocial{background-image:-moz-linear-gradient(rgba(255,255,255,.1),rgba(255,255,255,.05) 49%,rgba(0,0,0,.05) 51%,rgba(0,0,0,.1));background-image:-ms-linear-gradient(rgba(255,255,255,.1),rgba(255,255,255,.05) 49%,rgba(0,0,0,.05) 51%,rgba(0,0,0,.1));background-image:-o-linear-gradient(rgba(255,255,255,.1),rgba(255,255,255,.05) 49%,rgba(0,0,0,.05) 51%,rgba(0,0,0,.1));background-image:-webkit-gradient(linear,left top,left bottom,from(rgba(255,255,255,.1)),color-stop(49%,rgba(255,255,255,.05)),color-stop(51%,rgba(0,0,0,.05)),to(rgba(0,0,0,.1)));background-image:-webkit-linear-gradient(rgba(255,255,255,.1),rgba(255,255,255,.05) 49%,rgba(0,0,0,.05) 51%,rgba(0,0,0,.1));background-image:linear-gradient(rgba(255,255,255,.1),rgba(255,255,255,.05) 49%,rgba(0,0,0,.05) 51%,rgba(0,0,0,.1))}.zocial:hover,.zocial:focus{background-image:-moz-linear-gradient(rgba(255,255,255,.15) 49%,rgba(0,0,0,.1) 51%,rgba(0,0,0,.15));background-image:-ms-linear-gradient(rgba(255,255,255,.15) 49%,rgba(0,0,0,.1) 51%,rgba(0,0,0,.15));background-image:-o-linear-gradient(rgba(255,255,255,.15) 49%,rgba(0,0,0,.1) 51%,rgba(0,0,0,.15));background-image:-webkit-gradient(linear,left top,left bottom,from(rgba(255,255,255,.15)),color-stop(49%,rgba(255,255,255,.15)),color-stop(51%,rgba(0,0,0,.1)),to(rgba(0,0,0,.15)));background-image:-webkit-linear-gradient(rgba(255,255,255,.15) 49%,rgba(0,0,0,.1) 51%,rgba(0,0,0,.15));background-image:linear-gradient(rgba(255,255,255,.15) 49%,rgba(0,0,0,.1) 51%,rgba(0,0,0,.15))}.zocial:active{background-image:-moz-linear-gradient(bottom,rgba(255,255,255,.1),rgba(255,255,255,0) 30%,transparent 50%,rgba(0,0,0,.1));background-image:-ms-linear-gradient(bottom,rgba(255,255,255,.1),rgba(255,255,255,0) 30%,transparent 50%,rgba(0,0,0,.1));background-image:-o-linear-gradient(bottom,rgba(255,255,255,.1),rgba(255,255,255,0) 30%,transparent 50%,rgba(0,0,0,.1));background-image:-webkit-gradient(linear,left top,left bottom,from(rgba(255,255,255,.1)),color-stop(30%,rgba(255,255,255,0)),color-stop(50%,transparent),to(rgba(0,0,0,.1)));background-image:-webkit-linear-gradient(bottom,rgba(255,255,255,.1),rgba(255,255,255,0) 30%,transparent 50%,rgba(0,0,0,.1));background-image:linear-gradient(bottom,rgba(255,255,255,.1),rgba(255,255,255,0) 30%,transparent 50%,rgba(0,0,0,.1))}.zocial.acrobat,.zocial.bitcoin,.zocial.cloudapp,.zocial.dropbox,.zocial.email,.zocial.eventful,.zocial.github,.zocial.gmail,.zocial.instapaper,.zocial.itunes,.zocial.ninetyninedesigns,.zocial.openid,.zocial.plancast,.zocial.pocket,.zocial.posterous,.zocial.reddit,.zocial.secondary,.zocial.stackoverflow,.zocial.viadeo,.zocial.weibo,.zocial.wikipedia{border:1px solid #aaa;border-color:rgba(0,0,0,0.3);border-bottom-color:#777;border-bottom-color:rgba(0,0,0,0.5);-moz-box-shadow:inset 0 .08em 0 rgba(255,255,255,0.7),inset 0 0 .08em rgba(255,255,255,0.5);-webkit-box-shadow:inset 0 .08em 0 rgba(255,255,255,0.7),inset 0 0 .08em rgba(255,255,255,0.5);box-shadow:inset 0 .08em 0 rgba(255,255,255,0.7),inset 0 0 .08em rgba(255,255,255,0.5);text-shadow:0 1px 0 rgba(255,255,255,0.8)}.zocial.acrobat:focus,.zocial.acrobat:hover,.zocial.bitcoin:focus,.zocial.bitcoin:hover,.zocial.dropbox:focus,.zocial.dropbox:hover,.zocial.email:focus,.zocial.email:hover,.zocial.eventful:focus,.zocial.eventful:hover,.zocial.github:focus,.zocial.github:hover,.zocial.gmail:focus,.zocial.gmail:hover,.zocial.instapaper:focus,.zocial.instapaper:hover,.zocial.itunes:focus,.zocial.itunes:hover,.zocial.ninetyninedesigns:focus,.zocial.ninetyninedesigns:hover,.zocial.openid:focus,.zocial.openid:hover,.zocial.plancast:focus,.zocial.plancast:hover,.zocial.pocket:focus,.zocial.pocket:hover,.zocial.posterous:focus,.zocial.posterous:hover,.zocial.reddit:focus,.zocial.reddit:hover,.zocial.secondary:focus,.zocial.secondary:hover,.zocial.stackoverflow:focus,.zocial.stackoverflow:hover,.zocial.twitter:focus,.zocial.viadeo:focus,.zocial.viadeo:hover,.zocial.weibo:focus,.zocial.weibo:hover,.zocial.wikipedia:focus,.zocial.wikipedia:hover{background-image:-webkit-gradient(linear,left top,left bottom,from(rgba(255,255,255,0.5)),color-stop(49%,rgba(255,255,255,0.2)),color-stop(51%,rgba(0,0,0,0.05)),to(rgba(0,0,0,0.15)));background-image:-moz-linear-gradient(top,rgba(255,255,255,0.5),rgba(255,255,255,0.2) 49%,rgba(0,0,0,0.05) 51%,rgba(0,0,0,0.15));background-image:-webkit-linear-gradient(top,rgba(255,255,255,0.5),rgba(255,255,255,0.2) 49%,rgba(0,0,0,0.05) 51%,rgba(0,0,0,0.15));background-image:-o-linear-gradient(top,rgba(255,255,255,0.5),rgba(255,255,255,0.2) 49%,rgba(0,0,0,0.05) 51%,rgba(0,0,0,0.15));background-image:-ms-linear-gradient(top,rgba(255,255,255,0.5),rgba(255,255,255,0.2) 49%,rgba(0,0,0,0.05) 51%,rgba(0,0,0,0.15));background-image:linear-gradient(top,rgba(255,255,255,0.5),rgba(255,255,255,0.2) 49%,rgba(0,0,0,0.05) 51%,rgba(0,0,0,0.15))}.zocial.acrobat:active,.zocial.bitcoin:active,.zocial.dropbox:active,.zocial.email:active,.zocial.eventful:active,.zocial.github:active,.zocial.gmail:active,.zocial.instapaper:active,.zocial.itunes:active,.zocial.ninetyninedesigns:active,.zocial.openid:active,.zocial.plancast:active,.zocial.pocket:active,.zocial.posterous:active,.zocial.reddit:active,.zocial.secondary:active,.zocial.stackoverflow:active,.zocial.viadeo:active,.zocial.weibo:active,.zocial.wikipedia:active{background-image:-webkit-gradient(linear,left top,left bottom,from(rgba(255,255,255,0)),color-stop(30%,rgba(255,255,255,0)),color-stop(50%,rgba(0,0,0,0)),to(rgba(0,0,0,0.1)));background-image:-moz-linear-gradient(bottom,rgba(255,255,255,0),rgba(255,255,255,0) 30%,rgba(0,0,0,0) 50%,rgba(0,0,0,0.1));background-image:-webkit-linear-gradient(bottom,rgba(255,255,255,0),rgba(255,255,255,0) 30%,rgba(0,0,0,0) 50%,rgba(0,0,0,0.1));background-image:-o-linear-gradient(bottom,rgba(255,255,255,0),rgba(255,255,255,0) 30%,rgba(0,0,0,0) 50%,rgba(0,0,0,0.1));background-image:-ms-linear-gradient(bottom,rgba(255,255,255,0),rgba(255,255,255,0) 30%,rgba(0,0,0,0) 50%,rgba(0,0,0,0.1));background-image:linear-gradient(bottom,rgba(255,255,255,0),rgba(255,255,255,0) 30%,rgba(0,0,0,0) 50%,rgba(0,0,0,0.1))}.zocial.acrobat:before{content:"\f100"}.zocial.amazon:before{content:"\f101"}.zocial.android:before{content:"\f102"}.zocial.angellist:before{content:"\f103"}.zocial.aol:before{content:"\f104"}.zocial.appnet:before{content:"\f105"}.zocial.appstore:before{content:"\f106"}.zocial.bitbucket:before{content:"\f107"}.zocial.bitcoin:before{content:"\f108"}.zocial.blogger:before{content:"\f109"}.zocial.buffer:before{content:"\f10a"}.zocial.cal:before{content:"\f10b"}.zocial.call:before{content:"\f10c"}.zocial.cart:before{content:"\f10d"}.zocial.chrome:before{content:"\f10e"}.zocial.cloudapp:before{content:"\f10f"}.zocial.creativecommons:before{content:"\f110"}.zocial.delicious:before{content:"\f111"}.zocial.digg:before{content:"\f112"}.zocial.disqus:before{content:"\f113"}.zocial.dribbble:before{content:"\f114"}.zocial.dropbox:before{content:"\f115"}.zocial.drupal:before{content:"\f116"}.zocial.dwolla:before{content:"\f118"}.zocial.email:before{content:"\f119"}.zocial.eventasaurus:before{content:"\f11a"}.zocial.eventbrite:before{content:"\f11b"}.zocial.eventful:before{content:"\f11c"}.zocial.evernote:before{content:"\f11d"}.zocial.facebook:before{content:"\f11e"}.zocial.fivehundredpx:before{content:"\f11f"}.zocial.flattr:before{content:"\f120"}.zocial.flickr:before{content:"\f121"}.zocial.forrst:before{content:"\f122"}.zocial.foursquare:before{content:"\f123"}.zocial.github:before{content:"\f124"}.zocial.gmail:before{content:"\f125"}.zocial.google:before{content:"\f126"}.zocial.googleplay:before{content:"\f127"}.zocial.googleplus:before{content:"\f128"}.zocial.gowalla:before{content:"\f129"}.zocial.grooveshark:before{content:"\f12a"}.zocial.guest:before{content:"\f12b"}.zocial.html5:before{content:"\f12c"}.zocial.ie:before{content:"\f12d"}.zocial.instagram:before{content:"\f12e"}.zocial.instapaper:before{content:"\f12f"}.zocial.intensedebate:before{content:"\f130"}.zocial.itunes:before{content:"\f131"}.zocial.joinme:before{content:"\f165"}.zocial.klout:before{content:"\f132"}.zocial.lanyrd:before{content:"\f133"}.zocial.lastfm:before{content:"\f134"}.zocial.lego:before{content:"\f135"}.zocial.linkedin:before{content:"\f136"}.zocial.lkdto:before{content:"\f137"}.zocial.logmein:before{content:"\f138"}.zocial.macstore:before{content:"\f139"}.zocial.meetup:before{content:"\f13a"}.zocial.myspace:before{content:"\f13b"}.zocial.ninetyninedesigns:before{content:"\f13c"}.zocial.openid:before{content:"\f13d"}.zocial.opentable:before{content:"\f13e"}.zocial.paypal:before{content:"\f13f"}.zocial.persona:before{content:"\f164"}.zocial.pinboard:before{content:"\f140"}.zocial.pinterest:before{content:"\f141"}.zocial.plancast:before{content:"\f142"}.zocial.plurk:before{content:"\f143"}.zocial.pocket:before{content:"\f144"}.zocial.podcast:before{content:"\f145"}.zocial.posterous:before{content:"\f146"}.zocial.print:before{content:"\f147"}.zocial.quora:before{content:"\f148"}.zocial.reddit:before{content:"\f149"}.zocial.rss:before{content:"\f14a"}.zocial.scribd:before{content:"\f14b"}.zocial.skype:before{content:"\f14c"}.zocial.smashing:before{content:"\f14d"}.zocial.songkick:before{content:"\f14e"}.zocial.soundcloud:before{content:"\f14f"}.zocial.spotify:before{content:"\f150"}.zocial.stackoverflow:before{content:"\f151"}.zocial.statusnet:before{content:"\f152"}.zocial.steam:before{content:"\f153"}.zocial.stripe:before{content:"\f154"}.zocial.stumbleupon:before{content:"\f155"}.zocial.tumblr:before{content:"\f156"}.zocial.twitch:before{content:"\f166"}.zocial.twitter:before{content:"\f157"}.zocial.viadeo:before{content:"\f158"}.zocial.vimeo:before{content:"\f159"}.zocial.vk:before{content:"\f15a"}.zocial.weibo:before{content:"\f15b"}.zocial.wikipedia:before{content:"\f15c"}.zocial.windows:before{content:"\f15d"}.zocial.wordpress:before{content:"\f15e"}.zocial.xing:before{content:"\f15f"}.zocial.yahoo:before{content:"\f160"}.zocial.ycombinator:before{content:"\f161"}.zocial.yelp:before{content:"\f162"}.zocial.youtube:before{content:"\f163"}.zocial.acrobat:before{color:#fb0000}.zocial.bitcoin:before{color:#f7931a}.zocial.dropbox:before{color:#1f75cc}.zocial.drupal:before{color:#fff}.zocial.email:before{color:#312c2a}.zocial.eventasaurus:before{color:#9de428}.zocial.eventful:before{color:#06c}.zocial.fivehundredpx:before{color:#29b6ff}.zocial.forrst:before{color:#50894f}.zocial.gmail:before{color:red}.zocial.itunes:before{color:#1a6dd2}.zocial.lego:before{color:#fff900}.zocial.ninetyninedesigns:before{color:#f50}.zocial.openid:before{color:#ff921d}.zocial.pocket:before{color:#ee4056}.zocial.persona:before{color:#fff}.zocial.reddit:before{color:red}.zocial.scribd:before{color:#00d5ea}.zocial.stackoverflow:before{color:#ff7a15}.zocial.statusnet:before{color:#fff}.zocial.viadeo:before{color:#f59b20}.zocial.weibo:before{color:#e6162d}.zocial.acrobat{background-color:#fff;color:#000}.zocial.amazon{background-color:#ffad1d;color:#030037;text-shadow:0 1px 0 rgba(255,255,255,0.5)}.zocial.android{background-color:#a4c639}.zocial.angellist{background-color:#000}.zocial.aol{background-color:red}.zocial.appnet{background-color:#3178bd}.zocial.appstore{background-color:#000}.zocial.bitbucket{background-color:#205081}.zocial.bitcoin{background-color:#efefef;color:#4d4d4d}.zocial.blogger{background-color:#ee5a22}.zocial.buffer{background-color:#232323}.zocial.call{background-color:#008000}.zocial.cal{background-color:#d63538}.zocial.cart{background-color:#333}.zocial.chrome{background-color:#006cd4}.zocial.cloudapp{background-color:#fff;color:#312c2a}.zocial.creativecommons{background-color:#000}.zocial.delicious{background-color:#3271cb}.zocial.digg{background-color:#164673}.zocial.disqus{background-color:#5d8aad}.zocial.dribbble{background-color:#ea4c89}.zocial.dropbox{background-color:#fff;color:#312c2a}.zocial.drupal{background-color:#0077c0;color:#fff}.zocial.dwolla{background-color:#e88c02}.zocial.email{background-color:#f0f0eb;color:#312c2a}.zocial.eventasaurus{background-color:#192931;color:#fff}.zocial.eventbrite{background-color:#ff5616}.zocial.eventful{background-color:#fff;color:#47ab15}.zocial.evernote{background-color:#6bb130;color:#fff}.zocial.facebook{background-color:#4863ae}.zocial.fivehundredpx{background-color:#333}.zocial.flattr{background-color:#8aba42}.zocial.flickr{background-color:#ff0084}.zocial.forrst{background-color:#1e360d}.zocial.foursquare{background-color:#44a8e0}.zocial.github{background-color:#fbfbfb;color:#050505}.zocial.gmail{background-color:#efefef;color:#222}.zocial.google{background-color:#4e6cf7}.zocial.googleplay{background-color:#000}.zocial.googleplus{background-color:#dd4b39}.zocial.gowalla{background-color:#ff720a}.zocial.grooveshark{background-color:#111;color:#eee}.zocial.guest{background-color:#1b4d6d}.zocial.html5{background-color:#ff3617}.zocial.ie{background-color:#00a1d9}.zocial.instapaper{background-color:#eee;color:#222}.zocial.instagram{background-color:#3f729b}.zocial.intensedebate{background-color:#0099e1}.zocial.klout{background-color:#e34a25}.zocial.itunes{background-color:#efefeb;color:#312c2a}.zocial.lanyrd{background-color:#2e6ac2}.zocial.lastfm{background-color:#dc1a23}.zocial.lego{background-color:#fb0000}.zocial.linkedin{background-color:#0083a8}.zocial.lkdto{background-color:#7c786f}.zocial.logmein{background-color:#000}.zocial.macstore{background-color:#007dcb}.zocial.meetup{background-color:#ff0026}.zocial.myspace{background-color:#000}.zocial.ninetyninedesigns{background-color:#fff;color:#072243}.zocial.openid{background-color:#f5f5f5;color:#333}.zocial.opentable{background-color:#900}.zocial.paypal{background-color:#fff;color:#32689a;text-shadow:0 1px 0 rgba(255,255,255,0.5)}.zocial.persona{background-color:#1258a1;color:#fff}.zocial.pinboard{background-color:blue}.zocial.pinterest{background-color:#c91618}.zocial.plancast{background-color:#e7ebed;color:#333}.zocial.plurk{background-color:#cf682f}.zocial.pocket{background-color:#fff;color:#777}.zocial.podcast{background-color:#9365ce}.zocial.posterous{background-color:#ffd959;color:#bc7134}.zocial.print{background-color:#f0f0eb;color:#222;text-shadow:0 1px 0 rgba(255,255,255,0.8)}.zocial.quora{background-color:#a82400}.zocial.reddit{background-color:#fff;color:#222}.zocial.rss{background-color:#ff7f25}.zocial.scribd{background-color:#231c1a}.zocial.skype{background-color:#00a2ed}.zocial.smashing{background-color:#ff4f27}.zocial.songkick{background-color:#ff0050}.zocial.soundcloud{background-color:#ff4500}.zocial.spotify{background-color:#60af00}.zocial.stackoverflow{background-color:#fff;color:#555}.zocial.statusnet{background-color:#829d25}.zocial.steam{background-color:#000}.zocial.stripe{background-color:#2f7ed6}.zocial.stumbleupon{background-color:#eb4924}.zocial.tumblr{background-color:#374a61}.zocial.twitter{background-color:#46c0fb}.zocial.twitch{background-color:#6441a5}.zocial.viadeo{background-color:#fff;color:#000}.zocial.vimeo{background-color:#00a2cd}.zocial.vk{background-color:#45688e}.zocial.weibo{background-color:#faf6f1;color:#000}.zocial.wikipedia{background-color:#fff;color:#000}.zocial.windows{background-color:#0052a4;color:#fff}.zocial.wordpress{background-color:#464646}.zocial.xing{background-color:#0a5d5e}.zocial.yahoo{background-color:#a200c2}.zocial.ycombinator{background-color:#f60}.zocial.yelp{background-color:#e60010}.zocial.youtube{background-color:red}.zocial.primary,.zocial.secondary{margin:.1em 0;padding:0 1em}.zocial.primary:before,.zocial.secondary:before{display:none}.zocial.primary{background-color:#333}.zocial.secondary{background-color:#f0f0eb;color:#222;text-shadow:0 1px 0 rgba(255,255,255,0.8)}button:-moz-focus-inner{border:0;padding:0}@font-face{font-family:"zocial";src:url("./zocial.eot");src:url("./zocial.eot?#iefix") format("embedded-opentype"),url(data:application/x-font-woff;charset=utf-8;base64,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),url("./zocial.woff") format("woff"),url("./zocial.ttf") format("truetype"),url("./zocial.svg#zocial") format("svg");font-weight:normal;font-style:normal}@media screen and (-webkit-min-device-pixel-ratio:0){@font-face{font-family:"zocial";src:url("./zocial.svg#zocial") format("svg")}}
\ No newline at end of file
<?xml version="1.0"?>
<ZopeData>
<record id="1" aka="AAAAAAAAAAE=">
<pickle>
<global name="File" module="OFS.Image"/>
</pickle>
<pickle>
<dictionary>
<item>
<key> <string>__name__</string> </key>
<value> <string>zocial.min.css</string> </value>
</item>
<item>
<key> <string>content_type</string> </key>
<value> <string>text/css</string> </value>
</item>
<item>
<key> <string>precondition</string> </key>
<value> <string></string> </value>
</item>
<item>
<key> <string>title</string> </key>
<value> <string>zocial.min.css</string> </value>
</item>
</dictionary>
</pickle>
</record>
</ZopeData>
...@@ -34,11 +34,14 @@ from Products.PluggableAuthService.interfaces import plugins ...@@ -34,11 +34,14 @@ from Products.PluggableAuthService.interfaces import plugins
from Products.PluggableAuthService.utils import classImplements from Products.PluggableAuthService.utils import classImplements
from Products.PluggableAuthService.plugins.BasePlugin import BasePlugin from Products.PluggableAuthService.plugins.BasePlugin import BasePlugin
from Products import ERP5Security from Products import ERP5Security
from Products.PluggableAuthService.PluggableAuthService import DumbHTTPExtractor
from AccessControl.SecurityManagement import getSecurityManager, \ from AccessControl.SecurityManagement import getSecurityManager, \
setSecurityManager, newSecurityManager setSecurityManager, newSecurityManager
from Products.ERP5Type.Cache import DEFAULT_CACHE_SCOPE from Products.ERP5Type.Cache import DEFAULT_CACHE_SCOPE
import time
import socket import socket
import httplib
import urllib
import json
from zLOG import LOG, ERROR, INFO from zLOG import LOG, ERROR, INFO
try: try:
...@@ -58,6 +61,30 @@ manage_addERP5FacebookExtractionPluginForm = PageTemplateFile( ...@@ -58,6 +61,30 @@ manage_addERP5FacebookExtractionPluginForm = PageTemplateFile(
'www/ERP5Security_addERP5FacebookExtractionPlugin', globals(), 'www/ERP5Security_addERP5FacebookExtractionPlugin', globals(),
__name__='manage_addERP5FacebookExtractionPluginForm') __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[k[1]].encode('utf-8')
user_entry[k[0]] = value
return user_entry
def addERP5FacebookExtractionPlugin(dispatcher, id, title=None, REQUEST=None): def addERP5FacebookExtractionPlugin(dispatcher, id, title=None, REQUEST=None):
""" Add a ERP5FacebookExtractionPlugin to a Pluggable Auth Service. """ """ Add a ERP5FacebookExtractionPlugin to a Pluggable Auth Service. """
...@@ -91,7 +118,7 @@ def addERP5GoogleExtractionPlugin(dispatcher, id, title=None, REQUEST=None): ...@@ -91,7 +118,7 @@ def addERP5GoogleExtractionPlugin(dispatcher, id, title=None, REQUEST=None):
class ERP5ExternalOauth2ExtractionPlugin: class ERP5ExternalOauth2ExtractionPlugin:
cache_factory_name = 'extrenal_oauth2_token_cache_factory' cache_factory_name = 'external_oauth2_token_cache_factory'
security = ClassSecurityInfo() security = ClassSecurityInfo()
def __init__(self, id, title=None): def __init__(self, id, title=None):
...@@ -115,7 +142,7 @@ class ERP5ExternalOauth2ExtractionPlugin: ...@@ -115,7 +142,7 @@ class ERP5ExternalOauth2ExtractionPlugin:
cache_tool.updateCache() cache_tool.updateCache()
cache_factory = cache_tool.getRamCacheRoot().get(self.cache_factory_name) cache_factory = cache_tool.getRamCacheRoot().get(self.cache_factory_name)
if cache_factory is None: if cache_factory is None:
raise KeyError raise KeyError("Cache factory %s not found" % self.cache_factory_name)
return cache_factory return cache_factory
def setToken(self, key, body): def setToken(self, key, body):
...@@ -130,7 +157,13 @@ class ERP5ExternalOauth2ExtractionPlugin: ...@@ -130,7 +157,13 @@ class ERP5ExternalOauth2ExtractionPlugin:
for cache_plugin in cache_factory.getCachePluginList(): for cache_plugin in cache_factory.getCachePluginList():
cache_entry = cache_plugin.get(key, DEFAULT_CACHE_SCOPE) cache_entry = cache_plugin.get(key, DEFAULT_CACHE_SCOPE)
if cache_entry is not None: if cache_entry is not None:
return cache_entry.getValue() # 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) raise KeyError('Key %r not found' % key)
#################################### ####################################
...@@ -139,69 +172,53 @@ class ERP5ExternalOauth2ExtractionPlugin: ...@@ -139,69 +172,53 @@ class ERP5ExternalOauth2ExtractionPlugin:
security.declarePrivate('extractCredentials') security.declarePrivate('extractCredentials')
def extractCredentials(self, request): def extractCredentials(self, request):
""" Extract Oauth2 credentials from the request header. """ """ Extract Oauth2 credentials from the request header. """
Base_createOauth2User = getattr(self.getPortalObject(), user_dict = {}
'Base_createOauth2User', None) cookie_hash = request.get(self.cookie_name)
if Base_createOauth2User is None: if cookie_hash is not None:
LOG('ERP5ExternalOauth2ExtractionPlugin', INFO, try:
'No Base_createOauth2User script available, install ' user_dict = self.getToken(cookie_hash)
'erp5_credential_oauth2, disabled authentication.') except KeyError:
return DumbHTTPExtractor().extractCredentials(request) LOG(self.getId(), INFO, 'Hash %s not found' % cookie_hash)
return {}
creds = {}
token = None token = None
if request._auth is not None: if "access_token" in user_dict:
# 1st - try to fetch from Authorization header token = user_dict["access_token"]
if self.header_string.lower() in request._auth.lower():
l = request._auth.split()
if len(l) == 2:
token = l[1]
if token is None: if token is None:
# no token # no token, then no credentials
return DumbHTTPExtractor().extractCredentials(request) return {}
# token is available
user = None
user_entry = None user_entry = None
try: try:
user = self.getToken(self.prefix + token) user_entry = self.getToken(token)
except KeyError: except KeyError:
user_entry = self.getUserEntry(token) user_entry = self.getUserEntry(token)
if user_entry is not None: if user_entry is not None:
user = user_entry['reference'] # Reduce data size because, we don't need more than reference
user_entry = {"reference": user_entry["reference"]}
if user is None:
# fallback to default way if user_entry is None:
return DumbHTTPExtractor().extractCredentials(request) # no user, then no credentials
return {}
tag = '%s_user_creation_in_progress' % user.encode('hex')
if self.getPortalObject().portal_activities.countMessageWithTag(tag) > 0:
self.REQUEST['USER_CREATION_IN_PROGRESS'] = user
else:
# create the user if not found
if not self.searchUsers(id=user, exact_match=True):
sm = getSecurityManager()
if sm.getUser().getId() != ERP5Security.SUPER_USER:
newSecurityManager(self, self.getUser(ERP5Security.SUPER_USER))
try:
self.REQUEST['USER_CREATION_IN_PROGRESS'] = user
if user_entry is None:
user_entry = self.getUserEntry(token)
try:
self.Base_createOauth2User(tag, **user_entry)
except Exception:
LOG('ERP5ExternalOauth2ExtractionPlugin', ERROR,
'Issue while calling creation script:', error=True)
raise
finally:
setSecurityManager(sm)
try: try:
self.setToken(self.prefix + token, user) # Every request will update cache to postpone the cache expiration
except KeyError: # to keep the user logged in
self.setToken(token, user_entry)
except KeyError as error:
# allow to work w/o cache # allow to work w/o cache
LOG(self.getId(), INFO, error)
pass pass
creds['external_login'] = user
# 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', '') creds['remote_host'] = request.get('REMOTE_HOST', '')
try: try:
creds['remote_address'] = request.getClientAddr() creds['remote_address'] = request.getClientAddr()
...@@ -215,8 +232,11 @@ class ERP5FacebookExtractionPlugin(ERP5ExternalOauth2ExtractionPlugin, BasePlugi ...@@ -215,8 +232,11 @@ class ERP5FacebookExtractionPlugin(ERP5ExternalOauth2ExtractionPlugin, BasePlugi
""" """
meta_type = "ERP5 Facebook Extraction Plugin" meta_type = "ERP5 Facebook Extraction Plugin"
prefix = 'fb_' cookie_name = "__ac_facebook_hash"
header_string = 'facebook' cache_factory_name = "facebook_server_auth_token_cache_factory"
def refreshTokenIfExpired(self, key, cache_value):
return cache_value
def getUserEntry(self, token): def getUserEntry(self, token):
if facebook is None: if facebook is None:
...@@ -240,8 +260,7 @@ class ERP5FacebookExtractionPlugin(ERP5ExternalOauth2ExtractionPlugin, BasePlugi ...@@ -240,8 +260,7 @@ class ERP5FacebookExtractionPlugin(ERP5ExternalOauth2ExtractionPlugin, BasePlugi
try: try:
for k in ('first_name', 'last_name', 'id', 'email'): for k in ('first_name', 'last_name', 'id', 'email'):
if k == 'id': if k == 'id':
user_entry['reference'] = self.prefix + facebook_entry[k].encode( user_entry['reference'] = facebook_entry[k].encode('utf-8')
'utf-8')
else: else:
user_entry[k] = facebook_entry[k].encode('utf-8') user_entry[k] = facebook_entry[k].encode('utf-8')
except KeyError: except KeyError:
...@@ -254,43 +273,28 @@ class ERP5GoogleExtractionPlugin(ERP5ExternalOauth2ExtractionPlugin, BasePlugin) ...@@ -254,43 +273,28 @@ class ERP5GoogleExtractionPlugin(ERP5ExternalOauth2ExtractionPlugin, BasePlugin)
""" """
meta_type = "ERP5 Google Extraction Plugin" meta_type = "ERP5 Google Extraction Plugin"
prefix = 'go_' login_portal_type = "Google Login"
header_string = 'google' 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())
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): def getUserEntry(self, token):
if httplib2 is None: return getGoogleUserEntry(token)
LOG('ERP5GoogleExtractionPlugin', INFO,
'No Google modules available, please install google-api-python-client '
'package. Authentication disabled..')
return None
timeout = socket.getdefaulttimeout()
try:
# require really fast interaction
socket.setdefaulttimeout(5)
http = oauth2client.client.AccessTokenCredentials(token, 'ERP5 Client'
).authorize(httplib2.Http())
service = apiclient.discovery.build("oauth2", "v1", http=http)
google_entry = service.userinfo().get().execute()
except Exception:
google_entry = None
finally:
socket.setdefaulttimeout(timeout)
user_entry = {}
if google_entry is not None:
# sanitise value
try:
for k in (('first_name', 'given_name'),
('last_name', 'family_name'),
('reference', 'id'),
('email', 'email')):
value = google_entry[k[1]].encode('utf-8')
if k[0] == 'reference':
value = self.prefix + value
user_entry[k[0]] = value
except KeyError:
user_entry = None
return user_entry
#List implementation of class #List implementation of class
classImplements( ERP5FacebookExtractionPlugin, classImplements( ERP5FacebookExtractionPlugin,
......
...@@ -286,7 +286,7 @@ class ERP5LoginUserManager(BasePlugin): ...@@ -286,7 +286,7 @@ class ERP5LoginUserManager(BasePlugin):
# users so code checking if a user login exists before allowing it to be # users so code checking if a user login exists before allowing it to be
# reused, preventing misleading logins from being misused. # reused, preventing misleading logins from being misused.
result.append({ result.append({
'id': None, 'id': special_user_name,
'login': special_user_name, 'login': special_user_name,
'pluginid': plugin_id, 'pluginid': plugin_id,
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment