Merge lp:~jml/launchpad/extract-ssh-server-auth into lp:launchpad
- extract-ssh-server-auth
- Merge into devel
Proposed by
Jonathan Lange
Status: | Merged |
---|---|
Approved by: | Eleanor Berger |
Approved revision: | no longer in the source branch. |
Merged at revision: | not available |
Proposed branch: | lp:~jml/launchpad/extract-ssh-server-auth |
Merge into: | lp:launchpad |
Prerequisite: | lp:~jml/launchpad/extract-ssh-server-logging |
Diff against target: |
620 lines (+252/-125) (has conflicts) 9 files modified
daemons/sftp.tac (+3/-2) lib/lp/codehosting/sshserver/auth.py (+17/-49) lib/lp/codehosting/sshserver/daemon.py (+105/-0) lib/lp/codehosting/sshserver/service.py (+19/-57) lib/lp/codehosting/sshserver/tests/test_auth.py (+7/-7) lib/lp/codehosting/sshserver/tests/test_daemon.py (+93/-0) lib/lp/codehosting/sshserver/tests/test_session.py (+4/-4) lib/lp/codehosting/tests/helpers.py (+0/-2) lib/lp/codehosting/tests/test_sftp.py (+4/-4) Text conflict in daemons/sftp.tac Text conflict in lib/lp/codehosting/sshserver/accesslog.py Text conflict in lib/lp/codehosting/sshserver/service.py Text conflict in lib/lp/codehosting/sshserver/tests/test_auth.py |
To merge this branch: | bzr merge lp:~jml/launchpad/extract-ssh-server-auth |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Eleanor Berger (community) | code | Approve | |
Review via email: mp+23482@code.launchpad.net |
Commit message
Move codehosting-
Description of the change
This is yet another part of the massive branch that splits out the ssh server from codehosting.
This branch takes the codehosting-
Most of the changes are fairly simple. The only subtlety is that the LaunchpadAvatar class now has a new child class called CodehostingAvatar.
To post a comment you must log in.
Preview Diff
[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1 | === modified file 'daemons/sftp.tac' | |||
2 | --- daemons/sftp.tac 2010-04-16 19:00:51 +0000 | |||
3 | +++ daemons/sftp.tac 2010-04-16 19:00:53 +0000 | |||
4 | @@ -18,9 +18,10 @@ | |||
5 | 18 | # Construct an Application that has the codehosting SSH server. | 18 | # Construct an Application that has the codehosting SSH server. |
6 | 19 | ======= | 19 | ======= |
7 | 20 | 20 | ||
9 | 21 | from lp.codehosting.sshserver.service import ( | 21 | from lp.codehosting.sshserver.daemon import ( |
10 | 22 | ACCESS_LOG_NAME, get_key_path, LOG_NAME, make_portal, OOPS_CONFIG_SECTION, | 22 | ACCESS_LOG_NAME, get_key_path, LOG_NAME, make_portal, OOPS_CONFIG_SECTION, |
12 | 23 | PRIVATE_KEY_FILE, PUBLIC_KEY_FILE, SSHService) | 23 | PRIVATE_KEY_FILE, PUBLIC_KEY_FILE) |
13 | 24 | from lp.codehosting.sshserver.service import SSHService | ||
14 | 24 | 25 | ||
15 | 25 | 26 | ||
16 | 26 | # Construct an Application that has the codehosting SSH server. | 27 | # Construct an Application that has the codehosting SSH server. |
17 | 27 | 28 | ||
18 | === modified file 'lib/lp/codehosting/sshserver/auth.py' | |||
19 | --- lib/lp/codehosting/sshserver/auth.py 2010-04-16 19:00:51 +0000 | |||
20 | +++ lib/lp/codehosting/sshserver/auth.py 2010-04-16 19:00:53 +0000 | |||
21 | @@ -13,7 +13,8 @@ | |||
22 | 13 | 13 | ||
23 | 14 | __metaclass__ = type | 14 | __metaclass__ = type |
24 | 15 | __all__ = [ | 15 | __all__ = [ |
26 | 16 | 'get_portal', | 16 | 'LaunchpadAvatar', |
27 | 17 | 'PublicKeyFromLaunchpadChecker', | ||
28 | 17 | 'SSHUserAuthServer', | 18 | 'SSHUserAuthServer', |
29 | 18 | ] | 19 | ] |
30 | 19 | 20 | ||
31 | @@ -21,48 +22,47 @@ | |||
32 | 21 | 22 | ||
33 | 22 | from twisted.conch import avatar | 23 | from twisted.conch import avatar |
34 | 23 | from twisted.conch.error import ConchError | 24 | from twisted.conch.error import ConchError |
37 | 24 | from twisted.conch.interfaces import IConchUser, ISession | 25 | from twisted.conch.interfaces import IConchUser |
38 | 25 | from twisted.conch.ssh import filetransfer, keys, userauth | 26 | from twisted.conch.ssh import keys, userauth |
39 | 26 | from twisted.conch.ssh.common import getNS, NS | 27 | from twisted.conch.ssh.common import getNS, NS |
40 | 27 | from twisted.conch.checkers import SSHPublicKeyDatabase | 28 | from twisted.conch.checkers import SSHPublicKeyDatabase |
41 | 28 | 29 | ||
42 | 29 | from twisted.cred.error import UnauthorizedLogin | 30 | from twisted.cred.error import UnauthorizedLogin |
43 | 30 | from twisted.cred.checkers import ICredentialsChecker | 31 | from twisted.cred.checkers import ICredentialsChecker |
44 | 31 | from twisted.cred import credentials | 32 | from twisted.cred import credentials |
45 | 32 | from twisted.cred.portal import IRealm, Portal | ||
46 | 33 | 33 | ||
47 | 34 | from twisted.internet import defer | 34 | from twisted.internet import defer |
48 | 35 | 35 | ||
50 | 36 | from twisted.python import components, failure | 36 | from twisted.python import failure |
51 | 37 | 37 | ||
52 | 38 | from zope.event import notify | 38 | from zope.event import notify |
53 | 39 | from zope.interface import implements | 39 | from zope.interface import implements |
54 | 40 | 40 | ||
55 | 41 | from lp.codehosting import sftp | 41 | from lp.codehosting import sftp |
56 | 42 | from lp.codehosting.sshserver import events | 42 | from lp.codehosting.sshserver import events |
59 | 43 | from lp.codehosting.sshserver.session import ( | 43 | from lp.codehosting.sshserver.session import PatchedSSHSession |
58 | 44 | launch_smart_server, PatchedSSHSession) | ||
60 | 45 | from lp.services.twistedsupport.xmlrpc import trap_fault | 44 | from lp.services.twistedsupport.xmlrpc import trap_fault |
61 | 46 | from canonical.config import config | ||
62 | 47 | from canonical.launchpad.xmlrpc import faults | 45 | from canonical.launchpad.xmlrpc import faults |
63 | 48 | 46 | ||
64 | 49 | 47 | ||
65 | 50 | class LaunchpadAvatar(avatar.ConchUser): | 48 | class LaunchpadAvatar(avatar.ConchUser): |
66 | 51 | """An account on the SSH server, corresponding to a Launchpad person. | 49 | """An account on the SSH server, corresponding to a Launchpad person. |
67 | 52 | 50 | ||
68 | 53 | :ivar branchfs_proxy: A Twisted XML-RPC client for the authserver. The | ||
69 | 54 | server must implement `IBranchFileSystem`. | ||
70 | 55 | :ivar channelLookup: See `avatar.ConchUser`. | 51 | :ivar channelLookup: See `avatar.ConchUser`. |
71 | 56 | :ivar subsystemLookup: See `avatar.ConchUser`. | 52 | :ivar subsystemLookup: See `avatar.ConchUser`. |
72 | 57 | :ivar user_id: The Launchpad database ID of the Person for this account. | 53 | :ivar user_id: The Launchpad database ID of the Person for this account. |
73 | 58 | :ivar username: The Launchpad username for this account. | 54 | :ivar username: The Launchpad username for this account. |
74 | 59 | """ | 55 | """ |
75 | 60 | 56 | ||
77 | 61 | def __init__(self, userDict, branchfs_proxy): | 57 | def __init__(self, user_dict): |
78 | 58 | """Construct a `LaunchpadAvatar`. | ||
79 | 59 | |||
80 | 60 | :param user_dict: The result of a call to | ||
81 | 61 | `IAuthServer.getUserAndSSHKeys`. | ||
82 | 62 | """ | ||
83 | 62 | avatar.ConchUser.__init__(self) | 63 | avatar.ConchUser.__init__(self) |
87 | 63 | self.branchfs_proxy = branchfs_proxy | 64 | self.user_id = user_dict['id'] |
88 | 64 | self.user_id = userDict['id'] | 65 | self.username = user_dict['name'] |
86 | 65 | self.username = userDict['name'] | ||
89 | 66 | 66 | ||
90 | 67 | # Set the only channel as a standard SSH session (with a couple of bug | 67 | # Set the only channel as a standard SSH session (with a couple of bug |
91 | 68 | # fixes). | 68 | # fixes). |
92 | @@ -74,34 +74,10 @@ | |||
93 | 74 | notify(events.UserLoggedOut(self)) | 74 | notify(events.UserLoggedOut(self)) |
94 | 75 | 75 | ||
95 | 76 | 76 | ||
96 | 77 | components.registerAdapter(launch_smart_server, LaunchpadAvatar, ISession) | ||
97 | 78 | |||
98 | 79 | components.registerAdapter( | ||
99 | 80 | sftp.avatar_to_sftp_server, LaunchpadAvatar, filetransfer.ISFTPServer) | ||
100 | 81 | |||
101 | 82 | |||
102 | 83 | class UserDisplayedUnauthorizedLogin(UnauthorizedLogin): | 77 | class UserDisplayedUnauthorizedLogin(UnauthorizedLogin): |
103 | 84 | """UnauthorizedLogin which should be reported to the user.""" | 78 | """UnauthorizedLogin which should be reported to the user.""" |
104 | 85 | 79 | ||
105 | 86 | 80 | ||
106 | 87 | class Realm: | ||
107 | 88 | implements(IRealm) | ||
108 | 89 | |||
109 | 90 | def __init__(self, authentication_proxy, branchfs_proxy): | ||
110 | 91 | self.authentication_proxy = authentication_proxy | ||
111 | 92 | self.branchfs_proxy = branchfs_proxy | ||
112 | 93 | |||
113 | 94 | def requestAvatar(self, avatarId, mind, *interfaces): | ||
114 | 95 | # Fetch the user's details from the authserver | ||
115 | 96 | deferred = mind.lookupUserDetails(self.authentication_proxy, avatarId) | ||
116 | 97 | |||
117 | 98 | # Once all those details are retrieved, we can construct the avatar. | ||
118 | 99 | def gotUserDict(userDict): | ||
119 | 100 | avatar = LaunchpadAvatar(userDict, self.branchfs_proxy) | ||
120 | 101 | return interfaces[0], avatar, avatar.logout | ||
121 | 102 | return deferred.addCallback(gotUserDict) | ||
122 | 103 | |||
123 | 104 | |||
124 | 105 | class ISSHPrivateKeyWithMind(credentials.ISSHPrivateKey): | 81 | class ISSHPrivateKeyWithMind(credentials.ISSHPrivateKey): |
125 | 106 | """Marker interface for SSH credentials that reference a Mind.""" | 82 | """Marker interface for SSH credentials that reference a Mind.""" |
126 | 107 | 83 | ||
127 | @@ -303,7 +279,7 @@ | |||
128 | 303 | raise UserDisplayedUnauthorizedLogin( | 279 | raise UserDisplayedUnauthorizedLogin( |
129 | 304 | "No such Launchpad account: %s" % credentials.username) | 280 | "No such Launchpad account: %s" % credentials.username) |
130 | 305 | 281 | ||
132 | 306 | def _checkForAuthorizedKey(self, userDict, credentials): | 282 | def _checkForAuthorizedKey(self, user_dict, credentials): |
133 | 307 | """Check the key data in credentials against the keys found in LP.""" | 283 | """Check the key data in credentials against the keys found in LP.""" |
134 | 308 | if credentials.algName == 'ssh-dss': | 284 | if credentials.algName == 'ssh-dss': |
135 | 309 | wantKeyType = 'DSA' | 285 | wantKeyType = 'DSA' |
136 | @@ -313,12 +289,12 @@ | |||
137 | 313 | # unknown key type | 289 | # unknown key type |
138 | 314 | return False | 290 | return False |
139 | 315 | 291 | ||
141 | 316 | if len(userDict['keys']) == 0: | 292 | if len(user_dict['keys']) == 0: |
142 | 317 | raise UserDisplayedUnauthorizedLogin( | 293 | raise UserDisplayedUnauthorizedLogin( |
143 | 318 | "Launchpad user %r doesn't have a registered SSH key" | 294 | "Launchpad user %r doesn't have a registered SSH key" |
144 | 319 | % credentials.username) | 295 | % credentials.username) |
145 | 320 | 296 | ||
147 | 321 | for keytype, keytext in userDict['keys']: | 297 | for keytype, keytext in user_dict['keys']: |
148 | 322 | if keytype != wantKeyType: | 298 | if keytype != wantKeyType: |
149 | 323 | continue | 299 | continue |
150 | 324 | try: | 300 | try: |
151 | @@ -330,11 +306,3 @@ | |||
152 | 330 | raise UnauthorizedLogin( | 306 | raise UnauthorizedLogin( |
153 | 331 | "Your SSH key does not match any key registered for Launchpad " | 307 | "Your SSH key does not match any key registered for Launchpad " |
154 | 332 | "user %s" % credentials.username) | 308 | "user %s" % credentials.username) |
155 | 333 | |||
156 | 334 | |||
157 | 335 | def get_portal(authentication_proxy, branchfs_proxy): | ||
158 | 336 | """Get a portal for connecting to Launchpad codehosting.""" | ||
159 | 337 | portal = Portal(Realm(authentication_proxy, branchfs_proxy)) | ||
160 | 338 | portal.registerChecker( | ||
161 | 339 | PublicKeyFromLaunchpadChecker(authentication_proxy)) | ||
162 | 340 | return portal | ||
163 | 341 | 309 | ||
164 | === added file 'lib/lp/codehosting/sshserver/daemon.py' | |||
165 | --- lib/lp/codehosting/sshserver/daemon.py 1970-01-01 00:00:00 +0000 | |||
166 | +++ lib/lp/codehosting/sshserver/daemon.py 2010-04-16 19:00:53 +0000 | |||
167 | @@ -0,0 +1,105 @@ | |||
168 | 1 | # Copyright 2010 Canonical Ltd. This software is licensed under the | ||
169 | 2 | # GNU Affero General Public License version 3 (see the file LICENSE). | ||
170 | 3 | |||
171 | 4 | """Glues the codehosting SSH daemon together.""" | ||
172 | 5 | |||
173 | 6 | __metaclass__ = type | ||
174 | 7 | __all__ = [ | ||
175 | 8 | 'ACCESS_LOG_NAME', | ||
176 | 9 | 'CodehostingAvatar', | ||
177 | 10 | 'get_key_path', | ||
178 | 11 | 'get_portal', | ||
179 | 12 | 'LOG_NAME', | ||
180 | 13 | 'make_portal', | ||
181 | 14 | 'PRIVATE_KEY_FILE', | ||
182 | 15 | 'PUBLIC_KEY_FILE', | ||
183 | 16 | ] | ||
184 | 17 | |||
185 | 18 | import os | ||
186 | 19 | |||
187 | 20 | from twisted.conch.interfaces import ISession | ||
188 | 21 | from twisted.conch.ssh import filetransfer | ||
189 | 22 | from twisted.cred.portal import IRealm, Portal | ||
190 | 23 | from twisted.python import components | ||
191 | 24 | from twisted.web.xmlrpc import Proxy | ||
192 | 25 | |||
193 | 26 | from zope.interface import implements | ||
194 | 27 | |||
195 | 28 | from canonical.config import config | ||
196 | 29 | from lp.codehosting import sftp | ||
197 | 30 | from lp.codehosting.sshserver.auth import ( | ||
198 | 31 | LaunchpadAvatar, PublicKeyFromLaunchpadChecker) | ||
199 | 32 | from lp.codehosting.sshserver.session import launch_smart_server | ||
200 | 33 | |||
201 | 34 | |||
202 | 35 | # The names of the key files of the server itself. The directory itself is | ||
203 | 36 | # given in config.codehosting.host_key_pair_path. | ||
204 | 37 | PRIVATE_KEY_FILE = 'ssh_host_key_rsa' | ||
205 | 38 | PUBLIC_KEY_FILE = 'ssh_host_key_rsa.pub' | ||
206 | 39 | |||
207 | 40 | OOPS_CONFIG_SECTION = 'codehosting' | ||
208 | 41 | LOG_NAME = 'codehosting' | ||
209 | 42 | ACCESS_LOG_NAME = 'codehosting.access' | ||
210 | 43 | |||
211 | 44 | |||
212 | 45 | class CodehostingAvatar(LaunchpadAvatar): | ||
213 | 46 | """An SSH avatar specific to codehosting. | ||
214 | 47 | |||
215 | 48 | :ivar branchfs_proxy: A Twisted XML-RPC client for the authserver. The | ||
216 | 49 | server must implement `IBranchFileSystem`. | ||
217 | 50 | """ | ||
218 | 51 | |||
219 | 52 | def __init__(self, user_dict, branchfs_proxy): | ||
220 | 53 | LaunchpadAvatar.__init__(self, user_dict) | ||
221 | 54 | self.branchfs_proxy = branchfs_proxy | ||
222 | 55 | |||
223 | 56 | |||
224 | 57 | components.registerAdapter(launch_smart_server, CodehostingAvatar, ISession) | ||
225 | 58 | |||
226 | 59 | components.registerAdapter( | ||
227 | 60 | sftp.avatar_to_sftp_server, CodehostingAvatar, filetransfer.ISFTPServer) | ||
228 | 61 | |||
229 | 62 | |||
230 | 63 | class Realm: | ||
231 | 64 | implements(IRealm) | ||
232 | 65 | |||
233 | 66 | def __init__(self, authentication_proxy, branchfs_proxy): | ||
234 | 67 | self.authentication_proxy = authentication_proxy | ||
235 | 68 | self.branchfs_proxy = branchfs_proxy | ||
236 | 69 | |||
237 | 70 | def requestAvatar(self, avatar_id, mind, *interfaces): | ||
238 | 71 | # Fetch the user's details from the authserver | ||
239 | 72 | deferred = mind.lookupUserDetails( | ||
240 | 73 | self.authentication_proxy, avatar_id) | ||
241 | 74 | |||
242 | 75 | # Once all those details are retrieved, we can construct the avatar. | ||
243 | 76 | def got_user_dict(user_dict): | ||
244 | 77 | avatar = CodehostingAvatar(user_dict, self.branchfs_proxy) | ||
245 | 78 | return interfaces[0], avatar, avatar.logout | ||
246 | 79 | |||
247 | 80 | return deferred.addCallback(got_user_dict) | ||
248 | 81 | |||
249 | 82 | |||
250 | 83 | def get_portal(authentication_proxy, branchfs_proxy): | ||
251 | 84 | """Get a portal for connecting to Launchpad codehosting.""" | ||
252 | 85 | portal = Portal(Realm(authentication_proxy, branchfs_proxy)) | ||
253 | 86 | portal.registerChecker( | ||
254 | 87 | PublicKeyFromLaunchpadChecker(authentication_proxy)) | ||
255 | 88 | return portal | ||
256 | 89 | |||
257 | 90 | |||
258 | 91 | def get_key_path(key_filename): | ||
259 | 92 | key_directory = config.codehosting.host_key_pair_path | ||
260 | 93 | return os.path.join(config.root, key_directory, key_filename) | ||
261 | 94 | |||
262 | 95 | |||
263 | 96 | def make_portal(): | ||
264 | 97 | """Create and return a `Portal` for the SSH service. | ||
265 | 98 | |||
266 | 99 | This portal accepts SSH credentials and returns our customized SSH | ||
267 | 100 | avatars (see `lp.codehosting.sshserver.auth.CodehostingAvatar`). | ||
268 | 101 | """ | ||
269 | 102 | authentication_proxy = Proxy( | ||
270 | 103 | config.codehosting.authentication_endpoint) | ||
271 | 104 | branchfs_proxy = Proxy(config.codehosting.branchfs_endpoint) | ||
272 | 105 | return get_portal(authentication_proxy, branchfs_proxy) | ||
273 | 0 | 106 | ||
274 | === modified file 'lib/lp/codehosting/sshserver/service.py' | |||
275 | --- lib/lp/codehosting/sshserver/service.py 2010-04-16 19:00:51 +0000 | |||
276 | +++ lib/lp/codehosting/sshserver/service.py 2010-04-16 19:00:53 +0000 | |||
277 | @@ -8,12 +8,6 @@ | |||
278 | 8 | 8 | ||
279 | 9 | __metaclass__ = type | 9 | __metaclass__ = type |
280 | 10 | __all__ = [ | 10 | __all__ = [ |
281 | 11 | 'ACCESS_LOG_NAME', | ||
282 | 12 | 'get_key_path', | ||
283 | 13 | 'LOG_NAME', | ||
284 | 14 | 'make_portal', | ||
285 | 15 | 'PRIVATE_KEY_FILE', | ||
286 | 16 | 'PUBLIC_KEY_FILE', | ||
287 | 17 | 'SSHService', | 11 | 'SSHService', |
288 | 18 | ] | 12 | ] |
289 | 19 | 13 | ||
290 | @@ -27,13 +21,11 @@ | |||
291 | 27 | from twisted.conch.ssh.transport import SSHServerTransport | 21 | from twisted.conch.ssh.transport import SSHServerTransport |
292 | 28 | from twisted.internet import defer | 22 | from twisted.internet import defer |
293 | 29 | from twisted.protocols.policies import TimeoutFactory | 23 | from twisted.protocols.policies import TimeoutFactory |
294 | 30 | from twisted.web.xmlrpc import Proxy | ||
295 | 31 | 24 | ||
296 | 32 | from zope.event import notify | 25 | from zope.event import notify |
297 | 33 | 26 | ||
298 | 34 | from canonical.config import config | ||
299 | 35 | from lp.codehosting.sshserver import accesslog, events | 27 | from lp.codehosting.sshserver import accesslog, events |
301 | 36 | from lp.codehosting.sshserver.auth import get_portal, SSHUserAuthServer | 28 | from lp.codehosting.sshserver.auth import SSHUserAuthServer |
302 | 37 | from lp.services.twistedsupport import gatherResults | 29 | from lp.services.twistedsupport import gatherResults |
303 | 38 | from lp.services.twistedsupport.loggingsupport import set_up_oops_reporting | 30 | from lp.services.twistedsupport.loggingsupport import set_up_oops_reporting |
304 | 39 | 31 | ||
305 | @@ -43,16 +35,6 @@ | |||
306 | 43 | PRIVATE_KEY_FILE = 'ssh_host_key_rsa' | 35 | PRIVATE_KEY_FILE = 'ssh_host_key_rsa' |
307 | 44 | PUBLIC_KEY_FILE = 'ssh_host_key_rsa.pub' | 36 | PUBLIC_KEY_FILE = 'ssh_host_key_rsa.pub' |
308 | 45 | 37 | ||
309 | 46 | OOPS_CONFIG_SECTION = 'codehosting' | ||
310 | 47 | LOG_NAME = 'codehosting' | ||
311 | 48 | ACCESS_LOG_NAME = 'codehosting.access' | ||
312 | 49 | |||
313 | 50 | |||
314 | 51 | # The names of the key files of the server itself. The directory itself is | ||
315 | 52 | # given in config.codehosting.host_key_pair_path. | ||
316 | 53 | PRIVATE_KEY_FILE = 'ssh_host_key_rsa' | ||
317 | 54 | PUBLIC_KEY_FILE = 'ssh_host_key_rsa.pub' | ||
318 | 55 | |||
319 | 56 | 38 | ||
320 | 57 | class KeepAliveSettingSSHServerTransport(SSHServerTransport): | 39 | class KeepAliveSettingSSHServerTransport(SSHServerTransport): |
321 | 58 | 40 | ||
322 | @@ -61,44 +43,24 @@ | |||
323 | 61 | self.transport.setTcpKeepAlive(True) | 43 | self.transport.setTcpKeepAlive(True) |
324 | 62 | 44 | ||
325 | 63 | 45 | ||
364 | 64 | <<<<<<< TREE | 46 | def get_key_path(key_filename): |
365 | 65 | def get_key_path(key_filename): | 47 | key_directory = config.codehosting.host_key_pair_path |
366 | 66 | key_directory = config.codehosting.host_key_pair_path | 48 | return os.path.join(config.root, key_directory, key_filename) |
367 | 67 | return os.path.join(config.root, key_directory, key_filename) | 49 | |
368 | 68 | 50 | ||
369 | 69 | 51 | def make_portal(): | |
370 | 70 | def make_portal(): | 52 | """Create and return a `Portal` for the SSH service. |
371 | 71 | """Create and return a `Portal` for the SSH service. | 53 | |
372 | 72 | 54 | This portal accepts SSH credentials and returns our customized SSH | |
373 | 73 | This portal accepts SSH credentials and returns our customized SSH | 55 | avatars (see `lp.codehosting.sshserver.auth.LaunchpadAvatar`). |
374 | 74 | avatars (see `lp.codehosting.sshserver.auth.LaunchpadAvatar`). | 56 | """ |
375 | 75 | """ | 57 | authentication_proxy = Proxy( |
376 | 76 | authentication_proxy = Proxy( | 58 | config.codehosting.authentication_endpoint) |
377 | 77 | config.codehosting.authentication_endpoint) | 59 | branchfs_proxy = Proxy(config.codehosting.branchfs_endpoint) |
378 | 78 | branchfs_proxy = Proxy(config.codehosting.branchfs_endpoint) | 60 | return get_portal(authentication_proxy, branchfs_proxy) |
379 | 79 | return get_portal(authentication_proxy, branchfs_proxy) | 61 | |
380 | 80 | 62 | ||
381 | 81 | 63 | ||
344 | 82 | |||
345 | 83 | ======= | ||
346 | 84 | def get_key_path(key_filename): | ||
347 | 85 | key_directory = config.codehosting.host_key_pair_path | ||
348 | 86 | return os.path.join(config.root, key_directory, key_filename) | ||
349 | 87 | |||
350 | 88 | |||
351 | 89 | def make_portal(): | ||
352 | 90 | """Create and return a `Portal` for the SSH service. | ||
353 | 91 | |||
354 | 92 | This portal accepts SSH credentials and returns our customized SSH | ||
355 | 93 | avatars (see `lp.codehosting.sshserver.auth.LaunchpadAvatar`). | ||
356 | 94 | """ | ||
357 | 95 | authentication_proxy = Proxy( | ||
358 | 96 | config.codehosting.authentication_endpoint) | ||
359 | 97 | branchfs_proxy = Proxy(config.codehosting.branchfs_endpoint) | ||
360 | 98 | return get_portal(authentication_proxy, branchfs_proxy) | ||
361 | 99 | |||
362 | 100 | |||
363 | 101 | >>>>>>> MERGE-SOURCE | ||
382 | 102 | class Factory(SSHFactory): | 64 | class Factory(SSHFactory): |
383 | 103 | """SSH factory that uses Launchpad's custom authentication. | 65 | """SSH factory that uses Launchpad's custom authentication. |
384 | 104 | 66 | ||
385 | 105 | 67 | ||
386 | === modified file 'lib/lp/codehosting/sshserver/tests/test_auth.py' | |||
387 | --- lib/lp/codehosting/sshserver/tests/test_auth.py 2010-04-13 18:32:15 +0000 | |||
388 | +++ lib/lp/codehosting/sshserver/tests/test_auth.py 2010-04-16 19:00:53 +0000 | |||
389 | @@ -18,14 +18,13 @@ | |||
390 | 18 | from twisted.internet import defer | 18 | from twisted.internet import defer |
391 | 19 | from twisted.python import failure | 19 | from twisted.python import failure |
392 | 20 | from twisted.python.util import sibpath | 20 | from twisted.python.util import sibpath |
393 | 21 | from twisted.test.proto_helpers import StringTransport | ||
394 | 22 | 21 | ||
395 | 23 | from twisted.trial.unittest import TestCase as TrialTestCase | 22 | from twisted.trial.unittest import TestCase as TrialTestCase |
396 | 24 | 23 | ||
397 | 25 | from canonical.config import config | 24 | from canonical.config import config |
398 | 26 | from canonical.launchpad.xmlrpc import faults | 25 | from canonical.launchpad.xmlrpc import faults |
399 | 27 | from canonical.testing.layers import TwistedLayer | 26 | from canonical.testing.layers import TwistedLayer |
401 | 28 | from lp.codehosting.sshserver import auth, service | 27 | from lp.codehosting.sshserver import auth |
402 | 29 | from lp.services.twistedsupport import suppress_stderr | 28 | from lp.services.twistedsupport import suppress_stderr |
403 | 30 | 29 | ||
404 | 31 | 30 | ||
405 | @@ -38,14 +37,12 @@ | |||
406 | 38 | 37 | ||
407 | 39 | implements(IRealm) | 38 | implements(IRealm) |
408 | 40 | 39 | ||
410 | 41 | def requestAvatar(self, avatarId, mind, *interfaces): | 40 | def requestAvatar(self, avatar_id, mind, *interfaces): |
411 | 42 | user_dict = { | 41 | user_dict = { |
413 | 43 | 'id': avatarId, 'name': avatarId, 'teams': [], | 42 | 'id': avatar_id, 'name': avatar_id, 'teams': [], |
414 | 44 | 'initialBranches': []} | 43 | 'initialBranches': []} |
415 | 45 | return ( | 44 | return ( |
419 | 46 | interfaces[0], | 45 | interfaces[0], auth.LaunchpadAvatar(user_dict), lambda: None) |
417 | 47 | auth.LaunchpadAvatar(user_dict, None), | ||
418 | 48 | lambda: None) | ||
420 | 49 | 46 | ||
421 | 50 | 47 | ||
422 | 51 | class MockSSHTransport(SSHServerTransport): | 48 | class MockSSHTransport(SSHServerTransport): |
423 | @@ -501,6 +498,7 @@ | |||
424 | 501 | return d | 498 | return d |
425 | 502 | 499 | ||
426 | 503 | 500 | ||
427 | 501 | <<<<<<< TREE | ||
428 | 504 | class StringTransportWith_setTcpKeepAlive(StringTransport): | 502 | class StringTransportWith_setTcpKeepAlive(StringTransport): |
429 | 505 | def __init__(self, hostAddress=None, peerAddress=None): | 503 | def __init__(self, hostAddress=None, peerAddress=None): |
430 | 506 | StringTransport.__init__(self, hostAddress, peerAddress) | 504 | StringTransport.__init__(self, hostAddress, peerAddress) |
431 | @@ -569,5 +567,7 @@ | |||
432 | 569 | 567 | ||
433 | 570 | self.assertNotIdentical(mind1.cache, mind2.cache) | 568 | self.assertNotIdentical(mind1.cache, mind2.cache) |
434 | 571 | 569 | ||
435 | 570 | ======= | ||
436 | 571 | >>>>>>> MERGE-SOURCE | ||
437 | 572 | def test_suite(): | 572 | def test_suite(): |
438 | 573 | return unittest.TestLoader().loadTestsFromName(__name__) | 573 | return unittest.TestLoader().loadTestsFromName(__name__) |
439 | 574 | 574 | ||
440 | === added file 'lib/lp/codehosting/sshserver/tests/test_daemon.py' | |||
441 | --- lib/lp/codehosting/sshserver/tests/test_daemon.py 1970-01-01 00:00:00 +0000 | |||
442 | +++ lib/lp/codehosting/sshserver/tests/test_daemon.py 2010-04-16 19:00:53 +0000 | |||
443 | @@ -0,0 +1,93 @@ | |||
444 | 1 | # Copyright 2010 Canonical Ltd. This software is licensed under the | ||
445 | 2 | # GNU Affero General Public License version 3 (see the file LICENSE). | ||
446 | 3 | |||
447 | 4 | """Tests for the codehosting SSH server glue.""" | ||
448 | 5 | |||
449 | 6 | __metaclass__ = type | ||
450 | 7 | |||
451 | 8 | import unittest | ||
452 | 9 | |||
453 | 10 | from twisted.conch.ssh.common import NS | ||
454 | 11 | from twisted.conch.ssh.keys import Key | ||
455 | 12 | from twisted.test.proto_helpers import StringTransport | ||
456 | 13 | from twisted.trial.unittest import TestCase as TrialTestCase | ||
457 | 14 | |||
458 | 15 | from canonical.testing.layers import TwistedLayer | ||
459 | 16 | |||
460 | 17 | from lp.codehosting.sshserver.auth import SSHUserAuthServer | ||
461 | 18 | from lp.codehosting.sshserver.daemon import ( | ||
462 | 19 | get_key_path, get_portal, PRIVATE_KEY_FILE, PUBLIC_KEY_FILE) | ||
463 | 20 | from lp.codehosting.sshserver.service import Factory | ||
464 | 21 | |||
465 | 22 | |||
466 | 23 | class StringTransportWith_setTcpKeepAlive(StringTransport): | ||
467 | 24 | def __init__(self, hostAddress=None, peerAddress=None): | ||
468 | 25 | StringTransport.__init__(self, hostAddress, peerAddress) | ||
469 | 26 | self._keepAlive = False | ||
470 | 27 | |||
471 | 28 | def setTcpKeepAlive(self, flag): | ||
472 | 29 | self._keepAlive = flag | ||
473 | 30 | |||
474 | 31 | |||
475 | 32 | class TestFactory(TrialTestCase): | ||
476 | 33 | """Tests for our SSH factory.""" | ||
477 | 34 | |||
478 | 35 | layer = TwistedLayer | ||
479 | 36 | |||
480 | 37 | def makeFactory(self): | ||
481 | 38 | """Create and start the factory that our SSH server uses.""" | ||
482 | 39 | factory = Factory( | ||
483 | 40 | get_portal(None, None), | ||
484 | 41 | private_key=Key.fromFile( | ||
485 | 42 | get_key_path(PRIVATE_KEY_FILE)), | ||
486 | 43 | public_key=Key.fromFile( | ||
487 | 44 | get_key_path(PUBLIC_KEY_FILE))) | ||
488 | 45 | factory.startFactory() | ||
489 | 46 | return factory | ||
490 | 47 | |||
491 | 48 | def startConnecting(self, factory): | ||
492 | 49 | """Connect to the `factory`.""" | ||
493 | 50 | server_transport = factory.buildProtocol(None) | ||
494 | 51 | server_transport.makeConnection(StringTransportWith_setTcpKeepAlive()) | ||
495 | 52 | return server_transport | ||
496 | 53 | |||
497 | 54 | def test_set_keepalive_on_connection(self): | ||
498 | 55 | # The server transport sets TCP keep alives on the underlying | ||
499 | 56 | # transport. | ||
500 | 57 | factory = self.makeFactory() | ||
501 | 58 | server_transport = self.startConnecting(factory) | ||
502 | 59 | self.assertTrue(server_transport.transport._keepAlive) | ||
503 | 60 | |||
504 | 61 | def beginAuthentication(self, factory): | ||
505 | 62 | """Connect to `factory` and begin authentication on this connection. | ||
506 | 63 | |||
507 | 64 | :return: The `SSHServerTransport` after the process of authentication | ||
508 | 65 | has begun. | ||
509 | 66 | """ | ||
510 | 67 | server_transport = self.startConnecting(factory) | ||
511 | 68 | server_transport.ssh_SERVICE_REQUEST(NS('ssh-userauth')) | ||
512 | 69 | self.addCleanup(server_transport.service.serviceStopped) | ||
513 | 70 | return server_transport | ||
514 | 71 | |||
515 | 72 | def test_authentication_uses_our_userauth_service(self): | ||
516 | 73 | # The service of a SSHServerTransport after authentication has started | ||
517 | 74 | # is an instance of our SSHUserAuthServer class. | ||
518 | 75 | factory = self.makeFactory() | ||
519 | 76 | transport = self.beginAuthentication(factory) | ||
520 | 77 | self.assertIsInstance(transport.service, SSHUserAuthServer) | ||
521 | 78 | |||
522 | 79 | def test_two_connections_two_minds(self): | ||
523 | 80 | # Two attempts to authenticate do not share the user-details cache. | ||
524 | 81 | factory = self.makeFactory() | ||
525 | 82 | |||
526 | 83 | server_transport1 = self.beginAuthentication(factory) | ||
527 | 84 | server_transport2 = self.beginAuthentication(factory) | ||
528 | 85 | |||
529 | 86 | mind1 = server_transport1.service.getMind() | ||
530 | 87 | mind2 = server_transport2.service.getMind() | ||
531 | 88 | |||
532 | 89 | self.assertNotIdentical(mind1.cache, mind2.cache) | ||
533 | 90 | |||
534 | 91 | |||
535 | 92 | def test_suite(): | ||
536 | 93 | return unittest.TestLoader().loadTestsFromName(__name__) | ||
537 | 0 | 94 | ||
538 | === modified file 'lib/lp/codehosting/sshserver/tests/test_session.py' | |||
539 | --- lib/lp/codehosting/sshserver/tests/test_session.py 2010-03-15 06:42:34 +0000 | |||
540 | +++ lib/lp/codehosting/sshserver/tests/test_session.py 2010-04-16 19:00:53 +0000 | |||
541 | @@ -14,7 +14,7 @@ | |||
542 | 14 | 14 | ||
543 | 15 | from canonical.config import config | 15 | from canonical.config import config |
544 | 16 | from lp.codehosting import get_bzr_path, get_BZR_PLUGIN_PATH_for_subprocess | 16 | from lp.codehosting import get_bzr_path, get_BZR_PLUGIN_PATH_for_subprocess |
546 | 17 | from lp.codehosting.sshserver.auth import LaunchpadAvatar | 17 | from lp.codehosting.sshserver.daemon import CodehostingAvatar |
547 | 18 | from lp.codehosting.sshserver.session import ( | 18 | from lp.codehosting.sshserver.session import ( |
548 | 19 | ExecOnlySession, ForbiddenCommand, RestrictedExecOnlySession) | 19 | ExecOnlySession, ForbiddenCommand, RestrictedExecOnlySession) |
549 | 20 | from lp.codehosting.tests.helpers import AvatarTestCase | 20 | from lp.codehosting.tests.helpers import AvatarTestCase |
550 | @@ -83,7 +83,7 @@ | |||
551 | 83 | 83 | ||
552 | 84 | def setUp(self): | 84 | def setUp(self): |
553 | 85 | AvatarTestCase.setUp(self) | 85 | AvatarTestCase.setUp(self) |
555 | 86 | self.avatar = LaunchpadAvatar(self.aliceUserDict, None) | 86 | self.avatar = CodehostingAvatar(self.aliceUserDict, None) |
556 | 87 | # The logging system will try to get the id of avatar.transport, so | 87 | # The logging system will try to get the id of avatar.transport, so |
557 | 88 | # let's give it something to take the id of. | 88 | # let's give it something to take the id of. |
558 | 89 | self.avatar.transport = object() | 89 | self.avatar.transport = object() |
559 | @@ -239,7 +239,7 @@ | |||
560 | 239 | 239 | ||
561 | 240 | def setUp(self): | 240 | def setUp(self): |
562 | 241 | AvatarTestCase.setUp(self) | 241 | AvatarTestCase.setUp(self) |
564 | 242 | self.avatar = LaunchpadAvatar(self.aliceUserDict, None) | 242 | self.avatar = CodehostingAvatar(self.aliceUserDict, None) |
565 | 243 | self.reactor = MockReactor() | 243 | self.reactor = MockReactor() |
566 | 244 | self.session = RestrictedExecOnlySession( | 244 | self.session = RestrictedExecOnlySession( |
567 | 245 | self.avatar, self.reactor, 'foo', 'bar baz %(user_id)s') | 245 | self.avatar, self.reactor, 'foo', 'bar baz %(user_id)s') |
568 | @@ -305,7 +305,7 @@ | |||
569 | 305 | 305 | ||
570 | 306 | def setUp(self): | 306 | def setUp(self): |
571 | 307 | AvatarTestCase.setUp(self) | 307 | AvatarTestCase.setUp(self) |
573 | 308 | self.avatar = LaunchpadAvatar(self.aliceUserDict, None) | 308 | self.avatar = CodehostingAvatar(self.aliceUserDict, None) |
574 | 309 | 309 | ||
575 | 310 | def test_avatarAdaptsToRestrictedExecOnlySession(self): | 310 | def test_avatarAdaptsToRestrictedExecOnlySession(self): |
576 | 311 | # When Conch tries to adapt the SSH server avatar to ISession, it | 311 | # When Conch tries to adapt the SSH server avatar to ISession, it |
577 | 312 | 312 | ||
578 | === modified file 'lib/lp/codehosting/tests/helpers.py' | |||
579 | --- lib/lp/codehosting/tests/helpers.py 2009-06-25 04:06:00 +0000 | |||
580 | +++ lib/lp/codehosting/tests/helpers.py 2010-04-16 19:00:53 +0000 | |||
581 | @@ -7,9 +7,7 @@ | |||
582 | 7 | __all__ = [ | 7 | __all__ = [ |
583 | 8 | 'AvatarTestCase', | 8 | 'AvatarTestCase', |
584 | 9 | 'adapt_suite', | 9 | 'adapt_suite', |
585 | 10 | 'BranchTestCase', | ||
586 | 11 | 'CodeHostingTestProviderAdapter', | 10 | 'CodeHostingTestProviderAdapter', |
587 | 12 | 'CodeHostingRepositoryTestProviderAdapter', | ||
588 | 13 | 'create_branch_with_one_revision', | 11 | 'create_branch_with_one_revision', |
589 | 14 | 'deferToThread', | 12 | 'deferToThread', |
590 | 15 | 'LoomTestMixin', | 13 | 'LoomTestMixin', |
591 | 16 | 14 | ||
592 | === modified file 'lib/lp/codehosting/tests/test_sftp.py' | |||
593 | --- lib/lp/codehosting/tests/test_sftp.py 2010-02-01 04:33:22 +0000 | |||
594 | +++ lib/lp/codehosting/tests/test_sftp.py 2010-04-16 19:00:53 +0000 | |||
595 | @@ -25,7 +25,7 @@ | |||
596 | 25 | from lp.codehosting.inmemory import InMemoryFrontend, XMLRPCWrapper | 25 | from lp.codehosting.inmemory import InMemoryFrontend, XMLRPCWrapper |
597 | 26 | from lp.codehosting.sftp import ( | 26 | from lp.codehosting.sftp import ( |
598 | 27 | FatLocalTransport, TransportSFTPServer, FileIsADirectory) | 27 | FatLocalTransport, TransportSFTPServer, FileIsADirectory) |
600 | 28 | from lp.codehosting.sshserver.auth import LaunchpadAvatar | 28 | from lp.codehosting.sshserver.daemon import CodehostingAvatar |
601 | 29 | from lp.testing.factory import LaunchpadObjectFactory | 29 | from lp.testing.factory import LaunchpadObjectFactory |
602 | 30 | from canonical.testing.layers import TwistedLayer | 30 | from canonical.testing.layers import TwistedLayer |
603 | 31 | 31 | ||
604 | @@ -110,13 +110,13 @@ | |||
605 | 110 | self.branchfs_endpoint = XMLRPCWrapper( | 110 | self.branchfs_endpoint = XMLRPCWrapper( |
606 | 111 | frontend.getFilesystemEndpoint()) | 111 | frontend.getFilesystemEndpoint()) |
607 | 112 | 112 | ||
609 | 113 | def makeLaunchpadAvatar(self): | 113 | def makeCodehostingAvatar(self): |
610 | 114 | user = self.factory.makePerson() | 114 | user = self.factory.makePerson() |
611 | 115 | user_dict = dict(id=user.id, name=user.name) | 115 | user_dict = dict(id=user.id, name=user.name) |
613 | 116 | return LaunchpadAvatar(user_dict, self.branchfs_endpoint) | 116 | return CodehostingAvatar(user_dict, self.branchfs_endpoint) |
614 | 117 | 117 | ||
615 | 118 | def test_canAdaptToSFTPServer(self): | 118 | def test_canAdaptToSFTPServer(self): |
617 | 119 | avatar = self.makeLaunchpadAvatar() | 119 | avatar = self.makeCodehostingAvatar() |
618 | 120 | # The adapter logs the SFTPStarted event, which gets the id of the | 120 | # The adapter logs the SFTPStarted event, which gets the id of the |
619 | 121 | # transport attribute of 'avatar'. Here we set transport to an | 121 | # transport attribute of 'avatar'. Here we set transport to an |
620 | 122 | # arbitrary object that can have its id taken. | 122 | # arbitrary object that can have its id taken. |
There were a few places where moved legacy code didn't conform to our stringent coding conventions, and you kindly offered to fix them before landing this branch.