Commit b4efcf88 authored by Gabriel Monnerat's avatar Gabriel Monnerat

Support Google login in ERP5

This merge request contains the feature to login in ERP5 with Google Account.

Also, this branch is based on the work done in !185

We have a lot of discussion about this changes in !179, but with a lot of merge conflicts, a new MR is better to review.

This branch will wake up the discuss of "Moving API Keys from preference to a better place", I can work on it but I am not sure if we have a good design yet.

Any comment is welcome

/cc @tb @romain @rafael @kazuhiko @vincent @jerome

/reviewed-on !219
parents dd3a03a4 9be9f39f
import hmac
from Products.ERP5Type.Cache import DEFAULT_CACHE_SCOPE
CACHE_FACTORY_NAME = 'bearer_token_cache_factory'
def getHMAC(self, key, body):
digest = hmac.new(key, body)
return digest.hexdigest()
def _getCacheFactory(self):
def _getCacheFactory(self, cache_factory_name):
portal = self.getPortalObject()
cache_tool = portal.portal_caches
cache_factory_name = 'bearer_token_cache_factory'
cache_factory = cache_tool.getRamCacheRoot().get(cache_factory_name)
#XXX This conditional statement should be remove as soon as
#Broadcasting will be enable among all zeo clients.
......@@ -19,15 +20,15 @@ def _getCacheFactory(self):
cache_tool.updateCache()
return cache_tool.getRamCacheRoot().get(cache_factory_name)
def setBearerToken(self, key, body):
cache_factory = _getCacheFactory(self)
def setBearerToken(self, key, body, cache_factory_name=CACHE_FACTORY_NAME):
cache_factory = _getCacheFactory(self, cache_factory_name)
cache_duration = cache_factory.cache_duration
for cache_plugin in cache_factory.getCachePluginList():
cache_plugin.set(key, DEFAULT_CACHE_SCOPE,
body, cache_duration=cache_duration)
def getBearerToken(self, key):
cache_factory = _getCacheFactory(self)
def getBearerToken(self, key, cache_factory_name=CACHE_FACTORY_NAME):
cache_factory = _getCacheFactory(self, cache_factory_name)
for cache_plugin in cache_factory.getCachePluginList():
cache_entry = cache_plugin.get(key, DEFAULT_CACHE_SCOPE)
if cache_entry is not None:
......
......@@ -3,6 +3,9 @@
xmlns:i18n="http://xml.zope.org/namespaces/i18n">
<tal:block tal:define="form_action string:logged_in;
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(), )]">
<tal:block metal:use-macro="here/main_template/macros/master">
<tal:block metal:fill-slot="main">
......@@ -45,7 +48,17 @@
<a tal:attributes="href string:${here/portal_url}/ERP5Site_viewCredentialRecoveryLoginDialog"
i18n:translate="" i18n:domain="ui">Can't access your account ?</a>
</div>
<p class="clear"></p>
</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>
<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>
......
<?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>
<string>my_description</string>
</list>
</value>
</item>
<item>
<key> <string>hidden</string> </key>
<value>
<list/>
</value>
</item>
<item>
<key> <string>left</string> </key>
<value>
<list>
<string>my_title</string>
<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>id</string> </key>
<value> <string>my_description</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_description</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>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>delegated_list</string> </key>
<value>
<list/>
</value>
</item>
<item>
<key> <string>id</string> </key>
<value> <string>my_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_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>
<?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>
......@@ -11,8 +11,6 @@
<value>
<list>
<string>columns</string>
<string>count_method</string>
<string>selection_name</string>
<string>title</string>
</list>
</value>
......@@ -44,10 +42,6 @@
<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>
......@@ -63,10 +57,6 @@
<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>
......@@ -74,16 +64,6 @@
<key> <string>values</string> </key>
<value>
<dictionary>
<item>
<key> <string>Base_viewSearchResultList</string> </key>
<value> <int>1</int> </value>
</item>
<item>
<key> <string>all_editable_columns</string> </key>
<value>
<list/>
</value>
</item>
<item>
<key> <string>columns</string> </key>
<value>
......@@ -93,64 +73,23 @@
<string>Reference</string>
</tuple>
<tuple>
<string>modification_date</string>
<string>Modification Date</string>
<string>translated_validation_state_title</string>
<string>State</string>
</tuple>
</list>
</value>
</item>
<item>
<key> <string>count_method</string> </key>
<value>
<persistent> <string encoding="base64">AAAAAAAAAAI=</string> </persistent>
</value>
</item>
<item>
<key> <string>field_id</string> </key>
<value> <string>my_list_mode_listbox</string> </value>
<value> <string>my_view_mode_module_listbox</string> </value>
</item>
<item>
<key> <string>form_id</string> </key>
<value> <string>Base_viewFieldLibrary</string> </value>
</item>
<item>
<key> <string>height</string> </key>
<value> <int>5</int> </value>
</item>
<item>
<key> <string>list_cookie</string> </key>
<value> <string>PERSON_LIST</string> </value>
</item>
<item>
<key> <string>more_columns</string> </key>
<value>
<list>
<tuple>
<string>path</string>
<string>Path</string>
</tuple>
</list>
</value>
</item>
<item>
<key> <string>reverse</string> </key>
<value> <int>0</int> </value>
</item>
<item>
<key> <string>selection_name</string> </key>
<value> <string>oauth_module_selection</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>OAuth Tokens</string> </value>
</item>
<item>
<key> <string>width</string> </key>
<value> <int>40</int> </value>
<value> <string>OAuth Connectors</string> </value>
</item>
</dictionary>
</value>
......@@ -158,17 +97,4 @@
</dictionary>
</pickle>
</record>
<record id="2" aka="AAAAAAAAAAI=">
<pickle>
<global name="Method" module="Products.Formulator.MethodField"/>
</pickle>
<pickle>
<dictionary>
<item>
<key> <string>method_name</string> </key>
<value> <string>countFolder</string> </value>
</item>
</dictionary>
</pickle>
</record>
</ZopeData>
......@@ -12,16 +12,10 @@
<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>
<persistent> <string encoding="base64">AAAAAAAAAAM=</string> </persistent>
</value>
</item>
<item>
......@@ -112,7 +106,7 @@
<item>
<key> <string>signature_methods</string> </key>
<value>
<persistent> <string encoding="base64">AAAAAAAAAAU=</string> </persistent>
<persistent> <string encoding="base64">AAAAAAAAAAQ=</string> </persistent>
</value>
</item>
<item>
......@@ -137,14 +131,6 @@
</pickle>
</record>
<record id="4" aka="AAAAAAAAAAQ=">
<pickle>
<global name="OOBTree" module="BTrees.OOBTree"/>
</pickle>
<pickle>
<none/>
</pickle>
</record>
<record id="5" aka="AAAAAAAAAAU=">
<pickle>
<tuple>
<global name="PersistentMapping" module="Persistence.mapping"/>
......@@ -162,11 +148,11 @@
<value>
<object>
<klass>
<global id="5.1" name="_dt_reconstructor" module="DateTime.DateTime"/>
<global id="4.1" name="_dt_reconstructor" module="DateTime.DateTime"/>
</klass>
<tuple>
<global name="OAuthSignatureMethod_HMAC_SHA1" module="Products.ERP5TioSafe.Tool.OAuthTool"/>
<global id="5.2" name="object" module="__builtin__"/>
<global id="4.2" name="object" module="__builtin__"/>
<none/>
</tuple>
</object>
......@@ -176,10 +162,10 @@
<key> <string>PLAINTEXT</string> </key>
<value>
<object>
<klass> <reference id="5.1"/> </klass>
<klass> <reference id="4.1"/> </klass>
<tuple>
<global name="OAuthSignatureMethod_PLAINTEXT" module="Products.ERP5TioSafe.Tool.OAuthTool"/>
<reference id="5.2"/>
<reference id="4.2"/>
<none/>
</tuple>
</object>
......
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
"""
self.login()
self.portal.TemplateTool_checkGoogleExtractionPluginExistenceConsistency(fixit=True)
# 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()
self.logout()
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)
cache_dict = self.portal.Base_getBearerToken(google_hash, "google_server_auth_token_cache_factory")
self.assertEqual(CLIENT_ID, cache_dict["client_id"])
self.assertEqual(ACCESS_TOKEN, cache_dict["access_token"])
self.assertEqual({'reference': getUserId(None)},
self.portal.Base_getBearerToken(ACCESS_TOKEN, "google_server_auth_token_cache_factory")
)
self.portal.REQUEST["__ac_google_hash"] = google_hash
erp5_google_extractor = self.portal.acl_users.erp5_google_extraction
self.assertEqual({'external_login': getUserId(None),
'login_portal_type': 'Google Login',
'remote_host': '',
'remote_address': ''}, erp5_google_extractor.extractCredentials(self.portal.REQUEST))
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
if REQUEST.has_key('portal_skin'):
portal.portal_skins.clearSkinCookie()
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')
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{
fieldset > div.large-gadget {
height: 85vh;
}
a.zocial {
margin-top: 10px;
}
......@@ -3,6 +3,9 @@
xmlns:i18n="http://xml.zope.org/namespaces/i18n">
<tal:block tal:define="form_action string:logged_in;
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(), )]">
<tal:block metal:use-macro="here/main_template/macros/master">
<tal:block metal:fill-slot="main">
......@@ -46,6 +49,15 @@
i18n:translate="" i18n:domain="ui">I forgot my password!</a>
</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>
<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>
......
@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
from Products.PluggableAuthService.utils import classImplements
from Products.PluggableAuthService.plugins.BasePlugin import BasePlugin
from Products import ERP5Security
from Products.PluggableAuthService.PluggableAuthService import DumbHTTPExtractor
from AccessControl.SecurityManagement import getSecurityManager, \
setSecurityManager, newSecurityManager
from Products.ERP5Type.Cache import DEFAULT_CACHE_SCOPE
import time
import socket
import httplib
import urllib
import json
from zLOG import LOG, ERROR, INFO
try:
......@@ -58,6 +61,30 @@ manage_addERP5FacebookExtractionPluginForm = PageTemplateFile(
'www/ERP5Security_addERP5FacebookExtractionPlugin', globals(),
__name__='manage_addERP5FacebookExtractionPluginForm')
def getGoogleUserEntry(token):
if httplib2 is None:
LOG('ERP5GoogleExtractionPlugin', INFO,
'No Google modules available, please install google-api-python-client '
'package. Authentication disabled..')
return None
http = oauth2client.client.AccessTokenCredentials(token,
'ERP5 Client'
).authorize(httplib2.Http(timeout=5))
service = apiclient.discovery.build("oauth2", "v1", http=http)
google_entry = service.userinfo().get().execute()
user_entry = {}
if google_entry is not None:
# sanitise value
for k in (('first_name', 'given_name'),
('last_name', 'family_name'),
('email', 'email'),
('reference', 'email'),):
value = google_entry[k[1]].encode('utf-8')
user_entry[k[0]] = value
return user_entry
def addERP5FacebookExtractionPlugin(dispatcher, id, title=None, REQUEST=None):
""" Add a ERP5FacebookExtractionPlugin to a Pluggable Auth Service. """
......@@ -91,7 +118,7 @@ def addERP5GoogleExtractionPlugin(dispatcher, id, title=None, REQUEST=None):
class ERP5ExternalOauth2ExtractionPlugin:
cache_factory_name = 'extrenal_oauth2_token_cache_factory'
cache_factory_name = 'external_oauth2_token_cache_factory'
security = ClassSecurityInfo()
def __init__(self, id, title=None):
......@@ -115,7 +142,7 @@ class ERP5ExternalOauth2ExtractionPlugin:
cache_tool.updateCache()
cache_factory = cache_tool.getRamCacheRoot().get(self.cache_factory_name)
if cache_factory is None:
raise KeyError
raise KeyError("Cache factory %s not found" % self.cache_factory_name)
return cache_factory
def setToken(self, key, body):
......@@ -130,7 +157,13 @@ class ERP5ExternalOauth2ExtractionPlugin:
for cache_plugin in cache_factory.getCachePluginList():
cache_entry = cache_plugin.get(key, DEFAULT_CACHE_SCOPE)
if cache_entry is not None:
return cache_entry.getValue()
# 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)
####################################
......@@ -139,69 +172,53 @@ class ERP5ExternalOauth2ExtractionPlugin:
security.declarePrivate('extractCredentials')
def extractCredentials(self, request):
""" Extract Oauth2 credentials from the request header. """
Base_createOauth2User = getattr(self.getPortalObject(),
'Base_createOauth2User', None)
if Base_createOauth2User is None:
LOG('ERP5ExternalOauth2ExtractionPlugin', INFO,
'No Base_createOauth2User script available, install '
'erp5_credential_oauth2, disabled authentication.')
return DumbHTTPExtractor().extractCredentials(request)
creds = {}
user_dict = {}
cookie_hash = request.get(self.cookie_name)
if cookie_hash is not None:
try:
user_dict = self.getToken(cookie_hash)
except KeyError:
LOG(self.getId(), INFO, 'Hash %s not found' % cookie_hash)
return {}
token = None
if request._auth is not None:
# 1st - try to fetch from Authorization header
if self.header_string.lower() in request._auth.lower():
l = request._auth.split()
if len(l) == 2:
token = l[1]
if "access_token" in user_dict:
token = user_dict["access_token"]
if token is None:
# no token
return DumbHTTPExtractor().extractCredentials(request)
# no token, then no credentials
return {}
# token is available
user = None
user_entry = None
try:
user = self.getToken(self.prefix + token)
user_entry = self.getToken(token)
except KeyError:
user_entry = self.getUserEntry(token)
if user_entry is not None:
user = user_entry['reference']
if user is None:
# fallback to default way
return DumbHTTPExtractor().extractCredentials(request)
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)
# Reduce data size because, we don't need more than reference
user_entry = {"reference": user_entry["reference"]}
if user_entry is None:
# no user, then no credentials
return {}
try:
self.setToken(self.prefix + token, user)
except KeyError:
# Every request will update cache to postpone the cache expiration
# to keep the user logged in
self.setToken(token, user_entry)
except KeyError as error:
# allow to work w/o cache
LOG(self.getId(), INFO, error)
pass
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', '')
try:
creds['remote_address'] = request.getClientAddr()
......@@ -215,8 +232,11 @@ class ERP5FacebookExtractionPlugin(ERP5ExternalOauth2ExtractionPlugin, BasePlugi
"""
meta_type = "ERP5 Facebook Extraction Plugin"
prefix = 'fb_'
header_string = 'facebook'
cookie_name = "__ac_facebook_hash"
cache_factory_name = "facebook_server_auth_token_cache_factory"
def refreshTokenIfExpired(self, key, cache_value):
return cache_value
def getUserEntry(self, token):
if facebook is None:
......@@ -240,8 +260,7 @@ class ERP5FacebookExtractionPlugin(ERP5ExternalOauth2ExtractionPlugin, BasePlugi
try:
for k in ('first_name', 'last_name', 'id', 'email'):
if k == 'id':
user_entry['reference'] = self.prefix + facebook_entry[k].encode(
'utf-8')
user_entry['reference'] = facebook_entry[k].encode('utf-8')
else:
user_entry[k] = facebook_entry[k].encode('utf-8')
except KeyError:
......@@ -254,43 +273,28 @@ class ERP5GoogleExtractionPlugin(ERP5ExternalOauth2ExtractionPlugin, BasePlugin)
"""
meta_type = "ERP5 Google Extraction Plugin"
prefix = 'go_'
header_string = 'google'
login_portal_type = "Google Login"
cookie_name = "__ac_google_hash"
cache_factory_name = "google_server_auth_token_cache_factory"
def refreshTokenIfExpired(self, key, cache_value):
expires_in = cache_value.get("token_response", {}).get("expires_in")
refresh_token = cache_value.get("refresh_token")
if expires_in and refresh_token:
if (time.time() - cache_value["response_timestamp"]) >= float(expires_in):
credential = oauth2client.client.OAuth2Credentials(
cache_value["access_token"], cache_value["client_id"],
cache_value["client_secret"], refresh_token,
cache_value["token_expiry"], cache_value["token_uri"],
cache_value["user_agent"])
credential.refresh(httplib2.Http(timeout=5))
cache_value = json.loads(credential.to_json())
cache_value["response_timestamp"] = time.time()
self.setToken(key, cache_value)
return cache_value
def getUserEntry(self, token):
if httplib2 is None:
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
return getGoogleUserEntry(token)
#List implementation of class
classImplements( ERP5FacebookExtractionPlugin,
......
......@@ -286,7 +286,7 @@ class ERP5LoginUserManager(BasePlugin):
# users so code checking if a user login exists before allowing it to be
# reused, preventing misleading logins from being misused.
result.append({
'id': None,
'id': special_user_name,
'login': special_user_name,
'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