Merge lp:~jml/launchpad/extract-ssh-server into lp:launchpad

Proposed by Jonathan Lange
Status: Merged
Approved by: Michael Hudson-Doyle
Approved revision: no longer in the source branch.
Merged at revision: not available
Proposed branch: lp:~jml/launchpad/extract-ssh-server
Merge into: lp:launchpad
Diff against target: 351 lines (+108/-79)
5 files modified
daemons/sftp.tac (+15/-6)
lib/lp/codehosting/sshserver/accesslog.py (+1/-4)
lib/lp/codehosting/sshserver/auth.py (+4/-4)
lib/lp/codehosting/sshserver/service.py (+66/-33)
lib/lp/codehosting/sshserver/tests/test_auth.py (+22/-32)
To merge this branch: bzr merge lp:~jml/launchpad/extract-ssh-server
Reviewer Review Type Date Requested Status
Michael Hudson-Doyle Approve
Canonical Launchpad Engineering Pending
Review via email: mp+23350@code.launchpad.net

Commit message

Refactor some codehosting SSH server stuff to not depend on codehosting configuration

Description of the change

This branch extracts some of the changes from my mega-branch lp:~jml/launchpad/ssh-key-auth.

In particular, it parametrizes three of the key classes so that they don't look up configuration variables themselves, but rather get passed the values that they need. This pushes most of the configuration up to the sftp.tac file, which is good, since tac files are all about configuration.

The three classes are SSHUserAuthServer, Factory and SSHService. The changes are pretty boring -- just moving config variables out into constructor parameters.

Other than that, there's a tweak to LoggingManager to remove an unused parameter.

I look forward to your review.

To post a comment you must log in.
Revision history for this message
Michael Hudson-Doyle (mwhudson) wrote :

Well, I certainly like the idea of this branch.

