Merge ~cjwatson/launchpad:py3-restricted-codehosting-session into launchpad:master

Proposed by Colin Watson
Status: Merged
Approved by: Colin Watson
Approved revision: 6eea481b643548f77da39cf87be4d0e39b7d9c3a
Merge reported by: Otto Co-Pilot
Merged at revision: not available
Proposed branch: ~cjwatson/launchpad:py3-restricted-codehosting-session
Merge into: launchpad:master
Diff against target: 189 lines (+30/-28)
2 files modified
lib/lp/codehosting/sshserver/session.py (+4/-3)
lib/lp/codehosting/sshserver/tests/test_session.py (+26/-25)
Reviewer Review Type Date Requested Status
Cristian Gonzalez (community) Approve
Review via email: mp+397325@code.launchpad.net

Commit message

Fix restricted codehosting session for Python 3

Description of the change

The command to execute is passed to us as bytes, not text.

To post a comment you must log in.
Revision history for this message
Cristian Gonzalez (cristiangsp) wrote :

Looks good!

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
diff --git a/lib/lp/codehosting/sshserver/session.py b/lib/lp/codehosting/sshserver/session.py
index 482dd4f..28a0a69 100644
--- a/lib/lp/codehosting/sshserver/session.py
+++ b/lib/lp/codehosting/sshserver/session.py
@@ -14,6 +14,7 @@ import os
1414
15from lazr.sshserver.events import AvatarEvent15from lazr.sshserver.events import AvatarEvent
16from lazr.sshserver.session import DoNothingSession16from lazr.sshserver.session import DoNothingSession
17import six
17from six.moves.urllib.parse import urlparse18from six.moves.urllib.parse import urlparse
18from twisted.internet import process19from twisted.internet import process
19from twisted.python import log20from twisted.python import log
@@ -107,7 +108,7 @@ class ExecOnlySession(DoNothingSession):
107 executable and arguments is a sequence of command-line arguments108 executable and arguments is a sequence of command-line arguments
108 with the name of the executable as the first value.109 with the name of the executable as the first value.
109 """110 """
110 args = command.split()111 args = six.ensure_binary(command).split()
111 return args[0], args112 return args[0], args
112113
113114
@@ -161,8 +162,8 @@ def lookup_command_template(command):
161 command_template = python_command + args162 command_template = python_command + args
162163
163 if command in (164 if command in (
164 'bzr serve --inet --directory=/ --allow-writes',165 b'bzr serve --inet --directory=/ --allow-writes',
165 'brz serve --inet --directory=/ --allow-writes'):166 b'brz serve --inet --directory=/ --allow-writes'):
166 return command_template167 return command_template
167 # At the moment, only bzr/brz branch serving is allowed.168 # At the moment, only bzr/brz branch serving is allowed.
168 raise ForbiddenCommand("Not allowed to execute %r." % (command,))169 raise ForbiddenCommand("Not allowed to execute %r." % (command,))
diff --git a/lib/lp/codehosting/sshserver/tests/test_session.py b/lib/lp/codehosting/sshserver/tests/test_session.py
index fb07cea..305c65b 100644
--- a/lib/lp/codehosting/sshserver/tests/test_session.py
+++ b/lib/lp/codehosting/sshserver/tests/test_session.py
@@ -77,9 +77,9 @@ class MockProcessTransport:
77 self.log.append(('childConnectionLost', childFD, reason))77 self.log.append(('childConnectionLost', childFD, reason))
7878
79 def signalProcess(self, signal):79 def signalProcess(self, signal):
80 if self._executable == 'raise-os-error':80 if self._executable == b'raise-os-error':
81 raise OSError()81 raise OSError()
82 if self._executable == 'already-terminated':82 if self._executable == b'already-terminated':
83 raise ProcessExitedAlready()83 raise ProcessExitedAlready()
84 self.log.append(('signalProcess', signal))84 self.log.append(('signalProcess', signal))
8585
@@ -116,7 +116,7 @@ class TestExecOnlySession(AvatarTestCase):
116116
117 def test_openShellNotImplemented(self):117 def test_openShellNotImplemented(self):
118 # openShell closes the connection.118 # openShell closes the connection.
119 protocol = MockProcessTransport('bash')119 protocol = MockProcessTransport(b'bash')
120 self.session.openShell(protocol)120 self.session.openShell(protocol)
121 self.assertEqual(121 self.assertEqual(
122 [('writeExtended', connection.EXTENDED_DATA_STDERR,122 [('writeExtended', connection.EXTENDED_DATA_STDERR,
@@ -148,7 +148,7 @@ class TestExecOnlySession(AvatarTestCase):
148 # inside, it tells the process transport to end the connection between148 # inside, it tells the process transport to end the connection between
149 # the SSH server and the child process.149 # the SSH server and the child process.
150 protocol = ProcessProtocol()150 protocol = ProcessProtocol()
151 self.session.execCommand(protocol, 'cat /etc/hostname')151 self.session.execCommand(protocol, b'cat /etc/hostname')
152 self.session.closed()152 self.session.closed()
153 self.assertEqual(153 self.assertEqual(
154 [('signalProcess', 'HUP'), ('loseConnection',)],154 [('signalProcess', 'HUP'), ('loseConnection',)],
@@ -160,7 +160,7 @@ class TestExecOnlySession(AvatarTestCase):
160 protocol = ProcessProtocol()160 protocol = ProcessProtocol()
161 # MockTransport will raise an OSError on signalProcess if the executed161 # MockTransport will raise an OSError on signalProcess if the executed
162 # command is 'raise-os-error'.162 # command is 'raise-os-error'.
163 self.session.execCommand(protocol, 'raise-os-error')163 self.session.execCommand(protocol, b'raise-os-error')
164 self.session.closed()164 self.session.closed()
165 self.assertEqual(165 self.assertEqual(
166 [('loseConnection',)],166 [('loseConnection',)],
@@ -172,22 +172,22 @@ class TestExecOnlySession(AvatarTestCase):
172 protocol = ProcessProtocol()172 protocol = ProcessProtocol()
173 # MockTransport will raise a ProcessExitedAlready on signalProcess if173 # MockTransport will raise a ProcessExitedAlready on signalProcess if
174 # the executed command is 'already-terminated'.174 # the executed command is 'already-terminated'.
175 self.session.execCommand(protocol, 'already-terminated')175 self.session.execCommand(protocol, b'already-terminated')
176 self.session.closed()176 self.session.closed()
177 self.assertEqual([('loseConnection',)], self.session._transport.log)177 self.assertEqual([('loseConnection',)], self.session._transport.log)
178178
179 def test_getCommandToRunSplitsCommandLine(self):179 def test_getCommandToRunSplitsCommandLine(self):
180 # getCommandToRun takes a command line and splits it into the name of180 # getCommandToRun takes a command line and splits it into the name of
181 # an executable to run and a sequence of arguments.181 # an executable to run and a sequence of arguments.
182 command = 'cat foo bar'182 command = b'cat foo bar'
183 executable, arguments = self.session.getCommandToRun(command)183 executable, arguments = self.session.getCommandToRun(command)
184 self.assertEqual('cat', executable)184 self.assertEqual(b'cat', executable)
185 self.assertEqual(['cat', 'foo', 'bar'], list(arguments))185 self.assertEqual([b'cat', b'foo', b'bar'], list(arguments))
186186
187 def test_execCommandSpawnsProcess(self):187 def test_execCommandSpawnsProcess(self):
188 # ExecOnlySession.execCommand spawns the appropriate process.188 # ExecOnlySession.execCommand spawns the appropriate process.
189 protocol = ProcessProtocol()189 protocol = ProcessProtocol()
190 command = 'cat /etc/hostname'190 command = b'cat /etc/hostname'
191 self.session.execCommand(protocol, command)191 self.session.execCommand(protocol, command)
192 executable, arguments = self.session.getCommandToRun(command)192 executable, arguments = self.session.getCommandToRun(command)
193 self.assertEqual([(protocol, executable, arguments, None, None,193 self.assertEqual([(protocol, executable, arguments, None, None,
@@ -204,7 +204,7 @@ class TestExecOnlySession(AvatarTestCase):
204 # 'eofReceived' closes standard input when called while a command is204 # 'eofReceived' closes standard input when called while a command is
205 # running.205 # running.
206 protocol = ProcessProtocol()206 protocol = ProcessProtocol()
207 self.session.execCommand(protocol, 'cat /etc/hostname')207 self.session.execCommand(protocol, b'cat /etc/hostname')
208 self.session.eofReceived()208 self.session.eofReceived()
209 self.assertEqual([('closeStdin',)], self.session._transport.log)209 self.assertEqual([('closeStdin',)], self.session._transport.log)
210210
@@ -227,10 +227,10 @@ class TestExecOnlySession(AvatarTestCase):
227 session = ExecOnlySession(227 session = ExecOnlySession(
228 self.avatar, self.reactor, environment={'FOO': 'BAR'})228 self.avatar, self.reactor, environment={'FOO': 'BAR'})
229 protocol = ProcessProtocol()229 protocol = ProcessProtocol()
230 session.execCommand(protocol, 'yes')230 session.execCommand(protocol, b'yes')
231 self.assertEqual({'FOO': 'BAR'}, session.environment)231 self.assertEqual({'FOO': 'BAR'}, session.environment)
232 self.assertEqual(232 self.assertEqual(
233 [(protocol, 'yes', ['yes'], {'FOO': 'BAR'}, None, None, None, 0,233 [(protocol, b'yes', [b'yes'], {'FOO': 'BAR'}, None, None, None, 0,
234 None)],234 None)],
235 self.reactor.log)235 self.reactor.log)
236236
@@ -289,12 +289,12 @@ class TestRestrictedExecOnlySession(AvatarTestCase):
289 # Note that Conch doesn't have a well-defined way of rejecting289 # Note that Conch doesn't have a well-defined way of rejecting
290 # commands. Disconnecting in execCommand will do. We don't raise290 # commands. Disconnecting in execCommand will do. We don't raise
291 # an exception to avoid logging an OOPS.291 # an exception to avoid logging an OOPS.
292 protocol = MockProcessTransport('cat')292 protocol = MockProcessTransport(b'cat')
293 self.assertEqual(293 self.assertEqual(
294 None, self.session.execCommand(protocol, 'cat'))294 None, self.session.execCommand(protocol, b'cat'))
295 self.assertEqual(295 self.assertEqual(
296 [('writeExtended', connection.EXTENDED_DATA_STDERR,296 [('writeExtended', connection.EXTENDED_DATA_STDERR,
297 "Not allowed to execute 'cat'.\r\n"),297 "Not allowed to execute %r.\r\n" % b'cat'),
298 ('loseConnection',)],298 ('loseConnection',)],
299 protocol.log)299 protocol.log)
300300
@@ -303,9 +303,10 @@ class TestRestrictedExecOnlySession(AvatarTestCase):
303 # executable and arguments corresponding to the provided executed303 # executable and arguments corresponding to the provided executed
304 # command template.304 # command template.
305 executable, arguments = self.session.getCommandToRun('foo')305 executable, arguments = self.session.getCommandToRun('foo')
306 self.assertEqual('bar', executable)306 self.assertEqual(b'bar', executable)
307 self.assertEqual(307 self.assertEqual(
308 ['bar', 'baz', str(self.avatar.user_id)], list(arguments))308 [b'bar', b'baz', str(self.avatar.user_id).encode('UTF-8')],
309 list(arguments))
309310
310 def test_getAvatarAdapter(self):311 def test_getAvatarAdapter(self):
311 # getAvatarAdapter is a convenience classmethod so that312 # getAvatarAdapter is a convenience classmethod so that
@@ -357,15 +358,15 @@ class TestSessionIntegration(AvatarTestCase):
357 session.environment['BRZ_EMAIL'])358 session.environment['BRZ_EMAIL'])
358359
359 executable, arguments = session.getCommandToRun(360 executable, arguments = session.getCommandToRun(
360 'bzr serve --inet --directory=/ --allow-writes')361 b'bzr serve --inet --directory=/ --allow-writes')
361 interpreter = '%s/bin/py' % config.root362 interpreter = ('%s/bin/py' % config.root).encode('UTF-8')
362 self.assertEqual(interpreter, executable)363 self.assertEqual(interpreter, executable)
363 self.assertEqual(364 self.assertEqual(
364 [interpreter, get_brz_path(), 'lp-serve',365 [interpreter, get_brz_path().encode('UTF-8'), b'lp-serve',
365 '--inet', str(self.avatar.user_id)],366 b'--inet', str(self.avatar.user_id).encode('UTF-8')],
366 list(arguments))367 list(arguments))
367 self.assertRaises(368 self.assertRaises(
368 ForbiddenCommand, session.getCommandToRun, 'rm -rf /')369 ForbiddenCommand, session.getCommandToRun, b'rm -rf /')
369370
370371
371class TestLookupCommand(TestCase):372class TestLookupCommand(TestCase):
@@ -378,11 +379,11 @@ class TestLookupCommand(TestCase):
378 config.root + '/bin/py ' + get_brz_path() +379 config.root + '/bin/py ' + get_brz_path() +
379 ' lp-serve --inet %(user_id)s',380 ' lp-serve --inet %(user_id)s',
380 lookup_command_template(381 lookup_command_template(
381 'bzr serve --inet --directory=/ --allow-writes'))382 b'bzr serve --inet --directory=/ --allow-writes'))
382383
383 def test_brz(self):384 def test_brz(self):
384 self.assertEqual(385 self.assertEqual(
385 config.root + '/bin/py ' + get_brz_path() +386 config.root + '/bin/py ' + get_brz_path() +
386 ' lp-serve --inet %(user_id)s',387 ' lp-serve --inet %(user_id)s',
387 lookup_command_template(388 lookup_command_template(
388 'brz serve --inet --directory=/ --allow-writes'))389 b'brz serve --inet --directory=/ --allow-writes'))

Subscribers

People subscribed via source and target branches

to status/vote changes: