Merge lp:~cjwatson/launchpad/split-lazr.sshserver into lp:launchpad

Proposed by Colin Watson on 2015-01-06
Status: Merged
Approved by: Colin Watson on 2015-01-13
Approved revision: no longer in the source branch.
Merged at revision: 17302
Proposed branch: lp:~cjwatson/launchpad/split-lazr.sshserver
Merge into: lp:launchpad
Diff against target: 2125 lines (+38/-1808)
25 files modified
daemons/poppy-sftp.tac (+7/-5)
daemons/sftp.tac (+1/-3)
lib/lp/codehosting/sftp.py (+1/-1)
lib/lp/codehosting/sshserver/daemon.py (+4/-5)
lib/lp/codehosting/sshserver/session.py (+2/-2)
lib/lp/codehosting/sshserver/tests/test_daemon.py (+17/-2)
lib/lp/codehosting/tests/test_sftp.py (+1/-1)
lib/lp/poppy/tests/test_twistedsftp.py (+1/-1)
lib/lp/poppy/twistedsftp.py (+2/-2)
lib/lp/services/sshserver/__init__.py (+0/-8)
lib/lp/services/sshserver/accesslog.py (+0/-86)
lib/lp/services/sshserver/auth.py (+0/-308)
lib/lp/services/sshserver/events.py (+0/-148)
lib/lp/services/sshserver/service.py (+0/-191)
lib/lp/services/sshserver/session.py (+0/-124)
lib/lp/services/sshserver/sftp.py (+0/-45)
lib/lp/services/sshserver/tests/__init__.py (+0/-8)
lib/lp/services/sshserver/tests/keys/ssh_host_key_rsa (+0/-15)
lib/lp/services/sshserver/tests/keys/ssh_host_key_rsa.pub (+0/-1)
lib/lp/services/sshserver/tests/test_accesslog.py (+0/-146)
lib/lp/services/sshserver/tests/test_auth.py (+0/-516)
lib/lp/services/sshserver/tests/test_events.py (+0/-91)
lib/lp/services/sshserver/tests/test_session.py (+0/-99)
setup.py (+1/-0)
versions.cfg (+1/-0)
To merge this branch: bzr merge lp:~cjwatson/launchpad/split-lazr.sshserver
Reviewer Review Type Date Requested Status
William Grant code 2015-01-06 Approve on 2015-01-13
Review via email: mp+245647@code.launchpad.net

Commit Message

Split out lp.services.sshserver to lazr.sshserver.

Description of the Change

Split out lp.services.sshserver to lazr.sshserver.

