Merge lp:~wgrant/launchpad/hardcoded-password into lp:launchpad

Proposed by William Grant on 2012-01-17
Status: Merged
Merged at revision: 14693
Proposed branch: lp:~wgrant/launchpad/hardcoded-password
Merge into: lp:launchpad
Diff against target: 420 lines (+82/-76)
11 files modified
configs/testrunner/launchpad-lazr.conf (+1/-0)
lib/lp/blueprints/browser/tests/test_sprint.py (+1/-1)
lib/lp/bugs/scripts/bugexpire.py (+1/-2)
lib/lp/bugs/scripts/checkwatches/base.py (+1/-1)
lib/lp/services/config/schema-lazr.conf (+6/-0)
lib/lp/services/webapp/authentication.py (+18/-32)
lib/lp/services/webapp/interaction.py (+1/-1)
lib/lp/services/webapp/interfaces.py (+4/-14)
lib/lp/services/webapp/tests/test_authutility.py (+47/-20)
lib/lp/testing/pages.py (+1/-4)
lib/lp/testing/yuixhr.py (+1/-1)
To merge this branch: bzr merge lp:~wgrant/launchpad/hardcoded-password
Reviewer Review Type Date Requested Status
Steve Kowalik (community) code 2012-01-17 Approve on 2012-01-18
Review via email: mp+88966@code.launchpad.net

Commit Message

[r=stevenk][no-qa] Disable basic auth outside the testrunner, and use 'test' as the password instead of looking things up in AccountPassword.

Description of the Change

This branch drops most of the remaining password support in Launchpad. The major changes are in lp.services.webapp.authentication, where I've disabled basic authentication outside the testrunner, added several more paranoid is-testrunner checks, and replaced the password validation with a string match against 'test'.

To post a comment you must log in.
Steve Kowalik (stevenk) wrote :

I'm a little nervous about this change, but having said that, as long as this passes ec2, I'm okay with it, since, as you say it won't get anywhere near production.

