launchpad-reviewers team mailing list archive
-
launchpad-reviewers team
-
Mailing list archive
-
Message #20781
[Merge] lp:~maxiberta/launchpad/named-auth-tokens-htaccess into lp:launchpad
Maximiliano Bertacchini has proposed merging lp:~maxiberta/launchpad/named-auth-tokens-htaccess into lp:launchpad with lp:~maxiberta/launchpad/named-auth-tokens as a prerequisite.
Commit message:
Add support for named auth tokens in ppa htpasswd creation.
Requested reviews:
Maximiliano Bertacchini (maxiberta)
For more details, see:
https://code.launchpad.net/~maxiberta/launchpad/named-auth-tokens-htaccess/+merge/300002
Add support for named auth tokens in ppa htpasswd creation (including the removal of revoked named tokens).
--
Your team Launchpad code reviewers is subscribed to branch lp:launchpad.
=== modified file 'lib/lp/archivepublisher/htaccess.py'
--- lib/lp/archivepublisher/htaccess.py 2013-06-20 05:50:00 +0000
+++ lib/lp/archivepublisher/htaccess.py 2016-07-13 20:42:37 +0000
@@ -58,43 +58,51 @@
file = open(filename, "a")
try:
- for entry in users:
- user, password, salt = entry
+ for user, password, salt in users:
encrypted = crypt.crypt(password, salt)
file.write("%s:%s\n" % (user, encrypted))
finally:
file.close()
-def htpasswd_credentials_for_archive(archive, tokens=None):
+def htpasswd_credentials_for_archive(archive):
"""Return credentials for an archive for use with write_htpasswd.
:param archive: An `IArchive` (must be private)
- :param tokens: Optional iterable of `IArchiveAuthToken`s.
:return: Iterable of tuples with (user, password, salt) for use with
write_htpasswd.
"""
assert archive.private, "Archive %r must be private" % archive
- if tokens is None:
- tokens = IStore(ArchiveAuthToken).find(
- (ArchiveAuthToken.person_id, ArchiveAuthToken.token),
- ArchiveAuthToken.archive == archive,
- ArchiveAuthToken.date_deactivated == None)
+ tokens = IStore(ArchiveAuthToken).find(
+ (ArchiveAuthToken.person_id, ArchiveAuthToken.name,
+ ArchiveAuthToken.token),
+ ArchiveAuthToken.archive == archive,
+ ArchiveAuthToken.date_deactivated == None)
# We iterate tokens more than once - materialise it.
tokens = list(tokens)
- # The first .htpasswd entry is the buildd_secret.
- yield (BUILDD_USER_NAME, archive.buildd_secret, BUILDD_USER_NAME[:2])
-
+ # Preload map with person ID to person name.
person_ids = map(itemgetter(0), tokens)
names = dict(
IStore(Person).find(
(Person.id, Person.name), Person.id.is_in(set(person_ids))))
- # Combine the token list with the person list, sorting by person ID
- # so the file can be compared later.
- sorted_tokens = [(token[1], names[token[0]]) for token in sorted(tokens)]
- # Iterate over tokens and write the appropriate htpasswd
- # entries for them.
- for token, person_name in sorted_tokens:
- yield (person_name, token, person_name[:2])
+
+ # Format the user field by combining the token list with the person list
+ # (when token has person_id) or prepending a '+' (for named tokens).
+ output = []
+ for person_id, token_name, token in tokens:
+ if token_name:
+ # A named auth token.
+ output.append(('+' + token_name, token, token_name[:2]))
+ else:
+ # A subscription auth token.
+ output.append((names[person_id], token, names[person_id][:2]))
+
+ # The first .htpasswd entry is the buildd_secret.
+ yield (BUILDD_USER_NAME, archive.buildd_secret, BUILDD_USER_NAME[:2])
+
+ # Iterate over tokens and write the appropriate htpasswd entries for them.
+ # Sort by name/person ID so the file can be compared later.
+ for user, password, salt in sorted(output):
+ yield (user, password, salt)
=== modified file 'lib/lp/archivepublisher/scripts/generate_ppa_htaccess.py'
--- lib/lp/archivepublisher/scripts/generate_ppa_htaccess.py 2014-10-29 06:04:09 +0000
+++ lib/lp/archivepublisher/scripts/generate_ppa_htaccess.py 2016-07-13 20:42:37 +0000
@@ -89,8 +89,7 @@
fd, temp_filename = tempfile.mkstemp(dir=pub_config.temproot)
os.close(fd)
- write_htpasswd(
- temp_filename, htpasswd_credentials_for_archive(ppa))
+ write_htpasswd(temp_filename, htpasswd_credentials_for_archive(ppa))
return temp_filename
@@ -176,6 +175,7 @@
store = IStore(ArchiveSubscriber)
valid_tokens = store.find(
ArchiveAuthToken,
+ ArchiveAuthToken.name == None,
ArchiveAuthToken.date_deactivated == None,
ArchiveAuthToken.archive_id == ArchiveSubscriber.archive_id,
ArchiveSubscriber.status == ArchiveSubscriberStatus.CURRENT,
@@ -186,6 +186,7 @@
# all active tokens and valid tokens.
all_active_tokens = store.find(
ArchiveAuthToken,
+ ArchiveAuthToken.name == None,
ArchiveAuthToken.date_deactivated == None)
return all_active_tokens.difference(valid_tokens)
@@ -274,6 +275,22 @@
*extra_expr)
return new_ppa_tokens
+ def getDeactivatedNamedTokens(self, since=None):
+ """Return result set of named tokens deactivated since given time."""
+ now = datetime.now(pytz.UTC)
+
+ store = IStore(ArchiveAuthToken)
+ extra_expr = []
+ if since:
+ extra_expr = [ArchiveAuthToken.date_deactivated >= since]
+ tokens = store.find(
+ ArchiveAuthToken,
+ ArchiveAuthToken.name != None,
+ ArchiveAuthToken.date_deactivated != None,
+ ArchiveAuthToken.date_deactivated <= now,
+ *extra_expr)
+ return tokens
+
def getNewPrivatePPAs(self, since=None):
"""Return the recently created private PPAs."""
store = IStore(Archive)
@@ -294,6 +311,18 @@
last_success = self.getTimeToSyncFrom()
+ # Include ppas with named tokens deactivated since last time we ran.
+ num_tokens = 0
+ for token in self.getDeactivatedNamedTokens(since=last_success):
+ affected_ppas.add(token.archive)
+ num_tokens += 1
+
+ new_ppa_count = len(affected_ppas)
+ self.logger.debug(
+ "%s deactivated named tokens since last run, %s PPAs affected"
+ % (num_tokens, new_ppa_count - current_ppa_count))
+ current_ppa_count = new_ppa_count
+
# In addition to the ppas that are affected by deactivated
# tokens, we also want to include any ppas that have tokens
# created since the last time we ran.
@@ -316,7 +345,7 @@
self.logger.debug('%s PPAs require updating' % new_ppa_count)
for ppa in affected_ppas:
- # If this PPA is blacklisted, do not touch it's htaccess/pwd
+ # If this PPA is blacklisted, do not touch its htaccess/pwd
# files.
blacklisted_ppa_names_for_owner = self.blacklist.get(
ppa.owner.name, [])
=== modified file 'lib/lp/archivepublisher/tests/test_generate_ppa_htaccess.py'
--- lib/lp/archivepublisher/tests/test_generate_ppa_htaccess.py 2016-01-26 15:47:37 +0000
+++ lib/lp/archivepublisher/tests/test_generate_ppa_htaccess.py 2016-07-13 20:42:37 +0000
@@ -32,6 +32,7 @@
from lp.registry.interfaces.person import IPersonSet
from lp.registry.interfaces.teammembership import TeamMembershipStatus
from lp.services.config import config
+from lp.services.features.testing import FeatureFixture
from lp.services.log.logger import BufferLogger
from lp.services.osutils import (
ensure_directory_exists,
@@ -43,6 +44,7 @@
ArchiveStatus,
ArchiveSubscriberStatus,
)
+from lp.soyuz.interfaces.archive import NAMED_AUTH_TOKEN_FEATURE_FLAG
from lp.testing import TestCaseWithFactory
from lp.testing.dbuser import (
lp_dbuser,
@@ -72,6 +74,9 @@
ubuntutest = getUtility(IDistributionSet)['ubuntutest']
self.ppa.distribution = ubuntutest
+ # Enable named auth tokens.
+ self.useFixture(FeatureFixture({NAMED_AUTH_TOKEN_FEATURE_FLAG: u"on"}))
+
def getScript(self, test_args=None):
"""Return a HtaccessTokenGenerator instance."""
if test_args is None:
@@ -260,6 +265,11 @@
all_persons, persons1, persons2, tokens) = data
team1_person = persons1[0]
+ # Named tokens should be ignored for deactivation.
+ self.ppa.newNamedAuthToken(u"tokenname1", as_dict=False)
+ named_token = self.ppa.newNamedAuthToken(u"tokenname2", as_dict=False)
+ named_token.deactivate()
+
# Initially, nothing is eligible for deactivation.
script = self.getScript()
script.deactivateInvalidTokens()
@@ -325,12 +335,9 @@
sub2 = self.ppa.newSubscription(name16, self.ppa.owner)
token1 = self.ppa.newAuthToken(name12)
token2 = self.ppa.newAuthToken(name16)
+ token3 = self.ppa.newNamedAuthToken(u"tokenname3", as_dict=False)
self.layer.txn.commit()
- subs = [sub1]
- subs.append(sub2)
- tokens = [token1]
- tokens.append(token2)
- return subs, tokens
+ return (sub1, sub2), (token1, token2, token3)
def ensureNoFiles(self):
"""Ensure the .ht* files don't already exist."""
@@ -375,6 +382,32 @@
os.remove(htaccess)
os.remove(htpasswd)
+ def testBasicOperation_with_named_tokens(self):
+ """Invoke the actual script and make sure it generates some files."""
+ token1 = self.ppa.newNamedAuthToken(u"tokenname1", as_dict=False)
+ token2 = self.ppa.newNamedAuthToken(u"tokenname2", as_dict=False)
+ token3 = self.ppa.newNamedAuthToken(u"tokenname3", as_dict=False)
+ token3.deactivate()
+
+ # Call the script and check that we have a .htaccess and a .htpasswd.
+ htaccess, htpasswd = self.ensureNoFiles()
+ script = self.getScript()
+ script.main()
+ self.assertThat([htaccess, htpasswd], AllMatch(FileExists()))
+ self.assertIn('+' + token1.name, open(htpasswd).read())
+ self.assertIn('+' + token2.name, open(htpasswd).read())
+ self.assertNotIn('+' + token3.name, open(htpasswd).read())
+
+ # Deactivate a named token and verify it is removed from .htpasswd.
+ token2.deactivate()
+ script.main()
+ self.assertThat([htaccess, htpasswd], AllMatch(FileExists()))
+ self.assertIn('+' + token1.name, open(htpasswd).read())
+ self.assertNotIn('+' + token2.name, open(htpasswd).read())
+ self.assertNotIn('+' + token3.name, open(htpasswd).read())
+ os.remove(htaccess)
+ os.remove(htpasswd)
+
def _setupOptionsData(self):
"""Setup test data for option testing."""
subs, tokens = self.setupDummyTokens()
@@ -598,14 +631,47 @@
script = self.getScript()
self.assertContentEqual(tokens[1:], script.getNewTokens())
+ def test_getDeactivatedNamedTokens_no_previous_run(self):
+ """All deactivated named tokens returned if there is no record
+ of previous run."""
+ last_start = datetime.now(pytz.UTC) - timedelta(seconds=90)
+ before_last_start = last_start - timedelta(seconds=30)
+
+ self.ppa.newNamedAuthToken(u"tokenname1", as_dict=False)
+ token2 = self.ppa.newNamedAuthToken(u"tokenname2", as_dict=False)
+ token2.deactivate()
+ token3 = self.ppa.newNamedAuthToken(u"tokenname3", as_dict=False)
+ token3.date_deactivated = before_last_start
+
+ script = self.getScript()
+ self.assertContentEqual(
+ [token2, token3], script.getDeactivatedNamedTokens())
+
+ def test_getDeactivatedNamedTokens_only_those_since_last_run(self):
+ """Only named tokens deactivated since last run are returned."""
+ last_start = datetime.now(pytz.UTC) - timedelta(seconds=90)
+ before_last_start = last_start - timedelta(seconds=30)
+ tomorrow = datetime.now(pytz.UTC) + timedelta(days=1)
+
+ self.ppa.newNamedAuthToken(u"tokenname1", as_dict=False)
+ token2 = self.ppa.newNamedAuthToken(u"tokenname2", as_dict=False)
+ token2.deactivate()
+ token3 = self.ppa.newNamedAuthToken(u"tokenname3", as_dict=False)
+ token3.date_deactivated = before_last_start
+ token4 = self.ppa.newNamedAuthToken(u"tokenname4", as_dict=False)
+ token4.date_deactivated = tomorrow
+
+ script = self.getScript()
+ self.assertContentEqual(
+ [token2], script.getDeactivatedNamedTokens(last_start))
+
def test_processes_PPAs_without_subscription(self):
# A .htaccess file is written for Private PPAs even if they don't have
# any subscriptions.
htaccess, htpasswd = self.ensureNoFiles()
transaction.commit()
- # Call the script and check that we have a .htaccess and a
- # .htpasswd.
+ # Call the script and check that we have a .htaccess and a .htpasswd.
return_code, stdout, stderr = self.runScript()
self.assertEqual(
return_code, 0, "Got a bad return code of %s\nOutput:\n%s" %
=== modified file 'lib/lp/archivepublisher/tests/test_htaccess.py'
--- lib/lp/archivepublisher/tests/test_htaccess.py 2012-05-31 01:57:07 +0000
+++ lib/lp/archivepublisher/tests/test_htaccess.py 2016-07-13 20:42:37 +0000
@@ -15,6 +15,8 @@
)
from lp.registry.interfaces.distribution import IDistributionSet
from lp.registry.interfaces.person import IPersonSet
+from lp.services.features.testing import FeatureFixture
+from lp.soyuz.interfaces.archive import NAMED_AUTH_TOKEN_FEATURE_FLAG
from lp.testing import TestCaseWithFactory
from lp.testing.layers import LaunchpadZopelessLayer
@@ -36,6 +38,9 @@
ubuntutest = getUtility(IDistributionSet)['ubuntutest']
self.ppa.distribution = ubuntutest
+ # Enable named auth tokens.
+ self.useFixture(FeatureFixture({NAMED_AUTH_TOKEN_FEATURE_FLAG: u"on"}))
+
def test_write_htpasswd(self):
"""Test that writing the .htpasswd file works properly."""
fd, filename = tempfile.mkstemp()
@@ -109,15 +114,20 @@
self.ppa.newSubscription(name12, self.ppa.owner)
self.ppa.newSubscription(name16, self.ppa.owner)
first_created_token = self.ppa.newAuthToken(name16)
- tokens = [
- (token.person_id, token.token)
- for token in [self.ppa.newAuthToken(name12), first_created_token]]
-
- credentials = list(htpasswd_credentials_for_archive(self.ppa, tokens))
-
- self.assertContentEqual(
+ second_created_token = self.ppa.newAuthToken(name12)
+ named_token_20 = self.ppa.newNamedAuthToken(u"name20", as_dict=False)
+ named_token_14 = self.ppa.newNamedAuthToken(u"name14", as_dict=False)
+ named_token_99 = self.ppa.newNamedAuthToken(u"name99", as_dict=False)
+ named_token_99.deactivate()
+
+ credentials = list(htpasswd_credentials_for_archive(self.ppa))
+
+ # Use assertEqual instead of assertContentEqual to verify order.
+ self.assertEqual(
credentials, [
("buildd", "geheim", "bu"),
- ("name12", tokens[0][1], "na"),
- ("name16", tokens[1][1], "na")
+ ("+name14", named_token_14.token, "na"),
+ ("+name20", named_token_20.token, "na"),
+ ("name12", second_created_token.token, "na"),
+ ("name16", first_created_token.token, "na"),
])
=== modified file 'lib/lp/soyuz/model/archive.py'
--- lib/lp/soyuz/model/archive.py 2016-07-13 20:42:37 +0000
+++ lib/lp/soyuz/model/archive.py 2016-07-13 20:42:37 +0000
@@ -1952,7 +1952,7 @@
IStore(ArchiveAuthToken).add(archive_auth_token)
return archive_auth_token
- def newNamedAuthToken(self, name, token=None):
+ def newNamedAuthToken(self, name, token=None, as_dict=True):
"""See `IArchive`."""
if not getFeatureFlag(NAMED_AUTH_TOKEN_FEATURE_FLAG):
@@ -1980,7 +1980,10 @@
archive_auth_token.name = name
archive_auth_token.token = token
IStore(ArchiveAuthToken).add(archive_auth_token)
- return archive_auth_token.asDict()
+ if as_dict:
+ return archive_auth_token.asDict()
+ else:
+ return archive_auth_token
def getNamedAuthToken(self, name):
"""See `IArchive`."""
=== modified file 'lib/lp/soyuz/tests/test_archive.py'
--- lib/lp/soyuz/tests/test_archive.py 2016-07-13 20:42:37 +0000
+++ lib/lp/soyuz/tests/test_archive.py 2016-07-13 20:42:37 +0000
@@ -1320,9 +1320,8 @@
self.private_ppa.newNamedAuthToken, u"tokenname")
def test_newNamedAuthToken_with_custom_secret(self):
- self.private_ppa.newNamedAuthToken(u"tokenname", u"somesecret")
- token = getUtility(IArchiveAuthTokenSet).getActiveNamedTokenForArchive(
- self.private_ppa, u"tokenname")
+ token = self.private_ppa.newNamedAuthToken(
+ u"tokenname", u"somesecret", as_dict=False)
self.assertEqual(u"somesecret", token.token)
def test_getNamedAuthToken_with_no_token(self):
@@ -1336,9 +1335,7 @@
res)
def test_revokeNamedAuthToken_with_token(self):
- self.private_ppa.newNamedAuthToken(u"tokenname")
- token = getUtility(IArchiveAuthTokenSet).getActiveNamedTokenForArchive(
- self.private_ppa, u"tokenname")
+ token = self.private_ppa.newNamedAuthToken(u"tokenname", as_dict=False)
self.private_ppa.revokeNamedAuthToken(u"tokenname")
self.assertIsNotNone(token.date_deactivated)
Follow ups