Commit a303a379 authored by Julien Muchembled's avatar Julien Muchembled

Implement authentication to Git repositories from ERP5VCS interface

parent 90d367e6
...@@ -66,7 +66,7 @@ return traverseName(context, caller)(**caller_kw)\n ...@@ -66,7 +66,7 @@ return traverseName(context, caller)(**caller_kw)\n
</item> </item>
<item> <item>
<key> <string>id</string> </key> <key> <string>id</string> </key>
<value> <string>BusinessTemplate_doSvnLogin</string> </value> <value> <string>BusinessTemplate_doVcsLogin</string> </value>
</item> </item>
</dictionary> </dictionary>
</pickle> </pickle>
......
...@@ -50,7 +50,8 @@ ...@@ -50,7 +50,8 @@
</item> </item>
<item> <item>
<key> <string>_body</string> </key> <key> <string>_body</string> </key>
<value> <string>from Products.ERP5VCS.SubversionClient import SubversionSSLTrustError, SubversionLoginError\n <value> <string>from Products.ERP5VCS.Git import GitLoginError\n
from Products.ERP5VCS.SubversionClient import SubversionSSLTrustError, SubversionLoginError\n
\n \n
try:\n try:\n
raise exception\n raise exception\n
...@@ -62,6 +63,10 @@ except SubversionLoginError, e:\n ...@@ -62,6 +63,10 @@ except SubversionLoginError, e:\n
message = \'Server needs authentication, no cookie found\'\n message = \'Server needs authentication, no cookie found\'\n
kw = dict(realm=e.getRealm(), username=context.getVcsTool().getPreferredUsername())\n kw = dict(realm=e.getRealm(), username=context.getVcsTool().getPreferredUsername())\n
method = \'BusinessTemplate_viewSvnLogin\'\n method = \'BusinessTemplate_viewSvnLogin\'\n
except GitLoginError, e:\n
message = str(e)\n
kw = dict(remote_url=context.getVcsTool().getRemoteUrl())\n
method = \'BusinessTemplate_viewGitLogin\'\n
\n \n
context.REQUEST.set(\'portal_status_message\', message)\n context.REQUEST.set(\'portal_status_message\', message)\n
return getattr(context.asContext(**kw), method)(caller=caller, caller_kw=caller_kw)\n return getattr(context.asContext(**kw), method)(caller=caller, caller_kw=caller_kw)\n
......
<?xml version="1.0"?>
<ZopeData>
<record id="1" aka="AAAAAAAAAAE=">
<pickle>
<global name="ERP5Form" module="Products.ERP5Form.Form"/>
</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>BusinessTemplate_doVcsLogin</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>remote_url</string>
<string>your_user</string>
<string>your_password</string>
<string>your_auth</string>
</list>
</value>
</item>
<item>
<key> <string>right</string> </key>
<value>
<list/>
</value>
</item>
</dictionary>
</value>
</item>
<item>
<key> <string>id</string> </key>
<value> <string>BusinessTemplate_viewGitLogin</string> </value>
</item>
<item>
<key> <string>method</string> </key>
<value> <string>POST</string> </value>
</item>
<item>
<key> <string>name</string> </key>
<value> <string></string> </value>
</item>
<item>
<key> <string>pt</string> </key>
<value> <string>vcs_dialog</string> </value>
</item>
<item>
<key> <string>row_length</string> </key>
<value> <int>4</int> </value>
</item>
<item>
<key> <string>stored_encoding</string> </key>
<value> <string>UTF-8</string> </value>
</item>
<item>
<key> <string>title</string> </key>
<value> <string>Login</string> </value>
</item>
<item>
<key> <string>unicode_mode</string> </key>
<value> <int>0</int> </value>
</item>
<item>
<key> <string>update_action</string> </key>
<value> <string></string> </value>
</item>
<item>
<key> <string>update_action_title</string> </key>
<value> <string></string> </value>
</item>
</dictionary>
</pickle>
</record>
</ZopeData>
<?xml version="1.0"?>
<ZopeData>
<record id="1" aka="AAAAAAAAAAE=">
<pickle>
<global name="ProxyField" module="Products.ERP5Form.ProxyField"/>
</pickle>
<pickle>
<dictionary>
<item>
<key> <string>delegated_list</string> </key>
<value>
<list>
<string>default</string>
<string>editable</string>
<string>title</string>
</list>
</value>
</item>
<item>
<key> <string>id</string> </key>
<value> <string>remote_url</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>default</string> </key>
<value>
<persistent> <string encoding="base64">AAAAAAAAAAI=</string> </persistent>
</value>
</item>
<item>
<key> <string>editable</string> </key>
<value> <string></string> </value>
</item>
<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>
<item>
<key> <string>title</string> </key>
<value> <string></string> </value>
</item>
</dictionary>
</value>
</item>
<item>
<key> <string>values</string> </key>
<value>
<dictionary>
<item>
<key> <string>default</string> </key>
<value> <string></string> </value>
</item>
<item>
<key> <string>editable</string> </key>
<value> <int>0</int> </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>Remote URL</string> </value>
</item>
</dictionary>
</value>
</item>
</dictionary>
</pickle>
</record>
<record id="2" aka="AAAAAAAAAAI=">
<pickle>
<global name="TALESMethod" module="Products.Formulator.TALESField"/>
</pickle>
<pickle>
<dictionary>
<item>
<key> <string>_text</string> </key>
<value> <string>here/remote_url</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>editable</string>
<string>hidden</string>
<string>title</string>
</list>
</value>
</item>
<item>
<key> <string>id</string> </key>
<value> <string>your_auth</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>hidden</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>editable</string> </key>
<value> <int>1</int> </value>
</item>
<item>
<key> <string>field_id</string> </key>
<value> <string>remote_url</string> </value>
</item>
<item>
<key> <string>form_id</string> </key>
<value> <string>BusinessTemplate_viewGitLogin</string> </value>
</item>
<item>
<key> <string>hidden</string> </key>
<value> <int>1</int> </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 encoding="cdata"><![CDATA[
&nbsp;
]]></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>your_user</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>
<item>
<key> <string>title</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_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 Name</string> </value>
</item>
</dictionary>
</value>
</item>
</dictionary>
</pickle>
</record>
</ZopeData>
...@@ -35,7 +35,7 @@ ...@@ -35,7 +35,7 @@
</item> </item>
<item> <item>
<key> <string>action</string> </key> <key> <string>action</string> </key>
<value> <string>BusinessTemplate_doSvnLogin</string> </value> <value> <string>BusinessTemplate_doVcsLogin</string> </value>
</item> </item>
<item> <item>
<key> <string>description</string> </key> <key> <string>description</string> </key>
...@@ -98,7 +98,7 @@ ...@@ -98,7 +98,7 @@
</item> </item>
<item> <item>
<key> <string>name</string> </key> <key> <string>name</string> </key>
<value> <string>BusinessTemplate_viewSvnLogin2</string> </value> <value> <string></string> </value>
</item> </item>
<item> <item>
<key> <string>pt</string> </key> <key> <string>pt</string> </key>
......
656 657
\ No newline at end of file \ No newline at end of file
...@@ -28,6 +28,7 @@ ...@@ -28,6 +28,7 @@
import os, re, subprocess import os, re, subprocess
from AccessControl import ClassSecurityInfo from AccessControl import ClassSecurityInfo
from AccessControl.SecurityInfo import ModuleSecurityInfo
from Acquisition import aq_base from Acquisition import aq_base
from DateTime import DateTime from DateTime import DateTime
from Products.ERP5Type.Message import translateString from Products.ERP5Type.Message import translateString
...@@ -35,12 +36,19 @@ from ZTUtils import make_query ...@@ -35,12 +36,19 @@ from ZTUtils import make_query
from Products.ERP5VCS.WorkingCopy import \ from Products.ERP5VCS.WorkingCopy import \
WorkingCopy, NotAWorkingCopyError, NotVersionedError, Dir, File, selfcached WorkingCopy, NotAWorkingCopyError, NotVersionedError, Dir, File, selfcached
# TODO: write a similar helper for 'nt' platform
GIT_ASKPASS = os.path.join(os.path.dirname(__file__), 'bin', 'git_askpass')
class GitError(EnvironmentError): class GitError(EnvironmentError):
def __init__(self, err, out, returncode): def __init__(self, err, out, returncode):
EnvironmentError.__init__(self, err) EnvironmentError.__init__(self, err)
self.stdout = out self.stdout = out
self.returncode = returncode self.returncode = returncode
class GitLoginError(EnvironmentError):
"""Raised when an authentication is required"""
ModuleSecurityInfo(__name__).declarePublic('GitLoginError')
class Git(WorkingCopy): class Git(WorkingCopy):
security = ClassSecurityInfo() security = ClassSecurityInfo()
...@@ -48,6 +56,8 @@ class Git(WorkingCopy): ...@@ -48,6 +56,8 @@ class Git(WorkingCopy):
reference = 'git' reference = 'git'
title = 'Git' title = 'Git'
_login_cookie_name = 'erp5_git_login'
def _git(self, *args, **kw): def _git(self, *args, **kw):
kw.setdefault('cwd', self.working_copy) kw.setdefault('cwd', self.working_copy)
argv = ['git'] argv = ['git']
...@@ -64,6 +74,41 @@ class Git(WorkingCopy): ...@@ -64,6 +74,41 @@ class Git(WorkingCopy):
return out.strip() return out.strip()
return out return out
@selfcached
def _getLogin(self):
target_url = self.getRemoteUrl()
try:
for url, user, password in self._getCookie(self._login_cookie_name, ()):
if target_url == url:
return user, password
except ValueError:
pass
def setLogin(self, remote_url, user, password):
"""Set login information"""
login_list = [x for x in self._getCookie(self._login_cookie_name, ())
if x[0] != remote_url]
login_list.append((remote_url, user, password))
self._setCookie(self._login_cookie_name, login_list)
def remote_git(self, *args, **kw):
try:
env = kw['env']
except KeyError:
kw['env'] = env = dict(os.environ)
env['GIT_ASKPASS'] = GIT_ASKPASS
userpwd = self._getLogin()
if userpwd:
env.update(ERP5_GIT_USERNAME=userpwd[0], ERP5_GIT_PASSWORD=userpwd[1])
try:
return self.git(*args, **kw)
except GitError, e:
message = 'Authentication failed'
if message in str(e):
raise GitLoginError(userpwd and message or
'Server needs authentication, no cookie found')
raise
def __init__(self, *args, **kw): def __init__(self, *args, **kw):
WorkingCopy.__init__(self, *args, **kw) WorkingCopy.__init__(self, *args, **kw)
out = self._git('rev-parse', '--show-toplevel', '--show-prefix', out = self._git('rev-parse', '--show-toplevel', '--show-prefix',
...@@ -206,7 +251,7 @@ class Git(WorkingCopy): ...@@ -206,7 +251,7 @@ class Git(WorkingCopy):
raise NotImplementedError raise NotImplementedError
if not keep: if not keep:
self.clean() self.clean()
self.git('pull', '--ff-only') self.remote_git('pull', '--ff-only')
elif 1: # elif local_changes: elif 1: # elif local_changes:
raise NotImplementedError raise NotImplementedError
# addremove # addremove
...@@ -219,7 +264,7 @@ class Git(WorkingCopy): ...@@ -219,7 +264,7 @@ class Git(WorkingCopy):
# finally: # finally:
# symbolic-ref HEAD B # symbolic-ref HEAD B
else: else:
self.git('pull', '--ff-only') self.remote_git('pull', '--ff-only')
return self.aq_parent.download(self.working_copy) return self.aq_parent.download(self.working_copy)
def showOld(self, path): def showOld(self, path):
...@@ -279,14 +324,14 @@ class Git(WorkingCopy): ...@@ -279,14 +324,14 @@ class Git(WorkingCopy):
remote, dst = remote.split('/', 1) remote, dst = remote.split('/', 1)
push_args = 'push', '--porcelain', remote, '%s:%s' % (src, dst) push_args = 'push', '--porcelain', remote, '%s:%s' % (src, dst)
try: try:
self.git(*push_args) self.remote_git(*push_args)
except GitError, e: except GitError, e:
# first check why we could not push # first check why we could not push
status = [x for x in e.stdout.splitlines() if x[:1] == '!'] status = [x for x in e.stdout.splitlines() if x[:1] == '!']
if (len(status) != 1 or if (len(status) != 1 or
status[0].split()[2:] != ['[rejected]', '(non-fast-forward)']): status[0].split()[2:] != ['[rejected]', '(non-fast-forward)']):
raise raise
self.git('fetch', '--prune', remote) self.remote_git('fetch', '--prune', remote)
if not self.getBehindCount(): if not self.getBehindCount():
raise raise
# try to update our working copy # try to update our working copy
...@@ -305,7 +350,7 @@ class Git(WorkingCopy): ...@@ -305,7 +350,7 @@ class Git(WorkingCopy):
if merge == 'merge': if merge == 'merge':
reset += 1 reset += 1
# retry to push everything # retry to push everything
self.git(*push_args) self.remote_git(*push_args)
except GitError, e: except GitError, e:
self.git('reset', '--soft', '@{%u}' % reset) self.git('reset', '--soft', '@{%u}' % reset)
portal_status_message = str(e) portal_status_message = str(e)
......
...@@ -29,9 +29,7 @@ ...@@ -29,9 +29,7 @@
# #
############################################################################## ##############################################################################
import errno, glob, json, os, re, shutil import errno, glob, os, re, shutil
from base64 import b64encode, b64decode
from DateTime import DateTime
from ZTUtils import make_query from ZTUtils import make_query
from Products.ERP5Type.Message import translateString from Products.ERP5Type.Message import translateString
from Products.ERP5.Document.BusinessTemplate import BusinessTemplateFolder from Products.ERP5.Document.BusinessTemplate import BusinessTemplateFolder
...@@ -55,22 +53,6 @@ class Subversion(WorkingCopy): ...@@ -55,22 +53,6 @@ class Subversion(WorkingCopy):
if path and not os.path.exists(os.path.join(self.working_copy, '.svn')): if path and not os.path.exists(os.path.join(self.working_copy, '.svn')):
raise NotAWorkingCopyError(self.working_copy) raise NotAWorkingCopyError(self.working_copy)
def _getCookie(self, name, default=None):
try:
return json.loads(b64decode(self.REQUEST[name]))
except StandardError:
return default
def _setCookie(self, name, value, days=30):
portal = self.getPortalObject()
request = portal.REQUEST
value = b64encode(json.dumps(value))
request.set(name, value)
if days:
expires = (DateTime() + days).toZone('GMT').rfc822()
request.RESPONSE.setCookie(name, value, path=portal.absolute_url_path(),
expires=expires)
def setLogin(self, realm, user, password): def setLogin(self, realm, user, password):
"""Set login information. """Set login information.
""" """
......
...@@ -29,13 +29,15 @@ ...@@ -29,13 +29,15 @@
# #
############################################################################## ##############################################################################
import errno, os, re, shutil import errno, json, os, re, shutil
from base64 import b64encode, b64decode
from tempfile import gettempdir from tempfile import gettempdir
import transaction import transaction
from AccessControl import Unauthorized from AccessControl import Unauthorized
from AccessControl.SecurityInfo import ModuleSecurityInfo from AccessControl.SecurityInfo import ModuleSecurityInfo
from Acquisition import aq_base, Implicit from Acquisition import aq_base, Implicit
from App.config import getConfiguration from App.config import getConfiguration
from DateTime import DateTime
from ZTUtils import make_query from ZTUtils import make_query
from Products.ERP5.Document.BusinessTemplate import BusinessTemplateFolder from Products.ERP5.Document.BusinessTemplate import BusinessTemplateFolder
from Products.ERP5Type.Utils import simple_decorator from Products.ERP5Type.Utils import simple_decorator
...@@ -141,6 +143,22 @@ class WorkingCopy(Implicit): ...@@ -141,6 +143,22 @@ class WorkingCopy(Implicit):
raise Unauthorized("Unauthorized access to path %r." raise Unauthorized("Unauthorized access to path %r."
" It is NOT in your Zope home instance." % path) " It is NOT in your Zope home instance." % path)
def _getCookie(self, name, default=None):
try:
return json.loads(b64decode(self.REQUEST[name]))
except StandardError:
return default
def _setCookie(self, name, value, days=30):
portal = self.getPortalObject()
request = portal.REQUEST
value = b64encode(json.dumps(value))
request.set(name, value)
if days:
expires = (DateTime() + days).toZone('GMT').rfc822()
request.RESPONSE.setCookie(name, value, path=portal.absolute_url_path(),
expires=expires)
# path is the path in svn working copy # path is the path in svn working copy
# return edit_path in zodb to edit it # return edit_path in zodb to edit it
def editPath(self, path, html=False): def editPath(self, path, html=False):
......
#!/bin/sh -e
case "$1" in
Username:\ ) echo -n "$ERP5_GIT_USERNAME" ;;
Password:\ ) echo -n "$ERP5_GIT_PASSWORD" ;;
*) false ;;
esac
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