To post a comment you must log in.
William Grant (wgrant) :
review: Approve (code)

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'daemons/poppy-sftp.tac'
2--- daemons/poppy-sftp.tac 2012-03-26 05:50:20 +0000
3+++ daemons/poppy-sftp.tac 2015-01-12 18:55:56 +0000
4@@ -7,6 +7,13 @@
5
6 import logging
7
8+from lazr.sshserver.auth import (
9+ LaunchpadAvatar,
10+ PublicKeyFromLaunchpadChecker,
11+ )
12+from lazr.sshserver.service import SSHService
13+from lazr.sshserver.session import DoNothingSession
14+
15 from twisted.application import service
16 from twisted.conch.interfaces import ISession
17 from twisted.conch.ssh import filetransfer
18@@ -26,10 +33,6 @@
19 FTPServiceFactory,
20 )
21 from lp.poppy.twistedsftp import SFTPServer
22-from lp.services.sshserver.auth import (
23- LaunchpadAvatar, PublicKeyFromLaunchpadChecker)
24-from lp.services.sshserver.service import SSHService
25-from lp.services.sshserver.session import DoNothingSession
26 from lp.services.twistedsupport.loggingsupport import set_up_oops_reporting
27
28
29@@ -106,7 +109,6 @@
30 portal=make_portal(),
31 private_key_path=config.poppy.host_key_private,
32 public_key_path=config.poppy.host_key_public,
33- oops_configuration='poppy',
34 main_log='poppy',
35 access_log='poppy.access',
36 access_log_path=config.poppy.access_log,
37
38=== modified file 'daemons/sftp.tac'
39--- daemons/sftp.tac 2011-12-29 05:29:36 +0000
40+++ daemons/sftp.tac 2015-01-12 18:55:56 +0000
41@@ -5,6 +5,7 @@
42 # twistd -noy sftp.tac
43 # or similar. Refer to the twistd(1) man page for details.
44
45+from lazr.sshserver.service import SSHService
46 from twisted.application import service
47 from twisted.protocols.policies import TimeoutFactory
48
49@@ -16,11 +17,9 @@
50 get_key_path,
51 LOG_NAME,
52 make_portal,
53- OOPS_CONFIG_SECTION,
54 PRIVATE_KEY_FILE,
55 PUBLIC_KEY_FILE,
56 )
57-from lp.services.sshserver.service import SSHService
58 from lp.services.twistedsupport.gracefulshutdown import (
59 ConnTrackingFactoryWrapper,
60 make_web_status_service,
61@@ -55,7 +54,6 @@
62 portal=make_portal(),
63 private_key_path=get_key_path(PRIVATE_KEY_FILE),
64 public_key_path=get_key_path(PUBLIC_KEY_FILE),
65- oops_configuration=OOPS_CONFIG_SECTION,
66 main_log=LOG_NAME,
67 access_log=ACCESS_LOG_NAME,
68 access_log_path=config.codehosting.access_log,
69
70=== modified file 'lib/lp/codehosting/sftp.py'
71--- lib/lp/codehosting/sftp.py 2012-06-29 08:40:05 +0000
72+++ lib/lp/codehosting/sftp.py 2015-01-12 18:55:56 +0000
73@@ -30,6 +30,7 @@
74 urlutils,
75 )
76 from bzrlib.transport.local import LocalTransport
77+from lazr.sshserver.sftp import FileIsADirectory
78 from twisted.conch.interfaces import (
79 ISFTPFile,
80 ISFTPServer,
81@@ -45,7 +46,6 @@
82 LaunchpadServer,
83 )
84 from lp.services.config import config
85-from lp.services.sshserver.sftp import FileIsADirectory
86 from lp.services.twistedsupport import gatherResults
87
88
89
90=== modified file 'lib/lp/codehosting/sshserver/daemon.py'
91--- lib/lp/codehosting/sshserver/daemon.py 2012-01-01 02:58:52 +0000
92+++ lib/lp/codehosting/sshserver/daemon.py 2015-01-12 18:55:56 +0000
93@@ -17,6 +17,10 @@
94
95 import os
96
97+from lazr.sshserver.auth import (
98+ LaunchpadAvatar,
99+ PublicKeyFromLaunchpadChecker,
100+ )
101 from twisted.conch.interfaces import ISession
102 from twisted.conch.ssh import filetransfer
103 from twisted.cred.portal import (
104@@ -30,17 +34,12 @@
105 from lp.codehosting import sftp
106 from lp.codehosting.sshserver.session import launch_smart_server
107 from lp.services.config import config
108-from lp.services.sshserver.auth import (
109- LaunchpadAvatar,
110- PublicKeyFromLaunchpadChecker,
111- )
112
113 # The names of the key files of the server itself. The directory itself is
114 # given in config.codehosting.host_key_pair_path.
115 PRIVATE_KEY_FILE = 'ssh_host_key_rsa'
116 PUBLIC_KEY_FILE = 'ssh_host_key_rsa.pub'
117
118-OOPS_CONFIG_SECTION = 'codehosting'
119 LOG_NAME = 'codehosting'
120 ACCESS_LOG_NAME = 'codehosting.access'
121
122
123=== modified file 'lib/lp/codehosting/sshserver/session.py'
124--- lib/lp/codehosting/sshserver/session.py 2012-06-29 08:40:05 +0000
125+++ lib/lp/codehosting/sshserver/session.py 2015-01-12 18:55:56 +0000
126@@ -14,6 +14,8 @@
127 import sys
128 import urlparse
129
130+from lazr.sshserver.events import AvatarEvent
131+from lazr.sshserver.session import DoNothingSession
132 from twisted.internet import (
133 error,
134 interfaces,
135@@ -25,8 +27,6 @@
136
137 from lp.codehosting import get_bzr_path
138 from lp.services.config import config
139-from lp.services.sshserver.events import AvatarEvent
140-from lp.services.sshserver.session import DoNothingSession
141
142
143 class BazaarSSHStarted(AvatarEvent):
144
145=== modified file 'lib/lp/codehosting/sshserver/tests/test_daemon.py'
146--- lib/lp/codehosting/sshserver/tests/test_daemon.py 2010-10-30 22:44:21 +0000
147+++ lib/lp/codehosting/sshserver/tests/test_daemon.py 2015-01-12 18:55:56 +0000
148@@ -5,6 +5,11 @@
149
150 __metaclass__ = type
151
152+from lazr.sshserver.auth import (
153+ NoSuchPersonWithName,
154+ SSHUserAuthServer,
155+ )
156+from lazr.sshserver.service import Factory
157 from twisted.conch.ssh.common import NS
158 from twisted.conch.ssh.keys import Key
159 from twisted.test.proto_helpers import StringTransport
160@@ -15,9 +20,8 @@
161 PRIVATE_KEY_FILE,
162 PUBLIC_KEY_FILE,
163 )
164-from lp.services.sshserver.auth import SSHUserAuthServer
165-from lp.services.sshserver.service import Factory
166 from lp.testing import TestCase
167+from lp.xmlrpc import faults
168
169
170 class StringTransportWith_setTcpKeepAlive(StringTransport):
171@@ -85,3 +89,14 @@
172 mind2 = server_transport2.service.getMind()
173
174 self.assertIsNot(mind1.cache, mind2.cache)
175+
176+
177+class TestXMLRPC(TestCase):
178+ """Test XML-RPC protocol integrity."""
179+
180+ def test_NoSuchPersonWithName_error_code(self):
181+ # The error code for NoSuchPersonWithName in lazr.sshserver matches
182+ # that in lp.xmlrpc.faults.
183+ self.assertEqual(
184+ faults.NoSuchPersonWithName.error_code,
185+ NoSuchPersonWithName.error_code)
186
187=== modified file 'lib/lp/codehosting/tests/test_sftp.py'
188--- lib/lp/codehosting/tests/test_sftp.py 2011-12-22 09:05:46 +0000
189+++ lib/lp/codehosting/tests/test_sftp.py 2015-01-12 18:55:56 +0000
190@@ -13,6 +13,7 @@
191 from bzrlib.tests import TestCaseInTempDir
192 from bzrlib.transport import get_transport
193 from bzrlib.transport.memory import MemoryTransport
194+from lazr.sshserver.sftp import FileIsADirectory
195 from testtools.deferredruntest import (
196 assert_fails_with,
197 AsynchronousDeferredRunTest,
198@@ -33,7 +34,6 @@
199 TransportSFTPServer,
200 )
201 from lp.codehosting.sshserver.daemon import CodehostingAvatar
202-from lp.services.sshserver.sftp import FileIsADirectory
203 from lp.services.utils import file_exists
204 from lp.testing import TestCase
205 from lp.testing.factory import LaunchpadObjectFactory
206
207=== modified file 'lib/lp/poppy/tests/test_twistedsftp.py'
208--- lib/lp/poppy/tests/test_twistedsftp.py 2011-11-30 16:27:15 +0000
209+++ lib/lp/poppy/tests/test_twistedsftp.py 2015-01-12 18:55:56 +0000
210@@ -8,9 +8,9 @@
211 import os
212
213 from fixtures import TempDir
214+from lazr.sshserver.sftp import FileIsADirectory
215
216 from lp.poppy.twistedsftp import SFTPServer
217-from lp.services.sshserver.sftp import FileIsADirectory
218 from lp.testing import (
219 NestedTempfile,
220 TestCase,
221
222=== modified file 'lib/lp/poppy/twistedsftp.py'
223--- lib/lp/poppy/twistedsftp.py 2012-06-29 08:40:05 +0000
224+++ lib/lp/poppy/twistedsftp.py 2015-01-12 18:55:56 +0000
225@@ -14,6 +14,8 @@
226 import os
227 import tempfile
228
229+from lazr.sshserver.events import SFTPClosed
230+from lazr.sshserver.sftp import FileIsADirectory
231 from twisted.conch.interfaces import (
232 ISFTPFile,
233 ISFTPServer,
234@@ -26,8 +28,6 @@
235
236 from lp.poppy.filesystem import UploadFileSystem
237 from lp.poppy.hooks import Hooks
238-from lp.services.sshserver.events import SFTPClosed
239-from lp.services.sshserver.sftp import FileIsADirectory
240
241
242 class SFTPServer:
243
244=== removed directory 'lib/lp/services/sshserver'
245=== removed file 'lib/lp/services/sshserver/__init__.py'
246--- lib/lp/services/sshserver/__init__.py 2010-04-15 14:49:55 +0000
247+++ lib/lp/services/sshserver/__init__.py 1970-01-01 00:00:00 +0000
248@@ -1,8 +0,0 @@
249-# Copyright 2010 Canonical Ltd. This software is licensed under the
250-# GNU Affero General Public License version 3 (see the file LICENSE).
251-
252-"""The Launchpad SSH server."""
253-
254-__metaclass__ = type
255-__all__ = []
256-
257
258=== removed file 'lib/lp/services/sshserver/accesslog.py'
259--- lib/lp/services/sshserver/accesslog.py 2014-08-29 04:29:16 +0000
260+++ lib/lp/services/sshserver/accesslog.py 1970-01-01 00:00:00 +0000
261@@ -1,86 +0,0 @@
262-# Copyright 2009 Canonical Ltd. This software is licensed under the
263-# GNU Affero General Public License version 3 (see the file LICENSE).
264-
265-"""Logging for the SSH server."""
266-
267-__metaclass__ = type
268-__all__ = [
269- 'LoggingManager',
270- ]
271-
272-import logging
273-from logging.handlers import WatchedFileHandler
274-
275-from twisted.python import log as tplog
276-from zope.component import (
277- adapter,
278- getGlobalSiteManager,
279- provideHandler,
280- )
281-# This non-standard import is necessary to hook up the event system.
282-import zope.component.event
283-
284-from lp.services.sshserver.events import ILoggingEvent
285-from lp.services.utils import synchronize
286-
287-
288-class LoggingManager:
289- """Class for managing SSH server logging."""
290-
291- def __init__(self, main_log, access_log, access_log_path):
292- """Construct the logging manager.
293-
294- :param main_log: The main log. Twisted will log to this.
295- :param access_log: The access log object.
296- :param access_log_path: The path to the file where access log
297- messages go.
298- """
299- self._main_log = main_log
300- self._access_log = access_log
301- self._access_log_path = access_log_path
302- self._is_set_up = False
303-
304- def setUp(self):
305- """Set up logging for the smart server.
306-
307- This sets up a debugging handler on the main logger and makes sure
308- that things logged there won't go to stderr. It also sets up an access
309- logger.
310- """
311- log = self._main_log
312- self._orig_level = log.level
313- self._orig_handlers = list(log.handlers)
314- self._orig_observers = list(tplog.theLogPublisher.observers)
315- log.setLevel(logging.INFO)
316- log.addHandler(logging.NullHandler())
317- handler = WatchedFileHandler(self._access_log_path)
318- handler.setFormatter(
319- logging.Formatter("%(asctime)s %(levelname)s %(message)s"))
320- self._access_log.addHandler(handler)
321- self._access_log.setLevel(logging.INFO)
322- # Make sure that our logging event handler is there, ready to receive
323- # logging events.
324- provideHandler(self._log_event)
325- self._is_set_up = True
326-
327- @adapter(ILoggingEvent)
328- def _log_event(self, event):
329- """Log 'event' to the access log."""
330- self._access_log.log(event.level, event.message)
331-
332- def tearDown(self):
333- if not self._is_set_up:
334- return
335- log = self._main_log
336- log.level = self._orig_level
337- synchronize(
338- log.handlers, self._orig_handlers, log.addHandler,
339- log.removeHandler)
340- synchronize(
341- self._access_log.handlers, self._orig_handlers,
342- self._access_log.addHandler, self._access_log.removeHandler)
343- synchronize(
344- tplog.theLogPublisher.observers, self._orig_observers,
345- tplog.addObserver, tplog.removeObserver)
346- getGlobalSiteManager().unregisterHandler(self._log_event)
347- self._is_set_up = False
348
349=== removed file 'lib/lp/services/sshserver/auth.py'
350--- lib/lp/services/sshserver/auth.py 2014-01-30 15:04:06 +0000
351+++ lib/lp/services/sshserver/auth.py 1970-01-01 00:00:00 +0000
352@@ -1,308 +0,0 @@
353-# Copyright 2009 Canonical Ltd. This software is licensed under the
354-# GNU Affero General Public License version 3 (see the file LICENSE).
355-
356-"""Custom authentication for the SSH server.
357-
358-Launchpad's SSH server authenticates users against a XML-RPC service (see
359-`lp.services.authserver.interfaces.IAuthServer` and
360-`PublicKeyFromLaunchpadChecker`) and provides richer error messages in the
361-case of failed authentication (see `SSHUserAuthServer`).
362-"""
363-
364-__metaclass__ = type
365-__all__ = [
366- 'LaunchpadAvatar',
367- 'PublicKeyFromLaunchpadChecker',
368- 'SSHUserAuthServer',
369- ]
370-
371-import binascii
372-
373-from twisted.conch import avatar
374-from twisted.conch.checkers import SSHPublicKeyDatabase
375-from twisted.conch.error import ConchError
376-from twisted.conch.interfaces import IConchUser
377-from twisted.conch.ssh import (
378- keys,
379- userauth,
380- )
381-from twisted.conch.ssh.common import (
382- getNS,
383- NS,
384- )
385-from twisted.cred import credentials
386-from twisted.cred.checkers import ICredentialsChecker
387-from twisted.cred.error import UnauthorizedLogin
388-from twisted.internet import defer
389-from twisted.python import failure
390-from zope.event import notify
391-from zope.interface import implements
392-
393-from lp.services.sshserver import events
394-from lp.services.sshserver.session import PatchedSSHSession
395-from lp.services.sshserver.sftp import FileTransferServer
396-from lp.services.twistedsupport.xmlrpc import trap_fault
397-from lp.xmlrpc import faults
398-
399-
400-class LaunchpadAvatar(avatar.ConchUser):
401- """An account on the SSH server, corresponding to a Launchpad person.
402-
403- :ivar channelLookup: See `avatar.ConchUser`.
404- :ivar subsystemLookup: See `avatar.ConchUser`.
405- :ivar user_id: The Launchpad database ID of the Person for this account.
406- :ivar username: The Launchpad username for this account.
407- """
408-
409- def __init__(self, user_dict):
410- """Construct a `LaunchpadAvatar`.
411-
412- :param user_dict: The result of a call to
413- `IAuthServer.getUserAndSSHKeys`.
414- """
415- avatar.ConchUser.__init__(self)
416- self.user_id = user_dict['id']
417- self.username = user_dict['name']
418-
419- # Set the only channel as a standard SSH session (with a couple of bug
420- # fixes).
421- self.channelLookup = {'session': PatchedSSHSession}
422- # ...and set the only subsystem to be SFTP.
423- self.subsystemLookup = {'sftp': FileTransferServer}
424-
425- def logout(self):
426- notify(events.UserLoggedOut(self))
427-
428-
429-class UserDisplayedUnauthorizedLogin(UnauthorizedLogin):
430- """UnauthorizedLogin which should be reported to the user."""
431-
432-
433-class ISSHPrivateKeyWithMind(credentials.ISSHPrivateKey):
434- """Marker interface for SSH credentials that reference a Mind."""
435-
436-
437-class SSHPrivateKeyWithMind(credentials.SSHPrivateKey):
438- """SSH credentials that also reference a Mind."""
439-
440- implements(ISSHPrivateKeyWithMind)
441-
442- def __init__(self, username, algName, blob, sigData, signature, mind):
443- credentials.SSHPrivateKey.__init__(
444- self, username, algName, blob, sigData, signature)
445- self.mind = mind
446-
447-
448-class UserDetailsMind:
449- """A 'Mind' object that answers and caches requests for user details.
450-
451- A mind is a (poorly named) concept from twisted.cred that basically can be
452- passed to portal.login to represent the client side view of
453- authentication. In our case we attach a mind to the SSHUserAuthServer
454- object that corresponds to an attempt to authenticate against the server.
455- """
456-
457- def __init__(self):
458- self.cache = {}
459-
460- def lookupUserDetails(self, proxy, username):
461- """Find details for the named user, including registered SSH keys.
462-
463- This method basically wraps `IAuthServer.getUserAndSSHKeys` -- see the
464- documentation of that method for more details -- and caches the
465- details found for any particular user.
466-
467- :param proxy: A twisted.web.xmlrpc.Proxy object for the authentication
468- endpoint.
469- :param username: The username to look up.
470- """
471- if username in self.cache:
472- return defer.succeed(self.cache[username])
473- else:
474- d = proxy.callRemote('getUserAndSSHKeys', username)
475- d.addBoth(self._add_to_cache, username)
476- return d
477-
478- def _add_to_cache(self, result, username):
479- """Add the results to our cache."""
480- self.cache[username] = result
481- return result
482-
483-
484-class SSHUserAuthServer(userauth.SSHUserAuthServer):
485- """Subclass of Conch's SSHUserAuthServer to customize various behaviours.
486-
487- There are two main differences:
488-
489- * We override ssh_USERAUTH_REQUEST to display as a banner the reason why
490- an authentication attempt failed.
491-
492- * We override auth_publickey to create credentials that reference a
493- UserDetailsMind and pass the same mind to self.portal.login.
494-
495- Conch is not written in a way to make this easy; we've had to copy and
496- paste and change the implementations of these methods.
497- """
498-
499- def __init__(self, transport=None, banner=None):
500- self.transport = transport
501- self._banner = banner
502- self._configured_banner_sent = False
503- self._mind = UserDetailsMind()
504- self.interfaceToMethod = userauth.SSHUserAuthServer.interfaceToMethod
505- self.interfaceToMethod[ISSHPrivateKeyWithMind] = 'publickey'
506-
507- def sendBanner(self, text, language='en'):
508- bytes = '\r\n'.join(text.encode('UTF8').splitlines() + [''])
509- self.transport.sendPacket(userauth.MSG_USERAUTH_BANNER,
510- NS(bytes) + NS(language))
511-
512- def _sendConfiguredBanner(self, passed_through):
513- if not self._configured_banner_sent and self._banner:
514- self._configured_banner_sent = True
515- self.sendBanner(self._banner)
516- return passed_through
517-
518- def ssh_USERAUTH_REQUEST(self, packet):
519- # This is copied and pasted from twisted/conch/ssh/userauth.py in
520- # Twisted 8.0.1. We do this so we can add _ebLogToBanner between
521- # two existing errbacks.
522- user, nextService, method, rest = getNS(packet, 3)
523- if user != self.user or nextService != self.nextService:
524- self.authenticatedWith = [] # clear auth state
525- self.user = user
526- self.nextService = nextService
527- self.method = method
528- d = self.tryAuth(method, user, rest)
529- if not d:
530- self._ebBadAuth(failure.Failure(ConchError('auth returned none')))
531- return
532- d.addCallback(self._sendConfiguredBanner)
533- d.addCallbacks(self._cbFinishedAuth)
534- d.addErrback(self._ebMaybeBadAuth)
535- # This line does not appear in the original.
536- d.addErrback(self._ebLogToBanner)
537- d.addErrback(self._ebBadAuth)
538- return d
539-
540- def _cbFinishedAuth(self, result):
541- ret = userauth.SSHUserAuthServer._cbFinishedAuth(self, result)
542- # Tell the avatar about the transport, so we can tie it to the
543- # connection in the logs.
544- avatar = self.transport.avatar
545- avatar.transport = self.transport
546- notify(events.UserLoggedIn(avatar))
547- return ret
548-
549- def _ebLogToBanner(self, reason):
550- reason.trap(UserDisplayedUnauthorizedLogin)
551- self.sendBanner(reason.getErrorMessage())
552- return reason
553-
554- def getMind(self):
555- """Return the mind that should be passed to self.portal.login().
556-
557- If multiple requests to authenticate within this overall login attempt
558- should share state, this method can return the same mind each time.
559- """
560- return self._mind
561-
562- def makePublicKeyCredentials(self, username, algName, blob, sigData,
563- signature):
564- """Construct credentials for a request to login with a public key.
565-
566- Our implementation returns a SSHPrivateKeyWithMind.
567-
568- :param username: The username the request is for.
569- :param algName: The algorithm name for the blob.
570- :param blob: The public key blob as sent by the client.
571- :param sigData: The data the signature was made from.
572- :param signature: The signed data. This is checked to verify that the
573- user owns the private key.
574- """
575- mind = self.getMind()
576- return SSHPrivateKeyWithMind(
577- username, algName, blob, sigData, signature, mind)
578-
579- def auth_publickey(self, packet):
580- # This is copied and pasted from twisted/conch/ssh/userauth.py in
581- # Twisted 8.0.1. We do this so we can customize how the credentials
582- # are built and pass a mind to self.portal.login.
583- hasSig = ord(packet[0])
584- algName, blob, rest = getNS(packet[1:], 2)
585- pubKey = keys.Key.fromString(blob).keyObject
586- signature = hasSig and getNS(rest)[0] or None
587- if hasSig:
588- b = NS(self.transport.sessionID) + \
589- chr(userauth.MSG_USERAUTH_REQUEST) + NS(self.user) + \
590- NS(self.nextService) + NS('publickey') + chr(hasSig) + \
591- NS(keys.objectType(pubKey)) + NS(blob)
592- # The next three lines are different from the original.
593- c = self.makePublicKeyCredentials(
594- self.user, algName, blob, b, signature)
595- return self.portal.login(c, self.getMind(), IConchUser)
596- else:
597- # The next four lines are different from the original.
598- c = self.makePublicKeyCredentials(
599- self.user, algName, blob, None, None)
600- return self.portal.login(
601- c, self.getMind(), IConchUser).addErrback(
602- self._ebCheckKey, packet[1:])
603-
604-
605-class PublicKeyFromLaunchpadChecker(SSHPublicKeyDatabase):
606- """Cred checker for getting public keys from launchpad.
607-
608- It knows how to get the public keys from the authserver.
609- """
610- credentialInterfaces = ISSHPrivateKeyWithMind,
611- implements(ICredentialsChecker)
612-
613- def __init__(self, authserver):
614- self.authserver = authserver
615-
616- def checkKey(self, credentials):
617- """Check whether `credentials` is a valid request to authenticate.
618-
619- We check the key data in credentials against the keys the named user
620- has registered in Launchpad.
621- """
622- d = credentials.mind.lookupUserDetails(
623- self.authserver, credentials.username)
624- d.addCallback(self._checkForAuthorizedKey, credentials)
625- d.addErrback(self._reportNoSuchUser, credentials)
626- return d
627-
628- def _reportNoSuchUser(self, failure, credentials):
629- """Report the user named in the credentials not existing nicely."""
630- trap_fault(failure, faults.NoSuchPersonWithName)
631- raise UserDisplayedUnauthorizedLogin(
632- "No such Launchpad account: %s" % credentials.username)
633-
634- def _checkForAuthorizedKey(self, user_dict, credentials):
635- """Check the key data in credentials against the keys found in LP."""
636- if credentials.algName == 'ssh-dss':
637- wantKeyType = 'DSA'
638- elif credentials.algName == 'ssh-rsa':
639- wantKeyType = 'RSA'
640- else:
641- # unknown key type
642- return False
643-
644- if len(user_dict['keys']) == 0:
645- raise UserDisplayedUnauthorizedLogin(
646- "Launchpad user %r doesn't have a registered SSH key"
647- % credentials.username)
648-
649- for keytype, keytext in user_dict['keys']:
650- if keytype != wantKeyType:
651- continue
652- try:
653- if keytext.decode('base64') == credentials.blob:
654- return True
655- except binascii.Error:
656- continue
657-
658- raise UnauthorizedLogin(
659- "Your SSH key does not match any key registered for Launchpad "
660- "user %s" % credentials.username)
661
662=== removed file 'lib/lp/services/sshserver/events.py'
663--- lib/lp/services/sshserver/events.py 2010-08-20 20:31:18 +0000
664+++ lib/lp/services/sshserver/events.py 1970-01-01 00:00:00 +0000
665@@ -1,148 +0,0 @@
666-# Copyright 2010 Canonical Ltd. This software is licensed under the
667-# GNU Affero General Public License version 3 (see the file LICENSE).
668-
669-"""Events generated by the SSH server."""
670-
671-__metaclass__ = type
672-__all__ = [
673- 'AuthenticationFailed',
674- 'AvatarEvent',
675- 'ILoggingEvent',
676- 'LoggingEvent',
677- 'ServerStarting',
678- 'ServerStopped',
679- 'SFTPClosed',
680- 'SFTPStarted',
681- 'UserConnected',
682- 'UserDisconnected',
683- 'UserLoggedIn',
684- 'UserLoggedOut',
685- ]
686-
687-import logging
688-
689-from zope.interface import (
690- Attribute,
691- implements,
692- Interface,
693- )
694-
695-
696-class ILoggingEvent(Interface):
697- """An event is a logging event if it has a message and a severity level.
698-
699- Events that provide this interface will be logged in the SSH server access
700- log.
701- """
702-
703- level = Attribute("The level to log the event at.")
704- message = Attribute("The message to log.")
705-
706-
707-class LoggingEvent:
708- """An event that can be logged to a Python logger.
709-
710- :ivar level: The level to log itself as. This should be defined as a
711- class variable in subclasses.
712- :ivar template: The format string of the message to log. This should be
713- defined as a class variable in subclasses.
714- """
715-
716- implements(ILoggingEvent)
717-
718- def __init__(self, level=None, template=None, **data):
719- """Construct a logging event.
720-
721- :param level: The level to log the event as. If specified, overrides
722- the 'level' class variable.
723- :param template: The format string of the message to log. If
724- specified, overrides the 'template' class variable.
725- :param **data: Information to be logged. Entries will be substituted
726- into the template and stored as attributes.
727- """
728- if level is not None:
729- self._level = level
730- if template is not None:
731- self.template = template
732- self._data = data
733-
734- @property
735- def level(self):
736- """See `ILoggingEvent`."""
737- return self._level
738-
739- @property
740- def message(self):
741- """See `ILoggingEvent`."""
742- return self.template % self._data
743-
744-
745-class ServerStarting(LoggingEvent):
746-
747- level = logging.INFO
748- template = '---- Server started ----'
749-
750-
751-class ServerStopped(LoggingEvent):
752-
753- level = logging.INFO
754- template = '---- Server stopped ----'
755-
756-
757-class UserConnected(LoggingEvent):
758-
759- level = logging.INFO
760- template = '[%(session_id)s] %(address)s connected.'
761-
762- def __init__(self, transport, address):
763- LoggingEvent.__init__(
764- self, session_id=id(transport), address=address)
765-
766-
767-class AuthenticationFailed(LoggingEvent):
768-
769- level = logging.INFO
770- template = '[%(session_id)s] failed to authenticate.'
771-
772- def __init__(self, transport):
773- LoggingEvent.__init__(self, session_id=id(transport))
774-
775-
776-class UserDisconnected(LoggingEvent):
777-
778- level = logging.INFO
779- template = '[%(session_id)s] disconnected.'
780-
781- def __init__(self, transport):
782- LoggingEvent.__init__(self, session_id=id(transport))
783-
784-
785-class AvatarEvent(LoggingEvent):
786- """Base avatar event."""
787-
788- level = logging.INFO
789-
790- def __init__(self, avatar):
791- self.avatar = avatar
792- LoggingEvent.__init__(
793- self, session_id=id(avatar.transport), username=avatar.username)
794-
795-
796-class UserLoggedIn(AvatarEvent):
797-
798- template = '[%(session_id)s] %(username)s logged in.'
799-
800-
801-class UserLoggedOut(AvatarEvent):
802-
803- template = '[%(session_id)s] %(username)s disconnected.'
804-
805-
806-class SFTPStarted(AvatarEvent):
807-
808- template = '[%(session_id)s] %(username)s started SFTP session.'
809-
810-
811-class SFTPClosed(AvatarEvent):
812-
813- template = '[%(session_id)s] %(username)s closed SFTP session.'
814
815=== removed file 'lib/lp/services/sshserver/service.py'
816--- lib/lp/services/sshserver/service.py 2012-04-16 23:02:44 +0000
817+++ lib/lp/services/sshserver/service.py 1970-01-01 00:00:00 +0000
818@@ -1,191 +0,0 @@
819-# Copyright 2009-2011 Canonical Ltd. This software is licensed under the
820-# GNU Affero General Public License version 3 (see the file LICENSE).
821-
822-"""Twisted `service.Service` class for the Launchpad SSH server.
823-
824-An `SSHService` object can be used to launch the SSH server.
825-"""
826-
827-__metaclass__ = type
828-__all__ = [
829- 'SSHService',
830- ]
831-
832-
833-import logging
834-import os
835-
836-from twisted.application import (
837- service,
838- strports,
839- )
840-from twisted.conch.ssh.factory import SSHFactory
841-from twisted.conch.ssh.keys import Key
842-from twisted.conch.ssh.transport import SSHServerTransport
843-from twisted.internet import defer
844-from zope.event import notify
845-
846-from lp.services.sshserver import (
847- accesslog,
848- events,
849- )
850-from lp.services.sshserver.auth import SSHUserAuthServer
851-from lp.services.twistedsupport import gatherResults
852-
853-
854-class KeepAliveSettingSSHServerTransport(SSHServerTransport):
855-
856- def connectionMade(self):
857- SSHServerTransport.connectionMade(self)
858- self.transport.setTcpKeepAlive(True)
859-
860-
861-class Factory(SSHFactory):
862- """SSH factory that uses Launchpad's custom authentication.
863-
864- This class tells the SSH service to use our custom authentication service
865- and configures the host keys for the SSH server. It also logs connection
866- to and disconnection from the SSH server.
867- """
868-
869- protocol = KeepAliveSettingSSHServerTransport
870-
871- def __init__(self, portal, private_key, public_key, banner=None):
872- """Construct an SSH factory.
873-
874- :param portal: The portal used to turn credentials into users.
875- :param private_key: The private key of the server, must be an RSA
876- key, given as a `twisted.conch.ssh.keys.Key` object.
877- :param public_key: The public key of the server, must be an RSA
878- key, given as a `twisted.conch.ssh.keys.Key` object.
879- :param banner: The text to display when users successfully log in.
880- """
881- # Although 'portal' isn't part of the defined interface for
882- # `SSHFactory`, defining it here is how the `SSHUserAuthServer` gets
883- # at it. (Look for the beautiful line "self.portal =
884- # self.transport.factory.portal").
885- self.portal = portal
886- self.services['ssh-userauth'] = self._makeAuthServer
887- self._private_key = private_key
888- self._public_key = public_key
889- self._banner = banner
890-
891- def _makeAuthServer(self, *args, **kwargs):
892- kwargs['banner'] = self._banner
893- return SSHUserAuthServer(*args, **kwargs)
894-
895- def buildProtocol(self, address):
896- """Build an SSH protocol instance, logging the event.
897-
898- The protocol object we return is slightly modified so that we can hook
899- into the 'connectionLost' event and log the disconnection.
900- """
901- transport = SSHFactory.buildProtocol(self, address)
902- transport._realConnectionLost = transport.connectionLost
903- transport.connectionLost = (
904- lambda reason: self.connectionLost(transport, reason))
905- notify(events.UserConnected(transport, address))
906- return transport
907-
908- def connectionLost(self, transport, reason):
909- """Call 'connectionLost' on 'transport', logging the event."""
910- try:
911- return transport._realConnectionLost(reason)
912- finally:
913- # Conch's userauth module sets 'avatar' on the transport if the
914- # authentication succeeded. Thus, if it's not there,
915- # authentication failed. We can't generate this event from the
916- # authentication layer since:
917- #
918- # a) almost every SSH login has at least one failure to
919- # authenticate due to multiple keys on the client-side.
920- #
921- # b) the server doesn't normally generate a "go away" event.
922- # Rather, the client simply stops trying.
923- if getattr(transport, 'avatar', None) is None:
924- notify(events.AuthenticationFailed(transport))
925- notify(events.UserDisconnected(transport))
926-
927- def getPublicKeys(self):
928- """Return the server's configured public key.
929-
930- See `SSHFactory.getPublicKeys`.
931- """
932- return {'ssh-rsa': self._public_key}
933-
934- def getPrivateKeys(self):
935- """Return the server's configured private key.
936-
937- See `SSHFactory.getPrivateKeys`.
938- """
939- return {'ssh-rsa': self._private_key}
940-
941-
942-class SSHService(service.Service):
943- """A Twisted service for the SSH server."""
944-
945- def __init__(self, portal, private_key_path, public_key_path,
946- oops_configuration, main_log, access_log,
947- access_log_path, strport='tcp:22', factory_decorator=None,
948- banner=None):
949- """Construct an SSH service.
950-
951- :param portal: The `twisted.cred.portal.Portal` that turns
952- authentication requests into views on the system.
953- :param private_key_path: The path to the SSH server's private key.
954- :param public_key_path: The path to the SSH server's public key.
955- :param oops_configuration: The section of the configuration file with
956- the OOPS config details for this server.
957- :param main_log: The name of the logger to log most of the server
958- stuff to.
959- :param access_log: The name of the logger object to log the server
960- access details to.
961- :param access_log_path: The path to the access log file.
962- :param strport: The port to run the server on, expressed in Twisted's
963- "strports" mini-language. Defaults to 'tcp:22'.
964- :param factory_decorator: An optional callable that can decorate the
965- server factory (e.g. with a
966- `twisted.protocols.policies.TimeoutFactory`). It takes one
967- argument, a factory, and must return a factory.
968- :param banner: An announcement printed to users when they connect.
969- By default, announce nothing.
970- """
971- ssh_factory = Factory(
972- portal,
973- private_key=Key.fromFile(private_key_path),
974- public_key=Key.fromFile(public_key_path),
975- banner=banner)
976- if factory_decorator is not None:
977- ssh_factory = factory_decorator(ssh_factory)
978- self.service = strports.service(strport, ssh_factory)
979- self._oops_configuration = oops_configuration
980- self._main_log = main_log
981- self._access_log = access_log
982- self._access_log_path = access_log_path
983-
984- def startService(self):
985- """Start the SSH service."""
986- manager = accesslog.LoggingManager(
987- logging.getLogger(self._main_log),
988- logging.getLogger(self._access_log_path),
989- self._access_log_path)
990- manager.setUp()
991- notify(events.ServerStarting())
992- # By default, only the owner of files should be able to write to them.
993- # Perhaps in the future this line will be deleted and the umask
994- # managed by the startup script.
995- os.umask(0022)
996- service.Service.startService(self)
997- self.service.startService()
998-
999- def stopService(self):
1000- """Stop the SSH service."""
1001- deferred = gatherResults([
1002- defer.maybeDeferred(service.Service.stopService, self),
1003- defer.maybeDeferred(self.service.stopService)])
1004-
1005- def log_stopped(ignored):
1006- notify(events.ServerStopped())
1007- return ignored
1008-
1009- return deferred.addBoth(log_stopped)
1010
1011=== removed file 'lib/lp/services/sshserver/session.py'
1012--- lib/lp/services/sshserver/session.py 2010-08-20 20:31:18 +0000
1013+++ lib/lp/services/sshserver/session.py 1970-01-01 00:00:00 +0000
1014@@ -1,124 +0,0 @@
1015-# Copyright 2010 Canonical Ltd. This software is licensed under the
1016-# GNU Affero General Public License version 3 (see the file LICENSE).
1017-
1018-"""Patched SSH session for the Launchpad server."""
1019-
1020-__metaclass__ = type
1021-__all__ = [
1022- 'DoNothingSession',
1023- 'PatchedSSHSession',
1024- ]
1025-
1026-from twisted.conch.interfaces import ISession
1027-from twisted.conch.ssh import (
1028- channel,
1029- connection,
1030- session,
1031- )
1032-from zope.interface import implements
1033-
1034-
1035-class PatchedSSHSession(session.SSHSession, object):
1036- """Session adapter that corrects bugs in Conch.
1037-
1038- This object provides no custom logic for Launchpad, it just addresses some
1039- simple bugs in the base `session.SSHSession` class that are not yet fixed
1040- upstream.
1041- """
1042-
1043- def closeReceived(self):
1044- # Without this, the client hangs when it's finished transferring.
1045- # XXX: JonathanLange 2009-01-05: This does not appear to have a
1046- # corresponding bug in Twisted. We should test that the above comment
1047- # is indeed correct and then file a bug upstream.
1048- self.loseConnection()
1049-
1050- def loseConnection(self):
1051- # XXX: JonathanLange 2008-03-31: This deliberately replaces the
1052- # implementation of session.SSHSession.loseConnection. The default
1053- # implementation will try to call loseConnection on the client
1054- # transport even if it's None. I don't know *why* it is None, so this
1055- # doesn't necessarily address the root cause.
1056- # See http://twistedmatrix.com/trac/ticket/2754.
1057- transport = getattr(self.client, 'transport', None)
1058- if transport is not None:
1059- transport.loseConnection()
1060- # This is called by session.SSHSession.loseConnection. SSHChannel is
1061- # the base class of SSHSession.
1062- channel.SSHChannel.loseConnection(self)
1063-
1064- def stopWriting(self):
1065- """See `session.SSHSession.stopWriting`.
1066-
1067- When the client can't keep up with us, we ask the child process to
1068- stop giving us data.
1069- """
1070- # XXX: MichaelHudson 2008-06-27: Being cagey about whether
1071- # self.client.transport is entirely paranoia inspired by the comment
1072- # in `loseConnection` above. It would be good to know if and why it is
1073- # necessary. See http://twistedmatrix.com/trac/ticket/2754.
1074- transport = getattr(self.client, 'transport', None)
1075- if transport is not None:
1076- # For SFTP connections, 'transport' is actually a _DummyTransport
1077- # instance. Neither _DummyTransport nor the protocol it wraps
1078- # (filetransfer.FileTransferServer) support pausing.
1079- pauseProducing = getattr(transport, 'pauseProducing', None)
1080- if pauseProducing is not None:
1081- pauseProducing()
1082-
1083- def startWriting(self):
1084- """See `session.SSHSession.startWriting`.
1085-
1086- The client is ready for data again, so ask the child to start
1087- producing data again.
1088- """
1089- # XXX: MichaelHudson 2008-06-27: Being cagey about whether
1090- # self.client.transport is entirely paranoia inspired by the comment
1091- # in `loseConnection` above. It would be good to know if and why it is
1092- # necessary. See http://twistedmatrix.com/trac/ticket/2754.
1093- transport = getattr(self.client, 'transport', None)
1094- if transport is not None:
1095- # For SFTP connections, 'transport' is actually a _DummyTransport
1096- # instance. Neither _DummyTransport nor the protocol it wraps
1097- # (filetransfer.FileTransferServer) support pausing.
1098- resumeProducing = getattr(transport, 'resumeProducing', None)
1099- if resumeProducing is not None:
1100- resumeProducing()
1101-
1102-
1103-class DoNothingSession:
1104- """A Conch user session that allows nothing."""
1105-
1106- implements(ISession)
1107-
1108- def __init__(self, avatar):
1109- self.avatar = avatar
1110-
1111- def closed(self):
1112- """See ISession."""
1113-
1114- def eofReceived(self):
1115- """See ISession."""
1116-
1117- def errorWithMessage(self, protocol, msg):
1118- protocol.session.writeExtended(
1119- connection.EXTENDED_DATA_STDERR, msg)
1120- protocol.loseConnection()
1121-
1122- def execCommand(self, protocol, command):
1123- """See ISession."""
1124- self.errorWithMessage(
1125- protocol, "Not allowed to execute commands on this server.\r\n")
1126-
1127- def getPty(self, term, windowSize, modes):
1128- """See ISession."""
1129- # Do nothing, as we don't provide shell access. openShell will get
1130- # called and handle this error message and disconnect.
1131-
1132- def openShell(self, protocol):
1133- """See ISession."""
1134- self.errorWithMessage(protocol, "No shells on this server.\r\n")
1135-
1136- def windowChanged(self, newWindowSize):
1137- """See ISession."""
1138- raise NotImplementedError(self.windowChanged)
1139
1140=== removed file 'lib/lp/services/sshserver/sftp.py'
1141--- lib/lp/services/sshserver/sftp.py 2010-08-20 20:31:18 +0000
1142+++ lib/lp/services/sshserver/sftp.py 1970-01-01 00:00:00 +0000
1143@@ -1,45 +0,0 @@
1144-# Copyright 2010 Canonical Ltd. This software is licensed under the
1145-# GNU Affero General Public License version 3 (see the file LICENSE).
1146-
1147-"""Generic SFTP server functionality."""
1148-
1149-__metaclass__ = type
1150-__all__ = [
1151- 'FileIsADirectory',
1152- 'FileTransferServer',
1153- ]
1154-
1155-from bzrlib import errors as bzr_errors
1156-from twisted.conch.ssh import filetransfer
1157-from zope.event import notify
1158-
1159-from lp.services.sshserver import events
1160-
1161-
1162-class FileIsADirectory(bzr_errors.PathError):
1163- """Raised when writeChunk is called on a directory.
1164-
1165- This exists mainly to be translated into the appropriate SFTP error.
1166- """
1167-
1168- _fmt = 'File is a directory: %(path)r%(extra)s'
1169-
1170-
1171-class FileTransferServer(filetransfer.FileTransferServer):
1172- """SFTP protocol implementation that logs key events."""
1173-
1174- def __init__(self, data=None, avatar=None):
1175- filetransfer.FileTransferServer.__init__(self, data, avatar)
1176- notify(events.SFTPStarted(avatar))
1177- self.avatar = avatar
1178-
1179- def connectionLost(self, reason):
1180- # This method gets called twice: once from `SSHChannel.closeReceived`
1181- # when the client closes the channel and once from `SSHSession.closed`
1182- # when the server closes the session. We change the avatar attribute
1183- # to avoid logging the `SFTPClosed` event twice.
1184- filetransfer.FileTransferServer.connectionLost(self, reason)
1185- if self.avatar is not None:
1186- avatar = self.avatar
1187- self.avatar = None
1188- notify(events.SFTPClosed(avatar))
1189
1190=== removed directory 'lib/lp/services/sshserver/tests'
1191=== removed file 'lib/lp/services/sshserver/tests/__init__.py'
1192--- lib/lp/services/sshserver/tests/__init__.py 2010-04-15 14:49:55 +0000
1193+++ lib/lp/services/sshserver/tests/__init__.py 1970-01-01 00:00:00 +0000
1194@@ -1,8 +0,0 @@
1195-# Copyright 2010 Canonical Ltd. This software is licensed under the
1196-# GNU Affero General Public License version 3 (see the file LICENSE).
1197-
1198-"""Tests for the Launchpad SSH server."""
1199-
1200-__metaclass__ = type
1201-__all__ = []
1202-
1203
1204=== removed directory 'lib/lp/services/sshserver/tests/keys'
1205=== removed file 'lib/lp/services/sshserver/tests/keys/ssh_host_key_rsa'
1206--- lib/lp/services/sshserver/tests/keys/ssh_host_key_rsa 2010-04-15 15:27:30 +0000
1207+++ lib/lp/services/sshserver/tests/keys/ssh_host_key_rsa 1970-01-01 00:00:00 +0000
1208@@ -1,15 +0,0 @@
1209------BEGIN RSA PRIVATE KEY-----
1210-MIICXAIBAAKBgQDSSpVRPhCiU9PPuZN7QyJdMOgTVwPyYpZGOHutR/9kxFvOLa39
1211-nY0Eqo39OTumfZBMEEVqIPadQanO9LcdTnl9/Z4LcBGn09EFQ2y7VUkC6J2dSQtr
1212-YMY0tV+C5HGZ2oYBWKBl5PZ1RI4+qrJpAMMmINdnF0uEE/x8B1iMWGB3PwIBIwKB
1213-gQCcN2ebb+8ZgBmwQLazVnFMirsHDXClbc630i/9EOmbUAmvGp6B4sCHH5ytevkc
1214-l8pHIget7JnxKXbUQMKKzJTCpPwwEyL3ZVDxYXg37WQU74cVf93CjOjChs+hOeS1
1215-sW5m9JFr9oomL5JWnGXr+TV/kYBCNVW++J1Bckn6kYpH+wJBAPUw0ZunXlBRuugA
1216-YTSmXUUX+ALu6maDD1t7gAk37waxNQMaH5DMk5R4IQtoxeQgCLqL2yEJUqK1lxOy
1217-wlp1k8UCQQDbj+Vr4poE9MpYNtPDiDqv2aXe6CJ3p1qQuNE0rSxk+0G9h3ASISRw
1218-DDLgcapg2xkOvG3pidfAJG9P827XiVszAkEA4CyiYm0jB5suivkIaqa7rOK23hxE
1219-BfQrTFOoQvFPkRcL5ZQ6Hfzes6EIRPIUA8WEUskC3GBLjXLTRTW5Aj+dCwJBAMi+
1220-E5XWfjBqx6EcL1OvwKDG/g2hCZH4GEnNjBLneQveZ/29qEsW/L43CfHHAiyrD5hx
1221-w5OxOklF4h02VrZu9EsCQBEZcTAQOrWnkmp7uBrz1V8nFLAE6zFh/6Wj1Mn8k08j
1222-pcsLJAhm+qlV7EtV/5rk+v3WcXrKiRIiiC0Ron96Dx0=
1223------END RSA PRIVATE KEY-----
1224
1225=== removed file 'lib/lp/services/sshserver/tests/keys/ssh_host_key_rsa.pub'
1226--- lib/lp/services/sshserver/tests/keys/ssh_host_key_rsa.pub 2010-04-15 15:27:30 +0000
1227+++ lib/lp/services/sshserver/tests/keys/ssh_host_key_rsa.pub 1970-01-01 00:00:00 +0000
1228@@ -1,1 +0,0 @@
1229-ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAIEA0kqVUT4QolPTz7mTe0MiXTDoE1cD8mKWRjh7rUf/ZMRbzi2t/Z2NBKqN/Tk7pn2QTBBFaiD2nUGpzvS3HU55ff2eC3ARp9PRBUNsu1VJAuidnUkLa2DGNLVfguRxmdqGAVigZeT2dUSOPqqyaQDDJiDXZxdLhBP8fAdYjFhgdz8= andrew@frobozz
1230
1231=== removed file 'lib/lp/services/sshserver/tests/test_accesslog.py'
1232--- lib/lp/services/sshserver/tests/test_accesslog.py 2012-03-26 06:08:39 +0000
1233+++ lib/lp/services/sshserver/tests/test_accesslog.py 1970-01-01 00:00:00 +0000
1234@@ -1,146 +0,0 @@
1235-# Copyright 2009 Canonical Ltd. This software is licensed under the
1236-# GNU Affero General Public License version 3 (see the file LICENSE).
1237-
1238-"""Tests for the logging system of the sshserver."""
1239-
1240-__metaclass__ = type
1241-
1242-import codecs
1243-import logging
1244-from logging.handlers import WatchedFileHandler
1245-import os
1246-from StringIO import StringIO
1247-import sys
1248-import tempfile
1249-
1250-from bzrlib.tests import TestCase as BzrTestCase
1251-import zope.component.event
1252-
1253-from lp.services.sshserver.accesslog import LoggingManager
1254-from lp.testing import TestCase
1255-
1256-
1257-class LoggingManagerMixin:
1258-
1259- _log_count = 0
1260-
1261- def makeLogger(self, name=None):
1262- if name is None:
1263- self._log_count += 1
1264- name = '%s-%s' % (self.id().split('.')[-1], self._log_count)
1265- return logging.getLogger(name)
1266-
1267- def installLoggingManager(self, main_log=None, access_log=None,
1268- access_log_path=None):
1269- if main_log is None:
1270- main_log = self.makeLogger()
1271- if access_log is None:
1272- access_log = self.makeLogger()
1273- if access_log_path is None:
1274- fd, access_log_path = tempfile.mkstemp()
1275- os.close(fd)
1276- self.addCleanup(os.unlink, access_log_path)
1277- manager = LoggingManager(main_log, access_log, access_log_path)
1278- manager.setUp()
1279- self.addCleanup(manager.tearDown)
1280- return manager
1281-
1282-
1283-class TestLoggingBazaarInteraction(BzrTestCase, LoggingManagerMixin):
1284-
1285- def setUp(self):
1286- BzrTestCase.setUp(self)
1287-
1288- # Trap stderr.
1289- self._real_stderr = sys.stderr
1290- sys.stderr = codecs.getwriter('utf8')(StringIO())
1291-
1292- def tearDown(self):
1293- sys.stderr = self._real_stderr
1294- BzrTestCase.tearDown(self)
1295-
1296- def test_leaves_bzr_handlers_unchanged(self):
1297- # Bazaar's log handling is untouched by logging setup.
1298- root_handlers = logging.getLogger('').handlers
1299- bzr_handlers = logging.getLogger('bzr').handlers
1300-
1301- self.installLoggingManager()
1302-
1303- self.assertEqual(root_handlers, logging.getLogger('').handlers)
1304- self.assertEqual(bzr_handlers, logging.getLogger('bzr').handlers)
1305-
1306- def test_log_doesnt_go_to_stderr(self):
1307- # Once logging setup is called, any messages logged to the
1308- # SSH server logger should *not* be logged to stderr. If they are,
1309- # they will appear on the user's terminal.
1310- log = self.makeLogger()
1311- self.installLoggingManager(log)
1312-
1313- # Make sure that a logged message does not go to stderr.
1314- log.info('Hello hello')
1315- self.assertEqual(sys.stderr.getvalue(), '')
1316-
1317-
1318-class TestLoggingManager(TestCase, LoggingManagerMixin):
1319-
1320- def test_main_log_handlers(self):
1321- # There needs to be at least one handler for the root logger. If there
1322- # isn't, we'll get constant errors complaining about the lack of
1323- # logging handlers.
1324- log = self.makeLogger()
1325- self.assertEqual([], log.handlers)
1326- self.installLoggingManager(log)
1327- self.assertNotEqual([], log.handlers)
1328-
1329- def _get_handlers(self):
1330- registrations = (
1331- zope.component.getGlobalSiteManager().registeredHandlers())
1332- return [
1333- registration.factory
1334- for registration in registrations]
1335-
1336- def test_set_up_registers_event_handler(self):
1337- manager = self.installLoggingManager()
1338- self.assertIn(manager._log_event, self._get_handlers())
1339-
1340- def test_teardown_restores_event_handlers(self):
1341- handlers = self._get_handlers()
1342- manager = self.installLoggingManager()
1343- manager.tearDown()
1344- self.assertEqual(handlers, self._get_handlers())
1345-
1346- def test_teardown_restores_level(self):
1347- log = self.makeLogger()
1348- old_level = log.level
1349- manager = self.installLoggingManager(log)
1350- manager.tearDown()
1351- self.assertEqual(old_level, log.level)
1352-
1353- def test_teardown_restores_main_log_handlers(self):
1354- # tearDown restores log handlers for the main logger.
1355- log = self.makeLogger()
1356- handlers = list(log.handlers)
1357- manager = self.installLoggingManager(log)
1358- manager.tearDown()
1359- self.assertEqual(handlers, log.handlers)
1360-
1361- def test_teardown_restores_access_log_handlers(self):
1362- # tearDown restores log handlers for the access logger.
1363- log = self.makeLogger()
1364- handlers = list(log.handlers)
1365- manager = self.installLoggingManager(access_log=log)
1366- manager.tearDown()
1367- self.assertEqual(handlers, log.handlers)
1368-
1369- def test_access_handlers(self):
1370- # The logging setup installs a rotatable log handler that logs output
1371- # to the SSH server access log.
1372- directory = self.makeTemporaryDirectory()
1373- access_log = self.makeLogger()
1374- access_log_path = os.path.join(directory, 'access.log')
1375- self.installLoggingManager(
1376- access_log=access_log,
1377- access_log_path=access_log_path)
1378- [handler] = access_log.handlers
1379- self.assertIsInstance(handler, WatchedFileHandler)
1380- self.assertEqual(access_log_path, handler.baseFilename)
1381
1382=== removed file 'lib/lp/services/sshserver/tests/test_auth.py'
1383--- lib/lp/services/sshserver/tests/test_auth.py 2012-01-01 02:58:52 +0000
1384+++ lib/lp/services/sshserver/tests/test_auth.py 1970-01-01 00:00:00 +0000
1385@@ -1,516 +0,0 @@
1386-# Copyright 2009-2010 Canonical Ltd. This software is licensed under the
1387-# GNU Affero General Public License version 3 (see the file LICENSE).
1388-
1389-import os
1390-
1391-from testtools.deferredruntest import (
1392- assert_fails_with,
1393- AsynchronousDeferredRunTest,
1394- flush_logged_errors,
1395- )
1396-from twisted.conch.checkers import SSHPublicKeyDatabase
1397-from twisted.conch.error import ConchError
1398-from twisted.conch.ssh import userauth
1399-from twisted.conch.ssh.common import (
1400- getNS,
1401- NS,
1402- )
1403-from twisted.conch.ssh.keys import (
1404- BadKeyError,
1405- Key,
1406- )
1407-from twisted.conch.ssh.transport import (
1408- SSHCiphers,
1409- SSHServerTransport,
1410- )
1411-from twisted.cred.error import UnauthorizedLogin
1412-from twisted.cred.portal import (
1413- IRealm,
1414- Portal,
1415- )
1416-from twisted.internet import defer
1417-from twisted.python import failure
1418-from twisted.python.util import sibpath
1419-from zope.interface import implements
1420-
1421-from lp.services.sshserver import auth
1422-from lp.services.twistedsupport import suppress_stderr
1423-from lp.testing import TestCase
1424-from lp.xmlrpc import faults
1425-
1426-
1427-class MockRealm:
1428- """A mock realm for testing userauth.SSHUserAuthServer.
1429-
1430- This realm is not actually used in the course of testing, so calls to
1431- requestAvatar will raise an exception.
1432- """
1433-
1434- implements(IRealm)
1435-
1436- def requestAvatar(self, avatar_id, mind, *interfaces):
1437- user_dict = {
1438- 'id': avatar_id, 'name': avatar_id, 'teams': [],
1439- 'initialBranches': []}
1440- return (
1441- interfaces[0], auth.LaunchpadAvatar(user_dict), lambda: None)
1442-
1443-
1444-class MockSSHTransport(SSHServerTransport):
1445- """A mock SSH transport for testing userauth.SSHUserAuthServer.
1446-
1447- SSHUserAuthServer expects an SSH transport which has a factory attribute
1448- which in turn has a portal attribute. Because the portal is important for
1449- testing authentication, we need to be able to provide an interesting portal
1450- object to the SSHUserAuthServer.
1451-
1452- In addition, we want to be able to capture any packets sent over the
1453- transport.
1454- """
1455-
1456- class Factory:
1457- def getService(self, transport, nextService):
1458- return lambda: None
1459-
1460- def __init__(self, portal):
1461- # In Twisted 8.0.1, Conch's transport starts referring to
1462- # currentEncryptions where it didn't before. Provide a dummy value for
1463- # it.
1464- self.currentEncryptions = SSHCiphers('none', 'none', 'none', 'none')
1465- self.packets = []
1466- self.factory = self.Factory()
1467- self.factory.portal = portal
1468-
1469- def sendPacket(self, messageType, payload):
1470- self.packets.append((messageType, payload))
1471-
1472- def setService(self, service):
1473- pass
1474-
1475-
1476-class UserAuthServerMixin(object):
1477- def setUp(self):
1478- self.portal = Portal(MockRealm())
1479- self.transport = MockSSHTransport(self.portal)
1480- self.user_auth = auth.SSHUserAuthServer(self.transport)
1481-
1482- def _getMessageName(self, message_type):
1483- """Get the name of the message for the given message type constant."""
1484- return userauth.SSHUserAuthServer.protocolMessages[message_type]
1485-
1486- def assertMessageOrder(self, message_types):
1487- """Assert that SSH messages were sent in the given order."""
1488- messages = userauth.SSHUserAuthServer.protocolMessages
1489- self.assertEqual(
1490- [messages[msg_type] for msg_type in message_types],
1491- [messages[packet_type]
1492- for packet_type, contents in self.transport.packets])
1493-
1494- def assertBannerSent(self, banner_message, expected_language='en'):
1495- """Assert that 'banner_message' was sent as an SSH banner."""
1496- # Check that we received a BANNER, then a FAILURE.
1497- for packet_type, packet_content in self.transport.packets:
1498- if packet_type == userauth.MSG_USERAUTH_BANNER:
1499- bytes, language, empty = getNS(packet_content, 2)
1500- self.assertEqual(banner_message, bytes.decode('UTF8'))
1501- self.assertEqual(expected_language, language)
1502- self.assertEqual('', empty)
1503- break
1504- else:
1505- self.fail("No banner logged.")
1506-
1507-
1508-class TestUserAuthServer(TestCase, UserAuthServerMixin):
1509-
1510- def setUp(self):
1511- TestCase.setUp(self)
1512- UserAuthServerMixin.setUp(self)
1513-
1514- def test_sendBanner(self):
1515- # sendBanner should send an SSH 'packet' with type MSG_USERAUTH_BANNER
1516- # and two fields. The first field is the message itself, and the
1517- # second is the language tag.
1518- #
1519- # sendBanner automatically adds a trailing newline, because openssh
1520- # and Twisted don't add one when displaying the banner.
1521- #
1522- # See RFC 4252, Section 5.4.
1523- message = u"test message"
1524- self.user_auth.sendBanner(message, language='en-US')
1525- self.assertBannerSent(message + '\r\n', 'en-US')
1526- self.assertEqual(
1527- 1, len(self.transport.packets),
1528- "More than just banner was sent: %r" % self.transport.packets)
1529-
1530- def test_sendBannerUsesCRLF(self):
1531- # sendBanner should make sure that any line breaks in the message are
1532- # sent as CR LF pairs.
1533- #
1534- # See RFC 4252, Section 5.4.
1535- self.user_auth.sendBanner(u"test\nmessage")
1536- [(messageType, payload)] = self.transport.packets
1537- bytes, language, empty = getNS(payload, 2)
1538- self.assertEqual(bytes.decode('UTF8'), u"test\r\nmessage\r\n")
1539-
1540- def test_requestRaisesConchError(self):
1541- # ssh_USERAUTH_REQUEST should raise a ConchError if tryAuth returns
1542- # None. Added to catch a bug noticed by pyflakes.
1543- # Whitebox test.
1544- def mock_try_auth(kind, user, data):
1545- return None
1546- def mock_eb_bad_auth(reason):
1547- reason.trap(ConchError)
1548- tryAuth = self.user_auth.tryAuth
1549- self.user_auth.tryAuth = mock_try_auth
1550- _ebBadAuth, self.user_auth._ebBadAuth = (self.user_auth._ebBadAuth,
1551- mock_eb_bad_auth)
1552- self.user_auth.serviceStarted()
1553- try:
1554- packet = NS('jml') + NS('foo') + NS('public_key') + NS('data')
1555- self.user_auth.ssh_USERAUTH_REQUEST(packet)
1556- finally:
1557- self.user_auth.serviceStopped()
1558- self.user_auth.tryAuth = tryAuth
1559- self.user_auth._ebBadAuth = _ebBadAuth
1560-
1561-
1562-class MockChecker(SSHPublicKeyDatabase):
1563- """A very simple public key checker which rejects all offered credentials.
1564-
1565- Used by TestAuthenticationBannerDisplay to test that errors raised by
1566- checkers are sent to SSH clients.
1567- """
1568-
1569- error_message = u'error message'
1570-
1571- def requestAvatarId(self, credentials):
1572- if credentials.username == 'success':
1573- return credentials.username
1574- else:
1575- return failure.Failure(
1576- auth.UserDisplayedUnauthorizedLogin(self.error_message))
1577-
1578-
1579-class TestAuthenticationBannerDisplay(UserAuthServerMixin, TestCase):
1580- """Check that auth error information is passed through to the client.
1581-
1582- Normally, SSH servers provide minimal information on failed authentication.
1583- With Launchpad, much more user information is public, so it is helpful and
1584- not insecure to tell users why they failed to authenticate.
1585-
1586- SSH doesn't provide a standard way of doing this, but the
1587- MSG_USERAUTH_BANNER message is allowed and seems appropriate. See RFC 4252,
1588- Section 5.4 for more information.
1589- """
1590-
1591- run_tests_with = AsynchronousDeferredRunTest
1592-
1593- def setUp(self):
1594- UserAuthServerMixin.setUp(self)
1595- TestCase.setUp(self)
1596- self.portal.registerChecker(MockChecker())
1597- self.user_auth.serviceStarted()
1598- self.key_data = self._makeKey()
1599-
1600- def tearDown(self):
1601- self.user_auth.serviceStopped()
1602- TestCase.tearDown(self)
1603-
1604- def _makeKey(self):
1605- keydir = sibpath(__file__, 'keys')
1606- public_key = Key.fromString(
1607- open(os.path.join(keydir, 'ssh_host_key_rsa.pub'), 'rb').read())
1608- if isinstance(public_key, str):
1609- return chr(0) + NS('rsa') + NS(public_key)
1610- else:
1611- return chr(0) + NS('rsa') + NS(public_key.blob())
1612-
1613- def requestFailedAuthentication(self):
1614- return self.user_auth.ssh_USERAUTH_REQUEST(
1615- NS('failure') + NS('') + NS('publickey') + self.key_data)
1616-
1617- def requestSuccessfulAuthentication(self):
1618- return self.user_auth.ssh_USERAUTH_REQUEST(
1619- NS('success') + NS('') + NS('publickey') + self.key_data)
1620-
1621- def requestUnsupportedAuthentication(self):
1622- # Note that it doesn't matter how the checker responds -- the server
1623- # doesn't get that far.
1624- return self.user_auth.ssh_USERAUTH_REQUEST(
1625- NS('success') + NS('') + NS('none') + NS(''))
1626-
1627- def test_bannerNotSentOnSuccess(self):
1628- # No banner is printed when the user authenticates successfully.
1629- self.user_auth._banner = None
1630-
1631- d = self.requestSuccessfulAuthentication()
1632- def check(ignored):
1633- # Check that no banner was sent to the user.
1634- self.assertMessageOrder([userauth.MSG_USERAUTH_SUCCESS])
1635- return d.addCallback(check)
1636-
1637- def test_defaultBannerSentOnSuccess(self):
1638- # If a banner was passed to the user auth agent then we send it to the
1639- # user when they log in.
1640- self.user_auth._banner = "Boogedy boo"
1641- d = self.requestSuccessfulAuthentication()
1642- def check(ignored):
1643- self.assertMessageOrder(
1644- [userauth.MSG_USERAUTH_BANNER, userauth.MSG_USERAUTH_SUCCESS])
1645- self.assertBannerSent(self.user_auth._banner + '\r\n')
1646- return d.addCallback(check)
1647-
1648- def test_defaultBannerSentOnlyOnce(self):
1649- # We don't send the banner on each authentication attempt, just on the
1650- # first one. It is usual for there to be many authentication attempts
1651- # per SSH session.
1652- self.user_auth._banner = "Boogedy boo"
1653-
1654- d = self.requestUnsupportedAuthentication()
1655- d.addCallback(lambda ignored: self.requestSuccessfulAuthentication())
1656-
1657- def check(ignored):
1658- # Check that no banner was sent to the user.
1659- self.assertMessageOrder(
1660- [userauth.MSG_USERAUTH_FAILURE, userauth.MSG_USERAUTH_BANNER,
1661- userauth.MSG_USERAUTH_SUCCESS])
1662- self.assertBannerSent(self.user_auth._banner + '\r\n')
1663-
1664- return d.addCallback(check)
1665-
1666- def test_defaultBannerNotSentOnFailure(self):
1667- # Failed authentication attempts do not get the default banner
1668- # sent.
1669- self.user_auth._banner = "You come away two hundred quid down"
1670-
1671- d = self.requestFailedAuthentication()
1672-
1673- def check(ignored):
1674- self.assertMessageOrder(
1675- [userauth.MSG_USERAUTH_BANNER, userauth.MSG_USERAUTH_FAILURE])
1676- self.assertBannerSent(MockChecker.error_message + '\r\n')
1677-
1678- return d.addCallback(check)
1679-
1680- def test_loggedToBanner(self):
1681- # When there's an authentication failure, we display an informative
1682- # error message through the SSH authentication protocol 'banner'.
1683- d = self.requestFailedAuthentication()
1684- def check(ignored):
1685- # Check that we received a BANNER, then a FAILURE.
1686- self.assertMessageOrder(
1687- [userauth.MSG_USERAUTH_BANNER, userauth.MSG_USERAUTH_FAILURE])
1688- self.assertBannerSent(MockChecker.error_message + '\r\n')
1689- return d.addCallback(check)
1690-
1691- def test_unsupportedAuthMethodNotLogged(self):
1692- # Trying various authentication methods is a part of the normal
1693- # operation of the SSH authentication protocol. We should not spam the
1694- # client with warnings about this, as whenever it becomes a problem,
1695- # we can rely on the SSH client itself to report it to the user.
1696- d = self.requestUnsupportedAuthentication()
1697- def check(ignored):
1698- # Check that we received only a FAILRE.
1699- self.assertMessageOrder([userauth.MSG_USERAUTH_FAILURE])
1700- return d.addCallback(check)
1701-
1702-
1703-class TestPublicKeyFromLaunchpadChecker(TestCase):
1704- """Tests for the SSH server authentication mechanism.
1705-
1706- PublicKeyFromLaunchpadChecker accepts the SSH authentication information
1707- and contacts the authserver to determine if the given details are valid.
1708-
1709- Any authentication errors are displayed back to the user via an SSH
1710- MSG_USERAUTH_BANNER message.
1711- """
1712-
1713- run_tests_with = AsynchronousDeferredRunTest
1714-
1715- class FakeAuthenticationEndpoint:
1716- """A fake client for enough of `IAuthServer` for this test.
1717- """
1718-
1719- valid_user = 'valid_user'
1720- no_key_user = 'no_key_user'
1721- valid_key = 'valid_key'
1722-
1723- def __init__(self):
1724- self.calls = []
1725-
1726- def callRemote(self, function_name, *args, **kwargs):
1727- return getattr(
1728- self, 'xmlrpc_%s' % function_name)(*args, **kwargs)
1729-
1730- def xmlrpc_getUserAndSSHKeys(self, username):
1731- self.calls.append(username)
1732- if username == self.valid_user:
1733- return defer.succeed({
1734- 'name': username,
1735- 'keys': [('DSA', self.valid_key.encode('base64'))],
1736- })
1737- elif username == self.no_key_user:
1738- return defer.succeed({
1739- 'name': username,
1740- 'keys': [],
1741- })
1742- else:
1743- try:
1744- raise faults.NoSuchPersonWithName(username)
1745- except faults.NoSuchPersonWithName:
1746- return defer.fail()
1747-
1748- def makeCredentials(self, username, public_key, mind=None):
1749- if mind is None:
1750- mind = auth.UserDetailsMind()
1751- return auth.SSHPrivateKeyWithMind(
1752- username, 'ssh-dss', public_key, '', None, mind)
1753-
1754- def makeChecker(self, do_signature_checking=False):
1755- """Construct a PublicKeyFromLaunchpadChecker.
1756-
1757- :param do_signature_checking: if False, as is the default, monkeypatch
1758- the returned instance to not verify the signatures of the keys.
1759- """
1760- checker = auth.PublicKeyFromLaunchpadChecker(self.authserver)
1761- if not do_signature_checking:
1762- checker._cbRequestAvatarId = self._cbRequestAvatarId
1763- return checker
1764-
1765- def _cbRequestAvatarId(self, is_key_valid, credentials):
1766- if is_key_valid:
1767- return credentials.username
1768- return failure.Failure(UnauthorizedLogin())
1769-
1770- def setUp(self):
1771- TestCase.setUp(self)
1772- self.authserver = self.FakeAuthenticationEndpoint()
1773-
1774- def test_successful(self):
1775- # Attempting to log in with a username and key known to the
1776- # authentication end-point succeeds.
1777- creds = self.makeCredentials(
1778- self.authserver.valid_user, self.authserver.valid_key)
1779- checker = self.makeChecker()
1780- d = checker.requestAvatarId(creds)
1781- return d.addCallback(self.assertEqual, self.authserver.valid_user)
1782-
1783- @suppress_stderr
1784- def test_invalid_signature(self):
1785- # The checker requests attempts to authenticate if the requests have
1786- # an invalid signature.
1787- creds = self.makeCredentials(
1788- self.authserver.valid_user, self.authserver.valid_key)
1789- creds.signature = 'a'
1790- checker = self.makeChecker(True)
1791- d = checker.requestAvatarId(creds)
1792- def flush_errback(f):
1793- flush_logged_errors(BadKeyError)
1794- return f
1795- d.addErrback(flush_errback)
1796- return assert_fails_with(d, UnauthorizedLogin)
1797-
1798- def assertLoginError(self, checker, creds, error_message):
1799- """Logging in with 'creds' against 'checker' fails with 'message'.
1800-
1801- In particular, this tests that the login attempt fails in a way that
1802- is sent to the client.
1803-
1804- :param checker: The `ICredentialsChecker` used.
1805- :param creds: SSHPrivateKey credentials.
1806- :param error_message: String excepted to match the exception's message.
1807- :return: Deferred. You must return this from your test.
1808- """
1809- d = assert_fails_with(
1810- checker.requestAvatarId(creds),
1811- auth.UserDisplayedUnauthorizedLogin)
1812- d.addCallback(
1813- lambda exception: self.assertEqual(str(exception), error_message))
1814- return d
1815-
1816- def test_noSuchUser(self):
1817- # When someone signs in with a non-existent user, they should be told
1818- # that. The usual security issues don't apply here because the list of
1819- # Launchpad user names is public.
1820- checker = self.makeChecker()
1821- creds = self.makeCredentials(
1822- 'no-such-user', self.authserver.valid_key)
1823- return self.assertLoginError(
1824- checker, creds, 'No such Launchpad account: no-such-user')
1825-
1826- def test_noKeys(self):
1827- # When you sign into an existing account with no SSH keys, the SSH
1828- # server informs you that the account has no keys.
1829- checker = self.makeChecker()
1830- creds = self.makeCredentials(
1831- self.authserver.no_key_user, self.authserver.valid_key)
1832- return self.assertLoginError(
1833- checker, creds,
1834- "Launchpad user %r doesn't have a registered SSH key"
1835- % self.authserver.no_key_user)
1836-
1837- def test_wrongKey(self):
1838- # When you sign into an existing account using the wrong key, you
1839- # are *not* informed of the wrong key. This is because SSH often
1840- # tries several keys as part of normal operation.
1841- checker = self.makeChecker()
1842- creds = self.makeCredentials(
1843- self.authserver.valid_user, 'invalid key')
1844- # We cannot use assertLoginError because we are checking that we fail
1845- # with UnauthorizedLogin and not its subclass
1846- # UserDisplayedUnauthorizedLogin.
1847- d = assert_fails_with(
1848- checker.requestAvatarId(creds),
1849- UnauthorizedLogin)
1850- d.addCallback(
1851- lambda exception:
1852- self.failIf(isinstance(exception,
1853- auth.UserDisplayedUnauthorizedLogin),
1854- "Should not be a UserDisplayedUnauthorizedLogin"))
1855- return d
1856-
1857- def test_successful_with_second_key_calls_authserver_once(self):
1858- # It is normal in SSH authentication to be presented with a number of
1859- # keys. When the valid key is presented after some invalid ones (a)
1860- # the login succeeds and (b) only one call is made to the authserver
1861- # to retrieve the user's details.
1862- checker = self.makeChecker()
1863- mind = auth.UserDetailsMind()
1864- wrong_key_creds = self.makeCredentials(
1865- self.authserver.valid_user, 'invalid key', mind)
1866- right_key_creds = self.makeCredentials(
1867- self.authserver.valid_user, self.authserver.valid_key, mind)
1868- d = checker.requestAvatarId(wrong_key_creds)
1869- def try_second_key(failure):
1870- failure.trap(UnauthorizedLogin)
1871- return checker.requestAvatarId(right_key_creds)
1872- d.addErrback(try_second_key)
1873- d.addCallback(self.assertEqual, self.authserver.valid_user)
1874- def check_one_call(r):
1875- self.assertEqual(
1876- [self.authserver.valid_user], self.authserver.calls)
1877- return r
1878- d.addCallback(check_one_call)
1879- return d
1880-
1881- def test_noSuchUser_with_two_keys_calls_authserver_once(self):
1882- # When more than one key is presented for a username that does not
1883- # exist, only one call is made to the authserver.
1884- checker = self.makeChecker()
1885- mind = auth.UserDetailsMind()
1886- creds_1 = self.makeCredentials(
1887- 'invalid-user', 'invalid key 1', mind)
1888- creds_2 = self.makeCredentials(
1889- 'invalid-user', 'invalid key 2', mind)
1890- d = checker.requestAvatarId(creds_1)
1891- def try_second_key(failure):
1892- return assert_fails_with(
1893- checker.requestAvatarId(creds_2),
1894- UnauthorizedLogin)
1895- d.addErrback(try_second_key)
1896- def check_one_call(r):
1897- self.assertEqual(
1898- ['invalid-user'], self.authserver.calls)
1899- return r
1900- d.addCallback(check_one_call)
1901- return d
1902
1903=== removed file 'lib/lp/services/sshserver/tests/test_events.py'
1904--- lib/lp/services/sshserver/tests/test_events.py 2011-08-12 11:37:08 +0000
1905+++ lib/lp/services/sshserver/tests/test_events.py 1970-01-01 00:00:00 +0000
1906@@ -1,91 +0,0 @@
1907-# Copyright 2010 Canonical Ltd. This software is licensed under the
1908-# GNU Affero General Public License version 3 (see the file LICENSE).
1909-
1910-"""Tests for the logging events."""
1911-
1912-__metaclass__ = type
1913-
1914-import logging
1915-
1916-from zope.component import (
1917- adapter,
1918- getGlobalSiteManager,
1919- provideHandler,
1920- )
1921-# This non-standard import is necessary to hook up the event system.
1922-import zope.component.event
1923-from zope.event import notify
1924-
1925-from lp.services.sshserver.events import (
1926- ILoggingEvent,
1927- LoggingEvent,
1928- )
1929-from lp.testing import TestCase
1930-
1931-
1932-class ListHandler(logging.Handler):
1933- """Logging handler that just appends records to a list.
1934-
1935- This handler isn't intended to be used by production code -- memory leak
1936- city! -- instead it's useful for unit tests that want to make sure the
1937- right events are being logged.
1938- """
1939-
1940- def __init__(self, logging_list):
1941- """Construct a `ListHandler`.
1942-
1943- :param logging_list: A list that will be appended to. The handler
1944- mutates this list.
1945- """
1946- logging.Handler.__init__(self)
1947- self._list = logging_list
1948-
1949- def emit(self, record):
1950- """Append 'record' to the list."""
1951- self._list.append(record)
1952-
1953-
1954-class TestLoggingEvent(TestCase):
1955-
1956- def assertLogs(self, records, function, *args, **kwargs):
1957- """Assert 'function' logs 'records' when run with the given args."""
1958- logged_events = []
1959- handler = ListHandler(logged_events)
1960- self.logger.addHandler(handler)
1961- result = function(*args, **kwargs)
1962- self.logger.removeHandler(handler)
1963- self.assertEqual(
1964- [(record.levelno, record.getMessage())
1965- for record in logged_events], records)
1966- return result
1967-
1968- def assertEventLogs(self, record, logging_event):
1969- self.assertLogs([record], notify, logging_event)
1970-
1971- def setUp(self):
1972- TestCase.setUp(self)
1973- logger = logging.getLogger(self.factory.getUniqueString())
1974- logger.setLevel(logging.DEBUG)
1975- self.logger = logger
1976-
1977- @adapter(ILoggingEvent)
1978- def _log_event(event):
1979- logger.log(event.level, event.message)
1980-
1981- provideHandler(_log_event)
1982- self.addCleanup(getGlobalSiteManager().unregisterHandler, _log_event)
1983-
1984- def test_level(self):
1985- event = LoggingEvent(logging.CRITICAL, "foo")
1986- self.assertEventLogs((logging.CRITICAL, 'foo'), event)
1987-
1988- def test_formatting(self):
1989- event = LoggingEvent(logging.DEBUG, "foo: %(name)s", name="bar")
1990- self.assertEventLogs((logging.DEBUG, 'foo: bar'), event)
1991-
1992- def test_subclass(self):
1993- class SomeEvent(LoggingEvent):
1994- template = "%(something)s happened."
1995- level = logging.INFO
1996- self.assertEventLogs(
1997- (logging.INFO, 'foo happened.'), SomeEvent(something='foo'))
1998
1999=== removed file 'lib/lp/services/sshserver/tests/test_session.py'
2000--- lib/lp/services/sshserver/tests/test_session.py 2011-08-12 11:37:08 +0000
2001+++ lib/lp/services/sshserver/tests/test_session.py 1970-01-01 00:00:00 +0000
2002@@ -1,99 +0,0 @@
2003-# Copyright 2010 Canonical Ltd. This software is licensed under the
2004-# GNU Affero General Public License version 3 (see the file LICENSE).
2005-
2006-"""Tests for generic SSH session support."""
2007-
2008-__metaclass__ = type
2009-
2010-from twisted.conch.interfaces import ISession
2011-from twisted.conch.ssh import connection
2012-
2013-from lp.services.sshserver.session import DoNothingSession
2014-from lp.testing import TestCase
2015-
2016-
2017-class MockSSHSession:
2018- """Just enough of SSHSession to allow checking of reporting to stderr."""
2019-
2020- def __init__(self, log):
2021- self.log = log
2022-
2023- def writeExtended(self, channel, data):
2024- self.log.append(('writeExtended', channel, data))
2025-
2026-
2027-class MockProcessTransport:
2028- """Mock transport used to fake speaking with child processes that are
2029- mocked out in tests.
2030- """
2031-
2032- def __init__(self, executable):
2033- self._executable = executable
2034- self.log = []
2035- self.session = MockSSHSession(self.log)
2036-
2037- def closeStdin(self):
2038- self.log.append(('closeStdin',))
2039-
2040- def loseConnection(self):
2041- self.log.append(('loseConnection',))
2042-
2043- def signalProcess(self, signal):
2044- self.log.append(('signalProcess', signal))
2045-
2046- def write(self, data):
2047- self.log.append(('write', data))
2048-
2049-
2050-class TestDoNothing(TestCase):
2051- """Tests for DoNothingSession."""
2052-
2053- def setUp(self):
2054- super(TestDoNothing, self).setUp()
2055- self.session = DoNothingSession(None)
2056-
2057- def test_getPtyIsANoOp(self):
2058- # getPty is called on the way to establishing a shell. Since we don't
2059- # give out shells, it should be a no-op. Raising an exception would
2060- # log an OOPS, so we won't do that.
2061- self.assertEqual(None, self.session.getPty(None, None, None))
2062-
2063- def test_openShellNotImplemented(self):
2064- # openShell closes the connection.
2065- protocol = MockProcessTransport('bash')
2066- self.session.openShell(protocol)
2067- self.assertEqual(
2068- [('writeExtended', connection.EXTENDED_DATA_STDERR,
2069- 'No shells on this server.\r\n'),
2070- ('loseConnection',)],
2071- protocol.log)
2072-
2073- def test_windowChangedNotImplemented(self):
2074- # windowChanged raises a NotImplementedError. It doesn't matter what
2075- # we pass it.
2076- self.assertRaises(NotImplementedError,
2077- self.session.windowChanged, None)
2078-
2079- def test_providesISession(self):
2080- # DoNothingSession must provide ISession.
2081- self.failUnless(ISession.providedBy(self.session),
2082- "DoNothingSession doesn't implement ISession")
2083-
2084- def test_closedDoesNothing(self):
2085- # closed is a no-op.
2086- self.assertEqual(None, self.session.closed())
2087-
2088- def test_execCommandNotImplemented(self):
2089- # DoNothingSession.execCommand spawns the appropriate process.
2090- protocol = MockProcessTransport('bash')
2091- command = 'cat /etc/hostname'
2092- self.session.execCommand(protocol, command)
2093- self.assertEqual(
2094- [('writeExtended', connection.EXTENDED_DATA_STDERR,
2095- 'Not allowed to execute commands on this server.\r\n'),
2096- ('loseConnection',)],
2097- protocol.log)
2098-
2099- def test_eofReceivedDoesNothingWhenNoCommand(self):
2100- # When no process has been created, 'eofReceived' is a no-op.
2101- self.assertEqual(None, self.session.eofReceived())
2102
2103=== modified file 'setup.py'
2104--- setup.py 2014-01-17 02:01:54 +0000
2105+++ setup.py 2015-01-12 18:55:56 +0000
2106@@ -55,6 +55,7 @@
2107 'lazr.restful',
2108 'lazr.jobrunner',
2109 'lazr.smtptest',
2110+ 'lazr.sshserver',
2111 'lazr.testing',
2112 'lazr.uri',
2113 'lpjsmin',
2114
2115=== modified file 'versions.cfg'
2116--- versions.cfg 2014-12-08 02:23:41 +0000
2117+++ versions.cfg 2015-01-12 18:55:56 +0000
2118@@ -53,6 +53,7 @@
2119 lazr.restful = 0.19.10
2120 lazr.restfulclient = 0.13.2
2121 lazr.smtptest = 1.3
2122+lazr.sshserver = 0.1
2123 lazr.testing = 0.1.1
2124 lazr.uri = 1.0.2
2125 lpjsmin = 0.5