review: Approve (code)

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'configs/testrunner/launchpad-lazr.conf'
2--- configs/testrunner/launchpad-lazr.conf 2012-01-01 02:25:04 +0000
3+++ configs/testrunner/launchpad-lazr.conf 2012-01-18 06:45:31 +0000
4@@ -114,6 +114,7 @@
5 max_scaling: 2
6
7 [launchpad]
8+basic_auth_password: test
9 max_attachment_size: 1024
10 geoip_database: /usr/share/GeoIP/GeoLiteCity.dat
11 logparser_max_parsed_lines: 100000
12
13=== modified file 'lib/lp/blueprints/browser/tests/test_sprint.py'
14--- lib/lp/blueprints/browser/tests/test_sprint.py 2012-01-01 02:58:52 +0000
15+++ lib/lp/blueprints/browser/tests/test_sprint.py 2012-01-18 06:45:31 +0000
16@@ -26,4 +26,4 @@
17 True)
18 Store.of(sprint).flush()
19 Store.of(sprint).invalidate()
20- self.assertThat(sprint, BrowsesWithQueryLimit(15, sprint.owner))
21+ self.assertThat(sprint, BrowsesWithQueryLimit(17, sprint.owner))
22
23=== modified file 'lib/lp/bugs/scripts/bugexpire.py'
24--- lib/lp/bugs/scripts/bugexpire.py 2012-01-01 02:58:52 +0000
25+++ lib/lp/bugs/scripts/bugexpire.py 2012-01-18 06:45:31 +0000
26@@ -120,8 +120,7 @@
27 auth_utility = getUtility(IPlacelessAuthUtility)
28 janitor_email = self.janitor.preferredemail.email
29 setupInteraction(
30- auth_utility.getPrincipalByLogin(
31- janitor_email, want_password=False),
32+ auth_utility.getPrincipalByLogin(janitor_email),
33 login=janitor_email)
34
35 def _logout(self):
36
37=== modified file 'lib/lp/bugs/scripts/checkwatches/base.py'
38--- lib/lp/bugs/scripts/checkwatches/base.py 2012-01-01 02:58:52 +0000
39+++ lib/lp/bugs/scripts/checkwatches/base.py 2012-01-18 06:45:31 +0000
40@@ -121,7 +121,7 @@
41 self._login = login
42 self._principal = (
43 getUtility(IPlacelessAuthUtility).getPrincipalByLogin(
44- self._login, want_password=False))
45+ self._login))
46 self._transaction_manager = transaction_manager
47 self.logger = logger
48
49
50=== modified file 'lib/lp/services/config/schema-lazr.conf'
51--- lib/lp/services/config/schema-lazr.conf 2012-01-06 11:08:30 +0000
52+++ lib/lp/services/config/schema-lazr.conf 2012-01-18 06:45:31 +0000
53@@ -894,6 +894,12 @@
54 # datatype: boolean
55 enable_test_openid_provider: False
56
57+# The password to accept for all users for HTTP basic auth.
58+# If None, HTTP basic auth is disabled
59+# Obviously this should never be set in anything even vaguely
60+# resembling a production enviornment.
61+basic_auth_password: none
62+
63 # Assume the slave database is lagged if it takes more than this many
64 # milliseconds to calculate this information from the Slony-I tables.
65 # datatype: integer
66
67=== modified file 'lib/lp/services/webapp/authentication.py'
68--- lib/lp/services/webapp/authentication.py 2012-01-03 11:08:31 +0000
69+++ lib/lp/services/webapp/authentication.py 2012-01-18 06:45:31 +0000
70@@ -64,13 +64,21 @@
71 self.nobody.__parent__ = self
72
73 def _authenticateUsingBasicAuth(self, credentials, request):
74+ # authenticate() only attempts basic auth if it's enabled. But
75+ # recheck here, just in case. There is a single password for all
76+ # users, so this must never get anywhere near production!
77+ if (not config.launchpad.basic_auth_password
78+ or config.launchpad.basic_auth_password.lower() == 'none'):
79+ raise AssertionError(
80+ "Attempted to use basic auth when it is disabled")
81+
82 login = credentials.getLogin()
83 if login is not None:
84 login_src = getUtility(IPlacelessLoginSource)
85 principal = login_src.getPrincipalByLogin(login)
86 if principal is not None and principal.person.is_valid_person:
87 password = credentials.getPassword()
88- if principal.validate(password):
89+ if password == config.launchpad.basic_auth_password:
90 # We send a LoggedInEvent here, when the
91 # cookie auth below sends a PrincipalIdentified,
92 # as the login form is never visited for BasicAuth.
93@@ -130,7 +138,8 @@
94 # encoded properly. That's a client error, so we don't really
95 # care, and we're done.
96 raise Unauthorized("Bad Basic authentication.")
97- if credentials is not None and credentials.getLogin() is not None:
98+ if (config.launchpad.basic_auth_password and credentials is not None
99+ and credentials.getLogin() is not None):
100 return self._authenticateUsingBasicAuth(credentials, request)
101 else:
102 # Hack to make us not even think of using a session if there
103@@ -166,10 +175,9 @@
104 utility = getUtility(IPlacelessLoginSource)
105 return utility.getPrincipals(name)
106
107- def getPrincipalByLogin(self, login, want_password=True):
108+ def getPrincipalByLogin(self, login):
109 """See IAuthenticationService."""
110- utility = getUtility(IPlacelessLoginSource)
111- return utility.getPrincipalByLogin(login, want_password=want_password)
112+ return getUtility(IPlacelessLoginSource).getPrincipalByLogin(login)
113
114
115 class SSHADigestEncryptor:
116@@ -251,16 +259,10 @@
117
118 def getPrincipalByLogin(self, login,
119 access_level=AccessLevel.WRITE_PRIVATE,
120- scope=None, want_password=True):
121+ scope=None):
122 """Return a principal based on the account with the email address
123 signified by "login".
124
125- :param want_password: If want_password is False, the pricipal
126- will have None for a password. Use this when trying to retrieve a
127- principal in contexts where we don't need the password and the
128- database connection does not have access to the Account or
129- AccountPassword tables.
130-
131 :return: None if there is no account with the given email address.
132
133 The `access_level` can be used for further restricting the capability
134@@ -279,27 +281,18 @@
135 person = getUtility(IPersonSet).getByEmail(login)
136 if person is None or person.account is None:
137 return None
138- return self._principalForAccount(
139- person.account, access_level, scope, want_password)
140+ return self._principalForAccount(person.account, access_level, scope)
141
142- def _principalForAccount(self, account, access_level, scope,
143- want_password=True):
144+ def _principalForAccount(self, account, access_level, scope):
145 """Return a LaunchpadPrincipal for the given account.
146
147 The LaunchpadPrincipal will also have the given access level and
148 scope.
149-
150- If want_password is True, the principal's password will be set to the
151- account's password. Otherwise it's set to None.
152 """
153 naked_account = removeSecurityProxy(account)
154- if want_password:
155- password = naked_account.password
156- else:
157- password = None
158 principal = LaunchpadPrincipal(
159 naked_account.id, naked_account.displayname,
160- naked_account.displayname, account, password,
161+ naked_account.displayname, account,
162 access_level=access_level, scope=scope)
163 principal.__parent__ = self
164 return principal
165@@ -315,7 +308,7 @@
166
167 implements(ILaunchpadPrincipal)
168
169- def __init__(self, id, title, description, account, pwd=None,
170+ def __init__(self, id, title, description, account,
171 access_level=AccessLevel.WRITE_PRIVATE, scope=None):
172 self.id = id
173 self.title = title
174@@ -324,17 +317,10 @@
175 self.scope = scope
176 self.account = account
177 self.person = IPerson(account, None)
178- self.__pwd = pwd
179
180 def getLogin(self):
181 return self.title
182
183- def validate(self, pw):
184- encryptor = getUtility(IPasswordEncryptor)
185- pw1 = (pw or '').strip()
186- pw2 = (self.__pwd or '').strip()
187- return encryptor.validate(pw1, pw2)
188-
189
190 # zope.app.apidoc expects our principals to be adaptable into IAnnotations, so
191 # we use these dummy adapters here just to make that code not OOPS.
192
193=== modified file 'lib/lp/services/webapp/interaction.py'
194--- lib/lp/services/webapp/interaction.py 2011-12-24 17:49:30 +0000
195+++ lib/lp/services/webapp/interaction.py 2012-01-18 06:45:31 +0000
196@@ -143,7 +143,7 @@
197 # IPersonSet.getByEmail() and since this is security wrapped, it needs
198 # an interaction available.
199 setupInteraction(authutil.unauthenticatedPrincipal())
200- principal = authutil.getPrincipalByLogin(email, want_password=False)
201+ principal = authutil.getPrincipalByLogin(email)
202 assert principal is not None, "Invalid login"
203 if principal.person is not None and principal.person.is_team:
204 raise AssertionError("Please do not try to login as a team")
205
206=== modified file 'lib/lp/services/webapp/interfaces.py'
207--- lib/lp/services/webapp/interfaces.py 2011-12-24 17:49:30 +0000
208+++ lib/lp/services/webapp/interfaces.py 2012-01-18 06:45:31 +0000
209@@ -494,11 +494,8 @@
210 login name.
211 """
212
213- def getPrincipalByLogin(login, want_password=True):
214- """Return a principal based on his login name.
215-
216- The principal's password is set to None if want_password is False.
217- """
218+ def getPrincipalByLogin(login):
219+ """Return a principal based on his login name."""
220
221
222 class IPlacelessLoginSource(IPrincipalSource):
223@@ -507,15 +504,8 @@
224 between the user id and login name.
225 """
226
227- # want_password is temporary. Eventually we will have accounts
228- # without passwords at all, authenticated via other means such as external
229- # OpenID providers or SSL certificates. Principals having passwords
230- # doesn't really make sense.
231- def getPrincipalByLogin(login, want_password=True):
232- """Return a principal based on his login name.
233-
234- If want_password is False, the principal's password is set to None.
235- """
236+ def getPrincipalByLogin(login):
237+ """Return a principal based on his login name."""
238
239 def getPrincipals(name):
240 """Not implemented.
241
242=== modified file 'lib/lp/services/webapp/tests/test_authutility.py'
243--- lib/lp/services/webapp/tests/test_authutility.py 2012-01-03 12:25:31 +0000
244+++ lib/lp/services/webapp/tests/test_authutility.py 2012-01-18 06:45:31 +0000
245@@ -4,8 +4,8 @@
246 __metaclass__ = type
247
248 import base64
249-import unittest
250
251+import testtools
252 from zope.app.security.basicauthadapter import BasicAuthAdapter
253 from zope.app.security.interfaces import ILoginPassword
254 from zope.app.security.principalregistry import UnauthenticatedPrincipal
255@@ -17,13 +17,13 @@
256 from zope.publisher.interfaces.http import IHTTPCredentials
257
258 from lp.registry.interfaces.person import IPerson
259+from lp.services.config import config
260 from lp.services.identity.interfaces.account import IAccount
261 from lp.services.webapp.authentication import (
262 LaunchpadPrincipal,
263 PlacelessAuthUtility,
264 )
265 from lp.services.webapp.interfaces import (
266- IPasswordEncryptor,
267 IPlacelessAuthUtility,
268 IPlacelessLoginSource,
269 )
270@@ -39,14 +39,14 @@
271 person = DummyPerson()
272
273
274-Bruce = LaunchpadPrincipal(42, 'bruce', 'Bruce', DummyAccount(), 'bruce!')
275+Bruce = LaunchpadPrincipal(42, 'bruce', 'Bruce', DummyAccount())
276 Bruce.person = Bruce.account.person
277
278
279 class DummyPlacelessLoginSource(object):
280 implements(IPlacelessLoginSource)
281
282- def getPrincipalByLogin(self, id, want_password=True):
283+ def getPrincipalByLogin(self, id):
284 return Bruce
285
286 getPrincipal = getPrincipalByLogin
287@@ -55,18 +55,11 @@
288 return [Bruce]
289
290
291-class DummyPasswordEncryptor(object):
292- implements(IPasswordEncryptor)
293-
294- def validate(self, plaintext, encrypted):
295- return plaintext == encrypted
296-
297-
298-class TestPlacelessAuth(PlacelessSetup, unittest.TestCase):
299+class TestPlacelessAuth(PlacelessSetup, testtools.TestCase):
300
301 def setUp(self):
302+ testtools.TestCase.setUp(self)
303 PlacelessSetup.setUp(self)
304- ztapi.provideUtility(IPasswordEncryptor, DummyPasswordEncryptor())
305 ztapi.provideUtility(IPlacelessLoginSource,
306 DummyPlacelessLoginSource())
307 ztapi.provideUtility(IPlacelessAuthUtility, PlacelessAuthUtility())
308@@ -74,10 +67,10 @@
309 IHTTPCredentials, ILoginPassword, BasicAuthAdapter)
310
311 def tearDown(self):
312- ztapi.unprovideUtility(IPasswordEncryptor)
313 ztapi.unprovideUtility(IPlacelessLoginSource)
314 ztapi.unprovideUtility(IPlacelessAuthUtility)
315 PlacelessSetup.tearDown(self)
316+ testtools.TestCase.tearDown(self)
317
318 def _make(self, login, pwd):
319 dict = {
320@@ -87,11 +80,11 @@
321 return getUtility(IPlacelessAuthUtility), request
322
323 def test_authenticate_ok(self):
324- authsvc, request = self._make('bruce', 'bruce!')
325+ authsvc, request = self._make('bruce', 'test')
326 self.assertEqual(authsvc.authenticate(request), Bruce)
327
328 def test_authenticate_notok(self):
329- authsvc, request = self._make('bruce', 'notbruce!')
330+ authsvc, request = self._make('bruce', 'nottest')
331 self.assertEqual(authsvc.authenticate(request), None)
332
333 def test_unauthenticatedPrincipal(self):
334@@ -100,18 +93,52 @@
335 UnauthenticatedPrincipal))
336
337 def test_unauthorized(self):
338- authsvc, request = self._make('bruce', 'bruce!')
339+ authsvc, request = self._make('bruce', 'test')
340 self.assertEqual(authsvc.unauthorized('bruce', request), None)
341 self.assertEqual(request._response._status, 401)
342
343+ def test_basic_auth_disabled(self):
344+ # Basic auth uses a single password for every user, so it must
345+ # never be used on production. authenticate() will skip basic
346+ # auth unless it's enabled.
347+ authsvc, request = self._make('bruce', 'test')
348+ self.assertEqual(authsvc.authenticate(request), Bruce)
349+ try:
350+ config.push(
351+ "no-basic", "[launchpad]\nbasic_auth_password: none")
352+ self.assertEqual(authsvc.authenticate(request), None)
353+ finally:
354+ config.pop("no-basic")
355+
356+ def test_direct_basic_call_fails_when_disabled(self):
357+ # Basic auth uses a single password for every user, so it must
358+ # never be used on production. authenticate() won't call the
359+ # underlying method unless it's enabled, but even if it somehow
360+ # does it will fail.
361+ authsvc, request = self._make('bruce', 'test')
362+ credentials = ILoginPassword(request, None)
363+ self.assertEqual(
364+ authsvc._authenticateUsingBasicAuth(credentials, request), Bruce)
365+ try:
366+ config.push(
367+ "no-basic", "[launchpad]\nbasic_auth_password: none")
368+ exception = self.assertRaises(
369+ AssertionError, authsvc._authenticateUsingBasicAuth,
370+ credentials, request)
371+ self.assertEquals(
372+ "Attempted to use basic auth when it is disabled",
373+ str(exception))
374+ finally:
375+ config.pop("no-basic")
376+
377 def test_getPrincipal(self):
378- authsvc, request = self._make('bruce', 'bruce!')
379+ authsvc, request = self._make('bruce', 'test')
380 self.assertEqual(authsvc.getPrincipal('bruce'), Bruce)
381
382 def test_getPrincipals(self):
383- authsvc, request = self._make('bruce', 'bruce!')
384+ authsvc, request = self._make('bruce', 'test')
385 self.assertEqual(authsvc.getPrincipals('bruce'), [Bruce])
386
387 def test_getPrincipalByLogin(self):
388- authsvc, request = self._make('bruce', 'bruce!')
389+ authsvc, request = self._make('bruce', 'test')
390 self.assertEqual(authsvc.getPrincipalByLogin('bruce'), Bruce)
391
392=== modified file 'lib/lp/testing/pages.py'
393--- lib/lp/testing/pages.py 2012-01-01 02:58:52 +0000
394+++ lib/lp/testing/pages.py 2012-01-18 06:45:31 +0000
395@@ -682,11 +682,8 @@
396 """
397 naked_user = removeSecurityProxy(user)
398 email = naked_user.preferredemail.email
399- if hasattr(naked_user, '_password_cleartext_cached'):
400- password = naked_user._password_cleartext_cached
401 logout()
402- return setupBrowser(
403- auth="Basic %s:%s" % (str(email), password))
404+ return setupBrowser(auth="Basic %s:test" % str(email))
405
406
407 def safe_canonical_url(*args, **kwargs):
408
409=== modified file 'lib/lp/testing/yuixhr.py'
410--- lib/lp/testing/yuixhr.py 2012-01-01 02:58:52 +0000
411+++ lib/lp/testing/yuixhr.py 2012-01-18 06:45:31 +0000
412@@ -111,7 +111,7 @@
413 request = get_current_browser_request()
414 assert request is not None, "We do not have a browser request."
415 authutil = getUtility(IPlacelessAuthUtility)
416- principal = authutil.getPrincipalByLogin(email, want_password=False)
417+ principal = authutil.getPrincipalByLogin(email)
418 launchbag = getUtility(IOpenLaunchBag)
419 launchbag.setLogin(email)
420 logInPrincipal(request, principal, email)