-
Notifications
You must be signed in to change notification settings - Fork 7
/
drupalGitSSHDaemon.py
executable file
·313 lines (265 loc) · 11.5 KB
/
drupalGitSSHDaemon.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
#!/usr/bin/env python
import os
import shlex
import sys
from twisted.conch.avatar import ConchUser
from twisted.conch.error import ConchError, UnauthorizedLogin
from twisted.conch.ssh.channel import SSHChannel
from twisted.conch.ssh.session import ISession, SSHSession, SSHSessionProcessProtocol
from twisted.conch.ssh.factory import SSHFactory
from twisted.conch.ssh.keys import Key
from twisted.cred.checkers import ICredentialsChecker
from twisted.cred.credentials import IUsernamePassword, ISSHPrivateKey
from twisted.cred.portal import IRealm, Portal
from twisted.internet import reactor, defer
from twisted.python import components, log
from twisted.python.failure import Failure
from zope import interface
# Workaround for early EOF in git-receive-pack
# Seems related to Twisted bug #4350
# See: http://twistedmatrix.com/trac/ticket/4350
SSHSessionProcessProtocol.outConnectionLost = lambda self: None
import urllib
import base64
import hashlib
from config import config
import drush
class DrupalMeta(object):
def __init__(self):
self.anonymousReadAccess = config.getboolean('drupalSSHGitServer', 'anonymousReadAccess')
def request(self, uri):
"""Build the request to run against drupal
request(project uri)
Values and structure returned:
{username: {uid:int,
repo_id:int,
access:boolean,
branch_create:boolean,
branch_update:boolean,
branch_delete:boolean,
tag_create:boolean,
tag_update:boolean,
tag_delete:boolean,
per_label:list,
name:str,
pass:md5,
ssh_keys: { key_name:fingerprint }
}
}"""
drush_process = drush.DrushProcessProtocolJSON('vcs-auth-data')
drush_process.call(self.projectname(uri))
def JSONasynch(self):
return self.data
drush_process.deferred.addCallback(JSONasynch)
return drush_process.deferred
def repopath(self, scheme, subpath):
'''Note, this is where we do further mapping into a subdirectory
for a user or issue's specific sandbox'''
# Build the path to the repository
try:
scheme_path = config.get(scheme, 'repositoryPath')
except:
# Fall back to the default configured path scheme
scheme_path = config.get('drupalSSHGitServer', 'repositoryPath')
path = os.path.join(scheme_path, *subpath)
# Check to see that the folder exists
if not os.path.exists(path):
raise ConchError('Invalid repository: {0}'.format(path))
return path
def projectname(self, uri):
'''Extract the project name alone from a path like /project/views.git'''
parts = uri.split('/')
for part in parts:
if len(part) > 4 and part[-4:] == '.git':
return part[:-4]
log.err("ERROR: Couldn't determine project name for '%s'." % (uri,))
def find_git_shell():
# Find git-shell path.
# Adapted from http://bugs.python.org/file15381/shutil_which.patch
path = os.environ.get("PATH", os.defpath)
for dir in path.split(os.pathsep):
full_path = os.path.join(dir, 'git-shell')
if (os.path.exists(full_path) and
os.access(full_path, (os.F_OK | os.X_OK))):
return full_path
raise Exception('Could not find git executable!')
class GitSession(object):
interface.implements(ISession)
def __init__(self, user):
self.user = user
def map_user(self, username, fingerprint, users):
"""Map the username from name or fingerprint, to users item."""
if username == "git":
# Use the fingerprint
for user in users.values():
if fingerprint in user["ssh_keys"].values():
return user
# No fingerprints match
return None
else:
# Use the username
return users[username]
def auth(self, auth_service, argv):
"""Verify we have permission to run the request command."""
# Key fingerprint
if hasattr(self.user.meta, "fingerprint"):
fingerprint = self.user.meta.fingerprint
else:
fingerprint = None
if hasattr(self.user.meta, "password"):
password = self.user.meta.password
else:
password = None
# Check to see if anonymous read access is enabled and if
# this is a read
if (not self.user.meta.anonymousReadAccess or \
'git-upload-pack' not in argv[:-1]):
# If anonymous access for this type of command is not allowed,
# check if the user is a maintainer on this project
users = auth_service["users"]
user = self.map_user(self.user.username, fingerprint, users)
# global values - d.o issue #1036686
# 0 = ok, 1 = suspended, 2 = ToS unchecked, 3 = other reason
# "git":key
if self.user.username == "git" and user and user["global"]:
return True, auth_service
# Username in maintainers list
elif self.user.username in users and user["global"]:
# username:key
if fingerprint in user["ssh_keys"].values():
return True, auth_service
# username:password
elif user["pass"] == password:
return True, auth_service
else:
# Both kinds of username auth failed
return False, auth_service
else:
# Account is globally disabled or disallowed
return False, auth_service
else:
# Read only command and anonymous access is enabled
return True, auth_service
def errorHandler(self, fail, proto):
fail.trap(ConchError)
log.err(fail.value.value)
if proto.connectionMade():
proto.loseConnection()
else:
reactor.spawnProcess(proto, "/bin/false")
def execCommand(self, proto, cmd):
"""Execute a git-shell command."""
argv = shlex.split(cmd)
# This starts an auth request and returns.
auth_service_deferred = self.user.meta.request(argv[-1])
# Once it completes, auth is run
auth_service_deferred.addCallback(self.auth, argv)
# Then the result of auth is passed to execGitCommand to run git-shell
auth_service_deferred.addCallback(self.execGitCommand, argv, proto)
auth_service_deferred.addErrback(self.errorHandler, proto)
def execGitCommand(self, auth_values, argv, proto):
"""After all authentication is done, setup an environment and execute the git-shell commands."""
repostring = argv[-1]
repolist = repostring.split('/')
scheme = repolist[1]
projectpath = repolist[2:]
projectname = self.user.meta.projectname(repostring)
authed, auth_service = auth_values
sh = self.user.shell
if authed:
# Check permissions by mapping requested path to file system path
repopath = self.user.meta.repopath(scheme, projectpath)
env = {'VERSION_CONTROL_GIT_REPOSITORY':projectname,
'VERSION_CONTROL_GIT_USERNAME':self.user.username}
if self.user.username in auth_service:
# The UID is known
env['VERSION_CONTROL_GIT_UID'] = auth_service[self.user.username]['uid']
command = ' '.join(argv[:-1] + ["'{0}'".format(repopath)])
reactor.spawnProcess(proto, sh, (sh, '-c', command), env=env)
else:
log.err('Permission denied when accessing {0}'.format(repostring))
reactor.spawnProcess(proto, "/bin/false")
def eofReceived(self): pass
def closed(self): pass
class GitConchUser(ConchUser):
shell = find_git_shell()
def __init__(self, username, meta):
ConchUser.__init__(self)
self.username = username
self.channelLookup.update({"session": SSHSession})
self.meta = meta
def logout(self): pass
class GitRealm(object):
interface.implements(IRealm)
def __init__(self, meta):
self.meta = meta
def requestAvatar(self, username, mind, *interfaces):
user = GitConchUser(username, self.meta)
return interfaces[0], user, user.logout
class GitPubKeyChecker(object):
"""Skip most of the auth process until the SSH session starts.
Save the public key fingerprint for later use."""
credentialInterfaces = ISSHPrivateKey,
interface.implements(ICredentialsChecker)
def __init__(self, meta):
self.meta = meta
def requestAvatarId(self, credentials):
key = Key.fromString(credentials.blob)
fingerprint = key.fingerprint().replace(':', '')
self.meta.fingerprint = fingerprint
if (credentials.username == 'git'):
return defer.succeed(credentials.username)
else:
""" If a user specified a non-git username, check that the user's key matches their username
so that we can request a password if it does not."""
drush_process = drush.DrushProcessProtocolBool('drupalorg-ssh-user-key')
drush_process.call(credentials.username, fingerprint)
def username(self):
if self.result:
return credentials.username
else:
return Failure(UnauthorizedLogin(credentials.username))
drush_process.deferred.addCallback(username)
return drush_process.deferred
class GitPasswordChecker(object):
"""Skip most of the auth process until the SSH session starts.
Save the password hash for later use."""
credentialInterfaces = IUsernamePassword,
interface.implements(ICredentialsChecker)
def __init__(self, meta):
self.meta = meta
def requestAvatarId(self, credentials):
self.meta.password = hashlib.md5(credentials.password).hexdigest()
drush_process = drush.DrushProcessProtocolBool('drupalorg-vcs-auth-check-user-pass')
drush_process.call(credentials.username, credentials.password)
def username(self):
if self.result:
return credentials.username
else:
return Failure(UnauthorizedLogin(credentials.username))
drush_process.deferred.addCallback(username)
return drush_process.deferred
class GitServer(SSHFactory):
authmeta = DrupalMeta()
portal = Portal(GitRealm(authmeta))
portal.registerChecker(GitPubKeyChecker(authmeta))
portal.registerChecker(GitPasswordChecker(authmeta))
def __init__(self, privkey):
pubkey = '.'.join((privkey, 'pub'))
self.privateKeys = {'ssh-rsa': Key.fromFile(privkey)}
self.publicKeys = {'ssh-rsa': Key.fromFile(pubkey)}
class Server(object):
def __init__(self):
self.port = config.getint('drupalSSHGitServer', 'port')
self.interface = config.get('drupalSSHGitServer', 'host')
self.key = config.get('drupalSSHGitServer', 'privateKeyLocation')
components.registerAdapter(GitSession, GitConchUser, ISession)
def application(self):
return GitServer(self.key)
if __name__ == '__main__':
log.startLogging(sys.stderr)
ssh_server = Server()
reactor.listenTCP(ssh_server.port,
ssh_server.application(),
interface=ssh_server.interface)
reactor.run()