I think SSHService.__init__'s port argument should perhaps be called strport. I'd expect 'port' to be an integer (I guess the config item is poorly named too, but that's harder to fix).

The parameterization around the key path names smells a little funny to me. Would it be safe for SSHService to just take the directory and assume the file names? Maybe not...

I think service.Factory.__doc__ could be clearer about the expected types of the key arguments.

Um, I think that's it.

Thanks for working on this!

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'daemons/sftp.tac'
--- daemons/sftp.tac 2009-06-24 20:55:31 +0000
+++ daemons/sftp.tac 2010-04-15 13:43:32 +0000
@@ -1,4 +1,4 @@
1# Copyright 2009 Canonical Ltd. This software is licensed under the1# Copyright 2009-2010 Canonical Ltd. This software is licensed under the
2# GNU Affero General Public License version 3 (see the file LICENSE).2# GNU Affero General Public License version 3 (see the file LICENSE).
33
4# This is a Twisted application config file. To run, use:4# This is a Twisted application config file. To run, use:
@@ -7,13 +7,22 @@
77
8from twisted.application import service8from twisted.application import service
99
10from canonical.config import config
10from canonical.launchpad.daemons import tachandler11from canonical.launchpad.daemons import tachandler
11from lp.codehosting.sshserver.service import SSHService12
1213from lp.codehosting.sshserver.service import (
1314 get_key_path, make_portal, PRIVATE_KEY_FILE, PUBLIC_KEY_FILE, SSHService)
14# Construct an Application that includes a supermirror SFTP service. 15
16
17# Construct an Application that has the codehosting SSH server.
15application = service.Application('sftponly')18application = service.Application('sftponly')
16svc = SSHService()19svc = SSHService(
20 portal=make_portal(),
21 private_key_path=get_key_path(PRIVATE_KEY_FILE),
22 public_key_path=get_key_path(PUBLIC_KEY_FILE),
23 strport=config.codehosting.port,
24 idle_timeout=config.codehosting.idle_timeout,
25 banner=config.codehosting.banner)
17svc.setServiceParent(application)26svc.setServiceParent(application)
1827
19# Service that announces when the daemon is ready28# Service that announces when the daemon is ready
2029
=== modified file 'lib/lp/codehosting/sshserver/accesslog.py'
--- lib/lp/codehosting/sshserver/accesslog.py 2010-04-09 12:45:48 +0000
+++ lib/lp/codehosting/sshserver/accesslog.py 2010-04-15 13:43:32 +0000
@@ -36,7 +36,7 @@
36class LoggingManager:36class LoggingManager:
37 """Class for managing codehosting logging."""37 """Class for managing codehosting logging."""
3838
39 def setUp(self, configure_oops_reporting=False, mangle_stdout=False):39 def setUp(self, configure_oops_reporting=False):
40 """Set up logging for the smart server.40 """Set up logging for the smart server.
4141
42 This sets up a debugging handler on the 'codehosting' logger, makes42 This sets up a debugging handler on the 'codehosting' logger, makes
@@ -45,9 +45,6 @@
4545
46 :param configure_oops_reporting: If True, install a Twisted log46 :param configure_oops_reporting: If True, install a Twisted log
47 observer that ensures unhandled exceptions get reported as OOPSes.47 observer that ensures unhandled exceptions get reported as OOPSes.
48 :param mangle_stdout: If True and if configure_oops_reporting is True,
49 redirect standard error and standard output to the OOPS logging
50 observer.
51 """48 """
52 log = get_codehosting_logger()49 log = get_codehosting_logger()
53 self._orig_level = log.level50 self._orig_level = log.level
5451
=== modified file 'lib/lp/codehosting/sshserver/auth.py'
--- lib/lp/codehosting/sshserver/auth.py 2010-04-10 13:55:56 +0000
+++ lib/lp/codehosting/sshserver/auth.py 2010-04-15 13:43:32 +0000
@@ -168,8 +168,9 @@
168 paste and change the implementations of these methods.168 paste and change the implementations of these methods.
169 """169 """
170170
171 def __init__(self, transport=None):171 def __init__(self, transport=None, banner=None):
172 self.transport = transport172 self.transport = transport
173 self._banner = banner
173 self._configured_banner_sent = False174 self._configured_banner_sent = False
174 self._mind = UserDetailsMind()175 self._mind = UserDetailsMind()
175 self.interfaceToMethod = userauth.SSHUserAuthServer.interfaceToMethod176 self.interfaceToMethod = userauth.SSHUserAuthServer.interfaceToMethod
@@ -181,10 +182,9 @@
181 NS(bytes) + NS(language))182 NS(bytes) + NS(language))
182183
183 def _sendConfiguredBanner(self, passed_through):184 def _sendConfiguredBanner(self, passed_through):
184 if (not self._configured_banner_sent185 if not self._configured_banner_sent and self._banner:
185 and config.codehosting.banner is not None):
186 self._configured_banner_sent = True186 self._configured_banner_sent = True
187 self.sendBanner(config.codehosting.banner)187 self.sendBanner(self._banner)
188 return passed_through188 return passed_through
189189
190 def ssh_USERAUTH_REQUEST(self, packet):190 def ssh_USERAUTH_REQUEST(self, packet):
191191
=== modified file 'lib/lp/codehosting/sshserver/service.py'
--- lib/lp/codehosting/sshserver/service.py 2010-03-19 10:43:51 +0000
+++ lib/lp/codehosting/sshserver/service.py 2010-04-15 13:43:32 +0000
@@ -30,6 +30,12 @@
30from lp.services.twistedsupport import gatherResults30from lp.services.twistedsupport import gatherResults
3131
3232
33# The names of the key files of the server itself. The directory itself is
34# given in config.codehosting.host_key_pair_path.
35PRIVATE_KEY_FILE = 'ssh_host_key_rsa'
36PUBLIC_KEY_FILE = 'ssh_host_key_rsa.pub'
37
38
33class KeepAliveSettingSSHServerTransport(SSHServerTransport):39class KeepAliveSettingSSHServerTransport(SSHServerTransport):
3440
35 def connectionMade(self):41 def connectionMade(self):
@@ -37,6 +43,24 @@
37 self.transport.setTcpKeepAlive(True)43 self.transport.setTcpKeepAlive(True)
3844
3945
46def get_key_path(key_filename):
47 key_directory = config.codehosting.host_key_pair_path
48 return os.path.join(config.root, key_directory, key_filename)
49
50
51def make_portal():
52 """Create and return a `Portal` for the SSH service.
53
54 This portal accepts SSH credentials and returns our customized SSH
55 avatars (see `lp.codehosting.sshserver.auth.LaunchpadAvatar`).
56 """
57 authentication_proxy = Proxy(
58 config.codehosting.authentication_endpoint)
59 branchfs_proxy = Proxy(config.codehosting.branchfs_endpoint)
60 return get_portal(authentication_proxy, branchfs_proxy)
61
62
63
40class Factory(SSHFactory):64class Factory(SSHFactory):
41 """SSH factory that uses the codehosting custom authentication.65 """SSH factory that uses the codehosting custom authentication.
4266
@@ -47,13 +71,29 @@
4771
48 protocol = KeepAliveSettingSSHServerTransport72 protocol = KeepAliveSettingSSHServerTransport
4973
50 def __init__(self, portal):74 def __init__(self, portal, private_key, public_key, banner=None):
75 """Construct an SSH factory.
76
77 :param portal: The portal used to turn credentials into users.
78 :param private_key: The private key of the server, must be an RSA
79 key, given as a `twisted.conch.ssh.keys.Key` object.
80 :param public_key: The public key of the server, must be an RSA
81 key, given as a `twisted.conch.ssh.keys.Key` object.
82 :param banner: The text to display when users successfully log in.
83 """
51 # Although 'portal' isn't part of the defined interface for84 # Although 'portal' isn't part of the defined interface for
52 # `SSHFactory`, defining it here is how the `SSHUserAuthServer` gets85 # `SSHFactory`, defining it here is how the `SSHUserAuthServer` gets
53 # at it. (Look for the beautiful line "self.portal =86 # at it. (Look for the beautiful line "self.portal =
54 # self.transport.factory.portal").87 # self.transport.factory.portal").
55 self.portal = portal88 self.portal = portal
56 self.services['ssh-userauth'] = SSHUserAuthServer89 self.services['ssh-userauth'] = self._makeAuthServer
90 self._private_key = private_key
91 self._public_key = public_key
92 self._banner = banner
93
94 def _makeAuthServer(self, *args, **kwargs):
95 kwargs['banner'] = self._banner
96 return SSHUserAuthServer(*args, **kwargs)
5797
58 def buildProtocol(self, address):98 def buildProtocol(self, address):
59 """Build an SSH protocol instance, logging the event.99 """Build an SSH protocol instance, logging the event.
@@ -87,58 +127,51 @@
87 notify(accesslog.AuthenticationFailed(transport))127 notify(accesslog.AuthenticationFailed(transport))
88 notify(accesslog.UserDisconnected(transport))128 notify(accesslog.UserDisconnected(transport))
89129
90 def _loadKey(self, key_filename):
91 key_directory = config.codehosting.host_key_pair_path
92 key_path = os.path.join(config.root, key_directory, key_filename)
93 return Key.fromFile(key_path)
94
95 def getPublicKeys(self):130 def getPublicKeys(self):
96 """Return the server's configured public key.131 """Return the server's configured public key.
97132
98 See `SSHFactory.getPublicKeys`.133 See `SSHFactory.getPublicKeys`.
99 """134 """
100 public_key = self._loadKey('ssh_host_key_rsa.pub')135 return {'ssh-rsa': self._public_key}
101 return {'ssh-rsa': public_key}
102136
103 def getPrivateKeys(self):137 def getPrivateKeys(self):
104 """Return the server's configured private key.138 """Return the server's configured private key.
105139
106 See `SSHFactory.getPrivateKeys`.140 See `SSHFactory.getPrivateKeys`.
107 """141 """
108 private_key = self._loadKey('ssh_host_key_rsa')142 return {'ssh-rsa': self._private_key}
109 return {'ssh-rsa': private_key}
110143
111144
112class SSHService(service.Service):145class SSHService(service.Service):
113 """A Twisted service for the codehosting SSH server."""146 """A Twisted service for the codehosting SSH server."""
114147
115 def __init__(self):148 def __init__(self, portal, private_key_path, public_key_path,
116 self.service = self.makeService()149 strport='tcp:22', idle_timeout=3600, banner=None):
117150 """Construct an SSH service.
118 def makePortal(self):151
119 """Create and return a `Portal` for the SSH service.152 :param portal: The `Portal` that turns authentication requests into
120153 views on the system.
121 This portal accepts SSH credentials and returns our customized SSH154 :param private_key_path: The path to the SSH server's private key.
122 avatars (see `lp.codehosting.sshserver.auth.LaunchpadAvatar`).155 :param public_key_path: The path to the SSH server's public key.
123 """156 :param strport: The port to run the server on, expressed in Twisted's
124 authentication_proxy = Proxy(157 "strports" mini-language. Defaults to 'tcp:22'.
125 config.codehosting.authentication_endpoint)158 :param idle_timeout: The number of seconds to wait before killing a
126 branchfs_proxy = Proxy(config.codehosting.branchfs_endpoint)159 connection that isn't doing anything. Defaults to 3600.
127 return get_portal(authentication_proxy, branchfs_proxy)160 :param banner: An announcement printed to users when they connect.
128161 By default, announce nothing.
129 def makeService(self):
130 """Return a service that provides an SFTP server. This is called in
131 the constructor.
132 """162 """
133 ssh_factory = TimeoutFactory(163 ssh_factory = TimeoutFactory(
134 Factory(self.makePortal()),164 Factory(
135 timeoutPeriod=config.codehosting.idle_timeout)165 portal,
136 return strports.service(config.codehosting.port, ssh_factory)166 private_key=Key.fromFile(private_key_path),
167 public_key=Key.fromFile(public_key_path),
168 banner=banner),
169 timeoutPeriod=idle_timeout)
170 self.service = strports.service(strport, ssh_factory)
137171
138 def startService(self):172 def startService(self):
139 """Start the SSH service."""173 """Start the SSH service."""
140 accesslog.LoggingManager().setUp(174 accesslog.LoggingManager().setUp(configure_oops_reporting=True)
141 configure_oops_reporting=True, mangle_stdout=True)
142 notify(accesslog.ServerStarting())175 notify(accesslog.ServerStarting())
143 # By default, only the owner of files should be able to write to them.176 # By default, only the owner of files should be able to write to them.
144 # Perhaps in the future this line will be deleted and the umask177 # Perhaps in the future this line will be deleted and the umask
145178
=== modified file 'lib/lp/codehosting/sshserver/tests/test_auth.py'
--- lib/lp/codehosting/sshserver/tests/test_auth.py 2010-03-19 10:43:51 +0000
+++ lib/lp/codehosting/sshserver/tests/test_auth.py 2010-04-15 13:43:32 +0000
@@ -1,4 +1,4 @@
1# Copyright 2009 Canonical Ltd. This software is licensed under the1# Copyright 2009-2010 Canonical Ltd. This software is licensed under the
2# GNU Affero General Public License version 3 (see the file LICENSE).2# GNU Affero General Public License version 3 (see the file LICENSE).
33
4import os4import os
@@ -193,11 +193,6 @@
193193
194 layer = TwistedLayer194 layer = TwistedLayer
195195
196 banner_conf = """
197 [codehosting]
198 banner: banner
199 """
200
201 def setUp(self):196 def setUp(self):
202 UserAuthServerMixin.setUp(self)197 UserAuthServerMixin.setUp(self)
203 self.portal.registerChecker(MockChecker())198 self.portal.registerChecker(MockChecker())
@@ -240,25 +235,22 @@
240 self.assertMessageOrder([userauth.MSG_USERAUTH_SUCCESS])235 self.assertMessageOrder([userauth.MSG_USERAUTH_SUCCESS])
241 return d.addCallback(check)236 return d.addCallback(check)
242237
243 def test_configuredBannerSentOnSuccess(self):238 def test_defaultBannerSentOnSuccess(self):
244 # If a banner is set in the codehosting config then we send it to the239 # If a banner was passed to the user auth agent then we send it to the
245 # user when they log in.240 # user when they log in.
246 config.push('codehosting_overlay', self.banner_conf)241 self.user_auth._banner = "Boogedy boo"
247 d = self.requestSuccessfulAuthentication()242 d = self.requestSuccessfulAuthentication()
248 def check(ignored):243 def check(ignored):
249 self.assertMessageOrder(244 self.assertMessageOrder(
250 [userauth.MSG_USERAUTH_BANNER, userauth.MSG_USERAUTH_SUCCESS])245 [userauth.MSG_USERAUTH_BANNER, userauth.MSG_USERAUTH_SUCCESS])
251 self.assertBannerSent(config.codehosting.banner + '\r\n')246 self.assertBannerSent(self.user_auth._banner + '\r\n')
252 def cleanup(ignored):247 return d.addCallback(check)
253 config.pop('codehosting_overlay')
254 return ignored
255 return d.addCallback(check).addBoth(cleanup)
256248
257 def test_configuredBannerSentOnlyOnce(self):249 def test_defaultBannerSentOnlyOnce(self):
258 # We don't send the banner on each authentication attempt, just on the250 # We don't send the banner on each authentication attempt, just on the
259 # first one. It is usual for there to be many authentication attempts251 # first one. It is usual for there to be many authentication attempts
260 # per SSH session.252 # per SSH session.
261 config.push('codehosting_overlay', self.banner_conf)253 self.user_auth._banner = "Boogedy boo"
262254
263 d = self.requestUnsupportedAuthentication()255 d = self.requestUnsupportedAuthentication()
264 d.addCallback(lambda ignored: self.requestSuccessfulAuthentication())256 d.addCallback(lambda ignored: self.requestSuccessfulAuthentication())
@@ -268,17 +260,14 @@
268 self.assertMessageOrder(260 self.assertMessageOrder(
269 [userauth.MSG_USERAUTH_FAILURE, userauth.MSG_USERAUTH_BANNER,261 [userauth.MSG_USERAUTH_FAILURE, userauth.MSG_USERAUTH_BANNER,
270 userauth.MSG_USERAUTH_SUCCESS])262 userauth.MSG_USERAUTH_SUCCESS])
271 self.assertBannerSent(config.codehosting.banner + '\r\n')263 self.assertBannerSent(self.user_auth._banner + '\r\n')
272264
273 def cleanup(ignored):265 return d.addCallback(check)
274 config.pop('codehosting_overlay')266
275 return ignored267 def test_defaultBannerNotSentOnFailure(self):
276 return d.addCallback(check).addBoth(cleanup)268 # Failed authentication attempts do not get the default banner
277
278 def test_configuredBannerNotSentOnFailure(self):
279 # Failed authentication attempts do not get the configured banner
280 # sent.269 # sent.
281 config.push('codehosting_overlay', self.banner_conf)270 self.user_auth._banner = "You come away two hundred quid down"
282271
283 d = self.requestFailedAuthentication()272 d = self.requestFailedAuthentication()
284273
@@ -287,11 +276,7 @@
287 [userauth.MSG_USERAUTH_BANNER, userauth.MSG_USERAUTH_FAILURE])276 [userauth.MSG_USERAUTH_BANNER, userauth.MSG_USERAUTH_FAILURE])
288 self.assertBannerSent(MockChecker.error_message + '\r\n')277 self.assertBannerSent(MockChecker.error_message + '\r\n')
289278
290 def cleanup(ignored):279 return d.addCallback(check)
291 config.pop('codehosting_overlay')
292 return ignored
293
294 return d.addCallback(check).addBoth(cleanup)
295280
296 def test_loggedToBanner(self):281 def test_loggedToBanner(self):
297 # When there's an authentication failure, we display an informative282 # When there's an authentication failure, we display an informative
@@ -532,7 +517,12 @@
532517
533 def makeFactory(self):518 def makeFactory(self):
534 """Create and start the factory that our SSH server uses."""519 """Create and start the factory that our SSH server uses."""
535 factory = service.Factory(auth.get_portal(None, None))520 factory = service.Factory(
521 auth.get_portal(None, None),
522 private_key=Key.fromFile(
523 service.get_key_path(service.PRIVATE_KEY_FILE)),
524 public_key=Key.fromFile(
525 service.get_key_path(service.PUBLIC_KEY_FILE)))
536 factory.startFactory()526 factory.startFactory()
537 return factory527 return factory
538528