Merge lp:~cjwatson/launchpad/split-lazr.sshserver into lp:launchpad
- split-lazr.sshserver
- Merge into devel
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 |
| Related bugs: |
| Reviewer | Review Type | Date Requested | Status |
|---|---|---|---|
| William Grant | code | 2015-01-06 | Approve on 2015-01-13 |
|
Review via email:
|
|||
Commit Message
Split out lp.services.
Description of the Change
Split out lp.services.
To post a comment you must log in.
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 |
