Diff comments:

> 
> === modified file 'lib/lp/code/xmlrpc/git.py'
> --- lib/lp/code/xmlrpc/git.py 2019-04-15 11:42:00 +0000
> +++ lib/lp/code/xmlrpc/git.py 2019-04-15 11:42:00 +0000
> @@ -97,17 +100,34 @@
>              return None
>          macaroon_raw = auth_params.get("macaroon")
>          naked_repository = removeSecurityProxy(repository)
> -        if (macaroon_raw is not None and
> -                self._verifyMacaroon(macaroon_raw, naked_repository)):
> -            # The authentication parameters specifically grant access to
> -            # this repository, so we can bypass other checks.
> -            # For the time being, this only works for code imports.
> -            assert (
> -                naked_repository.repository_type == 
> GitRepositoryType.IMPORTED)
> -            hosting_path = naked_repository.getInternalPath()
> -            writable = True
> -            private = naked_repository.private
> -        else:
> +        writable = None
> +
> +        if macaroon_raw is not None:
> +            if not self._verifyMacaroon(
> +                    macaroon_raw, naked_repository, user=requester):
> +                # Macaroon authentication failed.  Don't fall back to the
> +                # requester's permissions, since macaroons typically have
> +                # additional constraints.  Instead, return "permission
> +                # denied" for visible repositories, and deny the existence
> +                # of invisible ones.
> +                if check_permission("launchpad.View", repository):
> +                    raise faults.PermissionDenied()
> +                else:
> +                    return None

I think the basic problem here was that we were using (what turns into) 403/404 
as authz failures in the first place.  RFC 2616 suggests repeating 401 in the 
case where authz fails, and that seems to make sense, so I've just changed this 
to uniformly return an Unauthorized fault.

> +            # We have a macaroon that grants a subset of the requester's
> +            # permissions.  If the requester is None (the code import case),
> +            # then we ordinarily map that to the anonymous principal, but
> +            # that doesn't have any useful write access; in this case we're
> +            # really starting from a principal with all permissions and
> +            # constraining that, so just bypass other checks and say what we
> +            # mean directly.  If the requester is not None, then fall
> +            # through and do ordinary permission checks for them.
> +            if requester is None:
> +                hosting_path = naked_repository.getInternalPath()
> +                writable = True
> +                private = naked_repository.private

Ah.  In fact, I think this is a security vulnerability, not just a non-obvious 
structure.  If a user has write access to a repository, gets an access token, 
and then their access is revoked, they could still use that token until it 
expires simply by changing to a empty username.  I think it would also allow a 
user to write to any repository that they can view by requesting an access 
token for it and then using an empty username.  Not good.

How about if I were to make verifyMacaroon return either an object 
encapsulating bits of information about the macaroon or None?  That way the 
natural "if not issuer.verifyMacaroon" type of construction would still work, 
but call sites such as this one that need to do more checks could still do so; 
we could then explicitly check that an empty username can only be used with a 
macaroon that isn't bound to a particular user.

Another part of the confusion here is that we're overloading None.  We might 
switch to using a celebrity username instead rather than the empty username 
(perhaps "vcs-imports"), and treat that differently: it would make sense to 
continue to constrain that to imported repositories, we could require an 
appropriate issuer type, and it could be forbidden from doing anything unless 
it presents a valid macaroon.  Having specialised behaviour makes sense for 
code imports, but it would be easier to understand if that specialised 
behaviour were associated with a celebrity rather than with None.

> +
> +        if writable is None:
>              try:
>                  hosting_path = repository.getInternalPath()
>              except Unauthorized:
> @@ -315,12 +336,15 @@
>          getUtility(IGitRefScanJobSource).create(
>              removeSecurityProxy(repository))
>  
> +    @return_fault
>      def authenticateWithPassword(self, username, password):
>          """See `IGitAPI`."""
> -        # XXX cjwatson 2016-10-06: We only support free-floating macaroons
> -        # at the moment, not ones bound to a user.
> -        if not username and self._verifyMacaroon(password):
> -            return {"macaroon": password}
> +        user = getUtility(IPersonSet).getByName(username) if username else 
> None
> +        if self._verifyMacaroon(password, user=user):
> +            params = {"macaroon": password}
> +            if user is not None:
> +                params["uid"] = user.id

Would the discussion above about using the vcs-imports celebrity help?  We 
could then require a user.  It's true that the parameters would still 
technically be subtractive, but it would be much less of a problem.

We could encode the uid and macaroon into a single "principal" parameter, but 
I'm not sure that that actually makes it any clearer.

> +            return params
>          else:
>              # Only macaroons are supported for password authentication.
>              return faults.Unauthorized()


-- 
https://code.launchpad.net/~cjwatson/launchpad/git-honour-access-tokens/+merge/366053
Your team Launchpad code reviewers is requested to review the proposed merge of 
lp:~cjwatson/launchpad/git-honour-access-tokens into lp:launchpad.

_______________________________________________
Mailing list: https://launchpad.net/~launchpad-reviewers
Post to     : [email protected]
Unsubscribe : https://launchpad.net/~launchpad-reviewers
More help   : https://help.launchpad.net/ListHelp

Reply via email to