launchpad-reviewers team mailing list archive
-
launchpad-reviewers team
-
Mailing list archive
-
Message #23504
[Merge] lp:~cjwatson/launchpad/git-issue-access-tokens into lp:launchpad
Colin Watson has proposed merging lp:~cjwatson/launchpad/git-issue-access-tokens into lp:launchpad with lp:~cjwatson/launchpad/refactor-code-import-macaroons as a prerequisite.
Commit message:
Add a macaroon issuer for Git access tokens.
Requested reviews:
Launchpad code reviewers (launchpad-reviewers)
Related bugs:
Bug #1824399 in Launchpad itself: "Add Git HTTPS push tokens for snapcraft experiment"
https://bugs.launchpad.net/launchpad/+bug/1824399
For more details, see:
https://code.launchpad.net/~cjwatson/launchpad/git-issue-access-tokens/+merge/366052
These aren't honoured by the XML-RPC API yet, and there's no way for users to actually request them. Those will come in subsequent branches.
The expiry/persistence policy will likely need to change in future, but the current expiry time is short enough that we should be able to evolve this without too much difficulty.
--
Your team Launchpad code reviewers is requested to review the proposed merge of lp:~cjwatson/launchpad/git-issue-access-tokens into lp:launchpad.
=== modified file 'lib/lp/code/configure.zcml'
--- lib/lp/code/configure.zcml 2018-10-16 15:52:00 +0000
+++ lib/lp/code/configure.zcml 2019-04-15 11:35:18 +0000
@@ -1,4 +1,4 @@
-<!-- Copyright 2009-2018 Canonical Ltd. This software is licensed under the
+<!-- Copyright 2009-2019 Canonical Ltd. This software is licensed under the
GNU Affero General Public License version 3 (see the file LICENSE).
-->
@@ -856,6 +856,15 @@
<allow interface="lp.code.interfaces.gitrepository.IGitRepositorySet" />
</securedutility>
+ <!-- GitRepositoryMacaroonIssuer -->
+
+ <securedutility
+ class="lp.code.model.gitrepository.GitRepositoryMacaroonIssuer"
+ provides="lp.services.macaroons.interfaces.IMacaroonIssuer"
+ name="git-repository">
+ <allow interface="lp.services.macaroons.interfaces.IMacaroonIssuerPublic" />
+ </securedutility>
+
<!-- GitRepositoryDelta -->
<class class="lp.code.adapters.gitrepository.GitRepositoryDelta">
=== modified file 'lib/lp/code/model/codeimportjob.py'
--- lib/lp/code/model/codeimportjob.py 2019-04-15 11:35:16 +0000
+++ lib/lp/code/model/codeimportjob.py 2019-04-15 11:35:18 +0000
@@ -431,7 +431,7 @@
raise ValueError("context.code_import.git_repository is None")
return context.id
- def checkVerificationContext(self, context):
+ def checkVerificationContext(self, context, **kwargs):
"""See `MacaroonIssuerBase`.
For verification, the context may be an `ICodeImportJob`, in which
@@ -454,7 +454,7 @@
raise ValueError("%r is not in the RUNNING state." % context)
return context
- def verifyPrimaryCaveat(self, caveat_value, context):
+ def verifyPrimaryCaveat(self, caveat_value, context, **kwargs):
"""See `MacaroonIssuerBase`."""
if context is None:
# We're only verifying that the macaroon could be valid for some
=== modified file 'lib/lp/code/model/gitrepository.py'
--- lib/lp/code/model/gitrepository.py 2019-04-02 08:12:27 +0000
+++ lib/lp/code/model/gitrepository.py 2019-04-15 11:35:18 +0000
@@ -13,7 +13,10 @@
defaultdict,
OrderedDict,
)
-from datetime import datetime
+from datetime import (
+ datetime,
+ timedelta,
+ )
import email
from fnmatch import fnmatch
from functools import partial
@@ -174,6 +177,7 @@
from lp.services.database.decoratedresultset import DecoratedResultSet
from lp.services.database.enumcol import EnumCol
from lp.services.database.interfaces import IStore
+from lp.services.database.sqlbase import get_transaction_timestamp
from lp.services.database.stormbase import StormBase
from lp.services.database.stormexpr import (
Array,
@@ -182,8 +186,12 @@
BulkUpdate,
Values,
)
+from lp.services.features import getFeatureFlag
+from lp.services.identity.interfaces.account import IAccountSet
from lp.services.job.interfaces.job import JobStatus
from lp.services.job.model.job import Job
+from lp.services.macaroons.interfaces import IMacaroonIssuer
+from lp.services.macaroons.model import MacaroonIssuerBase
from lp.services.mail.notificationrecipientset import NotificationRecipientSet
from lp.services.propertycache import (
cachedproperty,
@@ -1764,6 +1772,98 @@
repository.project_id: repository for repository in repositories}
+@implementer(IMacaroonIssuer)
+class GitRepositoryMacaroonIssuer(MacaroonIssuerBase):
+
+ identifier = "git-repository"
+ allow_multiple = {"lp.expires"}
+
+ _timestamp_format = "%Y-%m-%dT%H:%M:%S.%f"
+
+ def __init__(self):
+ super(GitRepositoryMacaroonIssuer, self).__init__()
+ self.checkers = {
+ "lp.openid-identifier": self.verifyOpenIDIdentifier,
+ "lp.expires": self.verifyExpires,
+ }
+
+ def checkIssuingContext(self, context):
+ """See `MacaroonIssuerBase`.
+
+ For issuing, the context is an `IGitRepository`.
+ """
+ if not IGitRepository.providedBy(context):
+ raise ValueError("Cannot handle context %r." % context)
+ return context.id
+
+ def issueMacaroon(self, context):
+ """See `IMacaroonIssuer`."""
+ user = getUtility(ILaunchBag).user
+ if user is None:
+ raise Unauthorized(
+ "git-repository macaroons may only be issued for a logged-in "
+ "user.")
+ macaroon = super(GitRepositoryMacaroonIssuer, self).issueMacaroon(
+ context)
+ macaroon.add_first_party_caveat(
+ "lp.openid-identifier " +
+ user.account.openid_identifiers.any().identifier)
+ store = IStore(GitRepository)
+ # XXX cjwatson 2019-04-09: Expire macaroons after the number of
+ # seconds given in the code.git.access_token_expiry feature flag,
+ # defaulting to a week. This isn't very flexible, but for now it
+ # saves on having to implement macaroon persistence in order that
+ # users can revoke them.
+ expiry_seconds_str = getFeatureFlag("code.git.access_token_expiry")
+ if expiry_seconds_str is None:
+ expiry_seconds = 60 * 60 * 24 * 7
+ else:
+ expiry_seconds = int(expiry_seconds_str)
+ expiry = (
+ get_transaction_timestamp(store) +
+ timedelta(seconds=expiry_seconds))
+ macaroon.add_first_party_caveat(
+ "lp.expires " + expiry.strftime(self._timestamp_format))
+ return macaroon
+
+ def checkVerificationContext(self, context, **kwargs):
+ """See `MacaroonIssuerBase`.
+
+ For verification, the context is an `IGitRepository`.
+ """
+ if not IGitRepository.providedBy(context):
+ raise ValueError("Cannot handle context %r." % context)
+ return context
+
+ def verifyPrimaryCaveat(self, caveat_value, context, **kwargs):
+ """See `MacaroonIssuerBase`."""
+ if context is None:
+ # We're only verifying that the macaroon could be valid for some
+ # context.
+ return True
+ return caveat_value == str(context.id)
+
+ def verifyOpenIDIdentifier(self, caveat_value, context, **kwargs):
+ """Verify an lp.openid-identifier caveat."""
+ user = kwargs.get("user")
+ try:
+ account = getUtility(IAccountSet).getByOpenIDIdentifier(
+ caveat_value)
+ except LookupError:
+ return False
+ return user == IPerson(account)
+
+ def verifyExpires(self, caveat_value, context, **kwargs):
+ """Verify an lp.expires caveat."""
+ try:
+ expires = datetime.strptime(
+ caveat_value, self._timestamp_format).replace(tzinfo=pytz.UTC)
+ except ValueError:
+ return False
+ store = IStore(GitRepository)
+ return get_transaction_timestamp(store) < expires
+
+
def get_git_repository_privacy_filter(user, repository_class=GitRepository):
public_filter = repository_class.information_type.is_in(
PUBLIC_INFORMATION_TYPES)
=== modified file 'lib/lp/code/model/tests/test_gitrepository.py'
--- lib/lp/code/model/tests/test_gitrepository.py 2019-02-11 12:31:06 +0000
+++ lib/lp/code/model/tests/test_gitrepository.py 2019-04-15 11:35:18 +0000
@@ -17,12 +17,15 @@
import json
from bzrlib import urlutils
+from fixtures import MockPatch
from lazr.lifecycle.event import ObjectModifiedEvent
+from pymacaroons import Macaroon
import pytz
from sqlobject import SQLObjectNotFound
from storm.exceptions import LostObjectError
from storm.store import Store
from testtools.matchers import (
+ AnyMatch,
EndsWith,
Equals,
Is,
@@ -34,6 +37,7 @@
)
import transaction
from zope.component import getUtility
+from zope.publisher.xmlrpc import TestRequest
from zope.security.interfaces import Unauthorized
from zope.security.proxy import removeSecurityProxy
@@ -127,14 +131,22 @@
)
from lp.registry.interfaces.personproduct import IPersonProductFactory
from lp.registry.tests.test_accesspolicy import get_policies_for_artifact
+from lp.services.authserver.xmlrpc import AuthServerAPIView
from lp.services.config import config
from lp.services.database.constants import UTC_NOW
from lp.services.database.interfaces import IStore
from lp.services.database.sqlbase import get_transaction_timestamp
+from lp.services.features.testing import FeatureFixture
from lp.services.job.interfaces.job import JobStatus
from lp.services.job.model.job import Job
from lp.services.job.runner import JobRunner
+from lp.services.macaroons.interfaces import IMacaroonIssuer
+from lp.services.macaroons.testing import (
+ find_caveats_by_name,
+ MacaroonTestMixin,
+ )
from lp.services.mail import stub
+from lp.services.openid.model.openididentifier import OpenIdIdentifier
from lp.services.propertycache import clear_property_cache
from lp.services.utils import seconds_since_epoch
from lp.services.webapp.authorization import check_permission
@@ -163,6 +175,8 @@
HasQueryCount,
)
from lp.testing.pages import webservice_for_person
+from lp.xmlrpc import faults
+from lp.xmlrpc.interfaces import IPrivateApplication
class TestGitRepository(TestCaseWithFactory):
@@ -3894,3 +3908,216 @@
"refs/heads/next": Equals(["push", "force-push"]),
"refs/other": Equals([]),
}))
+
+
+class TestGitRepositoryMacaroonIssuer(MacaroonTestMixin, TestCaseWithFactory):
+ """Test GitRepository macaroon issuing and verification."""
+
+ layer = DatabaseFunctionalLayer
+
+ def setUp(self):
+ super(TestGitRepositoryMacaroonIssuer, self).setUp()
+ self.pushConfig(
+ "launchpad", internal_macaroon_secret_key="some-secret")
+
+ def test_issueMacaroon_refuses_branch(self):
+ branch = self.factory.makeAnyBranch()
+ issuer = getUtility(IMacaroonIssuer, "git-repository")
+ with person_logged_in(branch.owner):
+ self.assertRaises(
+ ValueError, removeSecurityProxy(issuer).issueMacaroon, branch)
+
+ def test_issueMacaroon_good(self):
+ repository = self.factory.makeGitRepository()
+ issuer = getUtility(IMacaroonIssuer, "git-repository")
+ with person_logged_in(repository.owner):
+ identifier = (
+ repository.owner.account.openid_identifiers.any().identifier)
+ macaroon = removeSecurityProxy(issuer).issueMacaroon(repository)
+ now = get_transaction_timestamp(Store.of(repository))
+ expires = now + timedelta(days=7)
+ self.assertThat(macaroon, MatchesStructure(
+ location=Equals(config.vhost.mainsite.hostname),
+ identifier=Equals("git-repository"),
+ caveats=MatchesListwise([
+ MatchesStructure.byEquality(
+ caveat_id="lp.git-repository %s" % repository.id),
+ MatchesStructure.byEquality(
+ caveat_id="lp.openid-identifier %s" % identifier),
+ MatchesStructure.byEquality(
+ caveat_id="lp.expires %s" % (
+ expires.strftime("%Y-%m-%dT%H:%M:%S.%f"))),
+ ])))
+
+ def test_issueMacaroon_expiry_feature_flag(self):
+ self.useFixture(FeatureFixture(
+ {"code.git.access_token_expiry": "3600"}))
+ repository = self.factory.makeGitRepository()
+ issuer = getUtility(IMacaroonIssuer, "git-repository")
+ with person_logged_in(repository.owner):
+ macaroon = removeSecurityProxy(issuer).issueMacaroon(repository)
+ now = get_transaction_timestamp(Store.of(repository))
+ expires = now + timedelta(hours=1)
+ self.assertThat(macaroon, MatchesStructure(
+ caveats=AnyMatch(
+ MatchesStructure.byEquality(
+ caveat_id="lp.expires %s" % (
+ expires.strftime("%Y-%m-%dT%H:%M:%S.%f"))))))
+
+ def test_issueMacaroon_no_user(self):
+ repository = self.factory.makeGitRepository()
+ issuer = getUtility(IMacaroonIssuer, "git-repository")
+ self.assertRaises(
+ Unauthorized,
+ removeSecurityProxy(issuer).issueMacaroon, repository)
+
+ def test_issueMacaroon_not_via_authserver(self):
+ repository = self.factory.makeGitRepository()
+ private_root = getUtility(IPrivateApplication)
+ authserver = AuthServerAPIView(private_root.authserver, TestRequest())
+ self.assertEqual(
+ faults.PermissionDenied(),
+ authserver.issueMacaroon("git-repository", repository))
+
+ def test_verifyMacaroon_good(self):
+ repository = self.factory.makeGitRepository()
+ issuer = getUtility(IMacaroonIssuer, "git-repository")
+ with person_logged_in(repository.owner):
+ macaroon = removeSecurityProxy(issuer).issueMacaroon(repository)
+ self.assertMacaroonVerifies(
+ issuer, macaroon, repository, user=repository.owner)
+
+ def test_verifyMacaroon_wrong_location(self):
+ repository = self.factory.makeGitRepository()
+ issuer = getUtility(IMacaroonIssuer, "git-repository")
+ macaroon = Macaroon(
+ location="another-location",
+ key=removeSecurityProxy(issuer)._root_secret)
+ self.assertMacaroonDoesNotVerify(
+ ["Macaroon has unknown location 'another-location'."],
+ issuer, macaroon, repository, user=repository.owner)
+
+ def test_verifyMacaroon_wrong_key(self):
+ repository = self.factory.makeGitRepository()
+ issuer = getUtility(IMacaroonIssuer, "git-repository")
+ macaroon = Macaroon(
+ location=config.vhost.mainsite.hostname, key="another-secret")
+ self.assertMacaroonDoesNotVerify(
+ ["Signatures do not match."],
+ issuer, macaroon, repository, user=repository.owner)
+
+ def test_verifyMacaroon_wrong_repository(self):
+ repository = self.factory.makeGitRepository()
+ issuer = getUtility(IMacaroonIssuer, "git-repository")
+ with person_logged_in(repository.owner):
+ macaroon = removeSecurityProxy(issuer).issueMacaroon(repository)
+ self.assertMacaroonDoesNotVerify(
+ ["Caveat check for 'lp.git-repository %s' failed." %
+ repository.id],
+ issuer, macaroon, self.factory.makeGitRepository(),
+ user=repository.owner)
+
+ def test_verifyMacaroon_multiple_repository_caveats(self):
+ repository = self.factory.makeGitRepository()
+ issuer = getUtility(IMacaroonIssuer, "git-repository")
+ with person_logged_in(repository.owner):
+ macaroon = removeSecurityProxy(issuer).issueMacaroon(repository)
+ macaroon.add_first_party_caveat("lp.git-repository another")
+ self.assertMacaroonDoesNotVerify(
+ ["Multiple 'lp.git-repository' caveats are not allowed."],
+ issuer, macaroon, repository, user=repository.owner)
+
+ def test_verifyMacaroon_wrong_user(self):
+ repository = self.factory.makeGitRepository()
+ issuer = getUtility(IMacaroonIssuer, "git-repository")
+ with person_logged_in(repository.owner):
+ identifier = (
+ repository.owner.account.openid_identifiers.any().identifier)
+ macaroon = removeSecurityProxy(issuer).issueMacaroon(repository)
+ self.assertMacaroonDoesNotVerify(
+ ["Caveat check for 'lp.openid-identifier %s' failed." %
+ identifier],
+ issuer, macaroon, repository, user=self.factory.makePerson())
+
+ def test_verifyMacaroon_closed_account(self):
+ # A closed account no longer has an OpenID identifier, so the
+ # corresponding caveat doesn't match.
+ repository = self.factory.makeGitRepository()
+ owner = repository.owner
+ issuer = getUtility(IMacaroonIssuer, "git-repository")
+ with person_logged_in(owner):
+ identifier = owner.account.openid_identifiers.any().identifier
+ macaroon = removeSecurityProxy(issuer).issueMacaroon(repository)
+ IStore(OpenIdIdentifier).find(
+ OpenIdIdentifier,
+ OpenIdIdentifier.account_id == owner.account.id).remove()
+ self.assertMacaroonDoesNotVerify(
+ ["Caveat check for 'lp.openid-identifier %s' failed." %
+ identifier],
+ issuer, macaroon, repository, user=owner)
+
+ def test_verifyMacaroon_multiple_openid_identifier_caveats(self):
+ repository = self.factory.makeGitRepository()
+ issuer = getUtility(IMacaroonIssuer, "git-repository")
+ with person_logged_in(repository.owner):
+ macaroon = removeSecurityProxy(issuer).issueMacaroon(repository)
+ macaroon.add_first_party_caveat("lp.openid-identifier another")
+ self.assertMacaroonDoesNotVerify(
+ ["Multiple 'lp.openid-identifier' caveats are not allowed."],
+ issuer, macaroon, repository, user=repository.owner)
+
+ def test_verifyMacaroon_expired(self):
+ repository = self.factory.makeGitRepository()
+ issuer = getUtility(IMacaroonIssuer, "git-repository")
+ with person_logged_in(repository.owner):
+ macaroon = removeSecurityProxy(issuer).issueMacaroon(repository)
+ now = get_transaction_timestamp(Store.of(repository))
+ self.useFixture(MockPatch(
+ "lp.code.model.gitrepository.get_transaction_timestamp",
+ lambda _: now + timedelta(days=8)))
+ self.assertMacaroonDoesNotVerify(
+ ["Caveat check for '%s' failed." %
+ find_caveats_by_name(macaroon, "lp.expires")[0].caveat_id],
+ issuer, macaroon, repository, user=repository.owner)
+
+ def test_verifyMacaroon_multiple_expires_caveats(self):
+ # If somebody attaches another expires caveat to the macaroon,
+ # that's OK; we just take the strictest.
+ repository = self.factory.makeGitRepository()
+ issuer = getUtility(IMacaroonIssuer, "git-repository")
+ with person_logged_in(repository.owner):
+ macaroon1 = removeSecurityProxy(issuer).issueMacaroon(repository)
+ macaroon2 = removeSecurityProxy(issuer).issueMacaroon(repository)
+ now = get_transaction_timestamp(Store.of(repository))
+ expires1 = now + timedelta(days=1)
+ expires2 = now + timedelta(days=14)
+ macaroon1.add_first_party_caveat(
+ "lp.expires " + expires1.strftime("%Y-%m-%dT%H:%M:%S.%f"))
+ macaroon2.add_first_party_caveat(
+ "lp.expires " + expires2.strftime("%Y-%m-%dT%H:%M:%S.%f"))
+ self.assertMacaroonVerifies(
+ issuer, macaroon1, repository, user=repository.owner)
+ self.assertMacaroonVerifies(
+ issuer, macaroon2, repository, user=repository.owner)
+ with MockPatch(
+ "lp.code.model.gitrepository.get_transaction_timestamp",
+ lambda _: now + timedelta(days=4)):
+ self.assertMacaroonDoesNotVerify(
+ ["Caveat check for '%s' failed." %
+ find_caveats_by_name(macaroon1, "lp.expires")[1].caveat_id],
+ issuer, macaroon1, repository, user=repository.owner)
+ self.assertMacaroonVerifies(
+ issuer, macaroon2, repository, user=repository.owner)
+ with MockPatch(
+ "lp.code.model.gitrepository.get_transaction_timestamp",
+ lambda _: now + timedelta(days=8)):
+ self.assertMacaroonDoesNotVerify(
+ ["Caveat check for '%s' failed." %
+ find_caveats_by_name(macaroon1, "lp.expires")[0].caveat_id,
+ "Caveat check for '%s' failed." %
+ find_caveats_by_name(macaroon1, "lp.expires")[1].caveat_id],
+ issuer, macaroon1, repository, user=repository.owner)
+ self.assertMacaroonDoesNotVerify(
+ ["Caveat check for '%s' failed." %
+ find_caveats_by_name(macaroon2, "lp.expires")[0].caveat_id],
+ issuer, macaroon2, repository, user=repository.owner)
=== modified file 'lib/lp/services/authserver/tests/test_authserver.py'
--- lib/lp/services/authserver/tests/test_authserver.py 2019-04-15 11:35:16 +0000
+++ lib/lp/services/authserver/tests/test_authserver.py 2019-04-15 11:35:18 +0000
@@ -88,7 +88,7 @@
raise ValueError("Cannot handle context %r." % context)
return context
- def verifyPrimaryCaveat(self, caveat_value, context):
+ def verifyPrimaryCaveat(self, caveat_value, context, **kwargs):
"""See `MacaroonIssuerBase`."""
return caveat_value == str(context)
=== modified file 'lib/lp/services/macaroons/interfaces.py'
--- lib/lp/services/macaroons/interfaces.py 2019-04-15 11:35:16 +0000
+++ lib/lp/services/macaroons/interfaces.py 2019-04-15 11:35:18 +0000
@@ -20,7 +20,7 @@
issuable_via_authserver = Bool(
"Does this issuer allow issuing macaroons via the authserver?")
- def verifyMacaroon(macaroon, context, require_context=True):
+ def verifyMacaroon(macaroon, context, require_context=True, **kwargs):
"""Verify that `macaroon` is valid for `context`.
:param macaroon: A `Macaroon`.
@@ -30,6 +30,8 @@
verify that the macaroon could be valid for some context. Use
this in the authentication part of an
authentication/authorisation API.
+ :param kwargs: Additional arguments that issuers may require to
+ verify a macaroon.
:return: True if `macaroon` is valid for `context`, otherwise False.
"""
=== modified file 'lib/lp/services/macaroons/model.py'
--- lib/lp/services/macaroons/model.py 2019-04-15 11:35:16 +0000
+++ lib/lp/services/macaroons/model.py 2019-04-15 11:35:18 +0000
@@ -25,6 +25,31 @@
issuable_via_authserver = False
+ # A mapping of caveat names to "checker" callables that verify the
+ # corresponding caveat text. The signature of each checker is
+ # (caveat_value, context, **kwargs) -> bool, where caveat_value is the
+ # text of the caveat with the caveat name removed, context is the
+ # issuer-specific context to check, and kwargs is any other keyword
+ # arguments that were given to verifyMacaroon; it should return True if
+ # the caveat is allowed, otherwise False.
+ #
+ # The context passed in may be None, in which case the checker may
+ # choose to only verify that the caveat could be valid for some context,
+ # or may simply return False if this is unsupported. This is useful for
+ # issuers that support APIs with separate authentication and
+ # authorisation phases.
+ #
+ # The "primary context caveat" added to all macaroons issued by this
+ # base class does not need to be listed here; it is handled by the
+ # verifyContextCaveat method.
+ checkers = {}
+
+ # Caveat names in this set may appear more than once (in which case they
+ # have the usual subtractive semantics, so the union of all the
+ # constraints they express applies). Any other caveats may only appear
+ # once.
+ allow_multiple = set()
+
@property
def identifier(self):
"""An identifying name for this issuer."""
@@ -58,11 +83,7 @@
return context
def issueMacaroon(self, context):
- """See `IMacaroonIssuer`.
-
- Concrete implementations should normally wrap this with some
- additional checks of and/or changes to the context.
- """
+ """See `IMacaroonIssuer`."""
context = self.checkIssuingContext(context)
macaroon = Macaroon(
location=config.vhost.mainsite.hostname,
@@ -71,7 +92,7 @@
"%s %s" % (self.primary_caveat_name, context))
return macaroon
- def checkVerificationContext(self, context):
+ def checkVerificationContext(self, context, **kwargs):
"""Check that the verification context is suitable.
Concrete implementations may implement this method to check that the
@@ -80,22 +101,27 @@
context that was passed in or an adapted one.
:param context: The context to check.
+ :param kwargs: Additional arguments that issuers may require to
+ verify a macaroon.
:raises ValueError: if the context is unsuitable.
:return: The context to pass to individual caveat checkers.
"""
return context
- def verifyPrimaryCaveat(self, caveat_value, context):
+ def verifyPrimaryCaveat(self, caveat_value, context, **kwargs):
"""Verify the primary context caveat on one of this issuer's macaroons.
- :param caveat_value: The text of the caveat, with this issuer's
- prefix removed.
+ :param caveat_value: The text of the caveat with the caveat name
+ removed.
:param context: The context to check.
+ :param kwargs: Additional arguments that issuers may require to
+ verify a macaroon.
:return: True if this caveat is allowed, otherwise False.
"""
raise NotImplementedError
- def verifyMacaroon(self, macaroon, context, require_context=True):
+ def verifyMacaroon(self, macaroon, context, require_context=True,
+ **kwargs):
"""See `IMacaroonIssuer`."""
if macaroon.location != config.vhost.mainsite.hostname:
log.info("Macaroon has unknown location '%s'." % macaroon.location)
@@ -109,6 +135,7 @@
except ValueError as e:
log.info(str(e))
return False
+ seen = set()
# XXX cjwatson 2019-04-11: Once we're on Python 3, we should use
# "nonlocal" instead of this hack.
@@ -124,16 +151,21 @@
log.info("Cannot parse caveat '%s'." % caveat)
state.logged_caveat_error = True
return False
+ if caveat_name not in self.allow_multiple and caveat_name in seen:
+ log.info(
+ "Multiple '%s' caveats are not allowed." % caveat_name)
+ state.logged_caveat_error = True
+ return False
+ seen.add(caveat_name)
if caveat_name == self.primary_caveat_name:
checker = self.verifyPrimaryCaveat
else:
- # XXX cjwatson 2019-04-09: For now we just fail closed if
- # there are any other caveats, which is good enough for
- # internal use.
- log.info("Unhandled caveat name '%s'." % caveat_name)
- state.logged_caveat_error = True
- return False
- if not checker(caveat_value, context):
+ checker = self.checkers.get(caveat_name)
+ if checker is None:
+ log.info("Unhandled caveat name '%s'." % caveat_name)
+ state.logged_caveat_error = True
+ return False
+ if not checker(caveat_value, context, **kwargs):
log.info("Caveat check for '%s' failed." % caveat)
state.logged_caveat_error = True
return False
=== modified file 'lib/lp/snappy/model/snapbuild.py'
--- lib/lp/snappy/model/snapbuild.py 2019-04-15 11:35:16 +0000
+++ lib/lp/snappy/model/snapbuild.py 2019-04-15 11:35:18 +0000
@@ -611,13 +611,13 @@
raise ValueError("Refusing to issue macaroon for public build.")
return removeSecurityProxy(context).id
- def checkVerificationContext(self, context):
+ def checkVerificationContext(self, context, **kwargs):
"""See `MacaroonIssuerBase`."""
if not IGitRepository.providedBy(context):
raise ValueError("Cannot handle context %r." % context)
return context
- def verifyPrimaryCaveat(self, caveat_value, context):
+ def verifyPrimaryCaveat(self, caveat_value, context, **kwargs):
"""See `MacaroonIssuerBase`.
For verification, the context is an `IGitRepository`. We check that
=== modified file 'lib/lp/soyuz/model/binarypackagebuild.py'
--- lib/lp/soyuz/model/binarypackagebuild.py 2019-04-15 11:35:16 +0000
+++ lib/lp/soyuz/model/binarypackagebuild.py 2019-04-15 11:35:18 +0000
@@ -1383,13 +1383,13 @@
raise ValueError("Refusing to issue macaroon for public build.")
return removeSecurityProxy(context).id
- def checkVerificationContext(self, context):
+ def checkVerificationContext(self, context, **kwargs):
"""See `MacaroonIssuerBase`."""
if not isinstance(context, int):
raise ValueError("Cannot handle context %r." % context)
return context
- def verifyPrimaryCaveat(self, caveat_value, context):
+ def verifyPrimaryCaveat(self, caveat_value, context, **kwargs):
"""See `MacaroonIssuerBase`.
For verification, the context is a `LibraryFileAlias` ID. We check
Follow ups