Source code for craft_store.store_client
# -*- Mode:Python; indent-tabs-mode:nil; tab-width:4 -*-
#
# Copyright 2021 Canonical Ltd.
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU Lesser General Public
# License version 3 as published by the Free Software Foundation.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
"""Craft Store StoreClient."""
import base64
import json
from macaroonbakery import bakery, httpbakery # type: ignore[import]
from overrides import overrides
from pymacaroons import Macaroon # type: ignore[import]
from pymacaroons.serializers import json_serializer # type: ignore[import]
from . import creds, endpoints, errors
from .base_client import BaseClient
from .http_client import HTTPClient
def _macaroon_to_json_string(macaroon: Macaroon) -> str:
json_string = macaroon.serialize(json_serializer.JsonSerializer())
if json_string is None:
return ""
return str(json_string)
class WebBrowserWaitingInteractor(httpbakery.WebBrowserInteractor): # type: ignore[misc]
"""WebBrowserInteractor implementation using HTTPClient.
Waiting for a token is implemented using HTTPClient which mounts
a session with backoff retries.
Better exception classes and messages are provided to handle errors.
"""
def __init__(self, user_agent: str) -> None:
super().__init__()
self.user_agent = user_agent
# TODO: transfer implementation to macaroonbakery.
def _wait_for_token(
self,
ctx: str | None, # noqa: ARG002
wait_token_url: str,
) -> httpbakery._interactor.DischargeToken:
request_client = HTTPClient(user_agent=self.user_agent)
resp = request_client.request("GET", wait_token_url)
if resp.status_code != 200:
raise errors.CandidTokenTimeoutError(url=wait_token_url)
json_resp = resp.json()
kind = json_resp.get("kind")
if kind is None:
raise errors.CandidTokenKindError(url=wait_token_url)
token_val = json_resp.get("token")
if token_val is None:
token_val = json_resp.get("token64")
if token_val is None:
raise errors.CandidTokenValueError(url=wait_token_url)
token_val = base64.b64decode(token_val)
return httpbakery._interactor.DischargeToken(kind=kind, value=token_val)
[docs]
class StoreClient(BaseClient):
"""Encapsulates API calls for the Snap Store or Charmhub."""
TOKEN_TYPE: str = "macaroon" # noqa: S105
@overrides
def __init__(
self,
*,
base_url: str,
storage_base_url: str,
endpoints: endpoints.Endpoints,
application_name: str,
user_agent: str,
environment_auth: str | None = None,
ephemeral: bool = False,
file_fallback: bool = False,
) -> None:
super().__init__(
base_url=base_url,
storage_base_url=storage_base_url,
endpoints=endpoints,
application_name=application_name,
user_agent=user_agent,
environment_auth=environment_auth,
ephemeral=ephemeral,
file_fallback=file_fallback,
)
self._bakery_client = httpbakery.Client(
interaction_methods=[WebBrowserWaitingInteractor(user_agent=user_agent)]
)
def _get_authorization_header(self) -> str:
auth = creds.unmarshal_candid_credentials(self._auth.get_credentials())
return f"Macaroon {auth}"
def _candid_discharge(self, macaroon: str) -> str:
bakery_macaroon = bakery.Macaroon.from_dict(json.loads(macaroon))
discharges = bakery.discharge_all(
bakery_macaroon, self._bakery_client.acquire_discharge
)
# serialize macaroons the bakery-way
discharged_macaroons = (
"[" + ",".join(map(_macaroon_to_json_string, discharges)) + "]"
)
return base64.urlsafe_b64encode(discharged_macaroons.encode()).decode("ascii")
def _authorize_token(self, candid_discharged_macaroon: str) -> str:
token_exchange_response = self.http_client.request(
"POST",
self._base_url + self._endpoints.tokens_exchange,
headers={"Macaroons": candid_discharged_macaroon},
json={},
)
return str(token_exchange_response.json()["macaroon"])
def _get_discharged_macaroon( # type: ignore[no-untyped-def]
self, root_macaroon: str, **_kwargs
) -> str:
candid_discharged_macaroon = self._candid_discharge(root_macaroon)
credentials = self._authorize_token(candid_discharged_macaroon)
return creds.marshal_candid_credentials(credentials)