diff --git a/api/organisations/usage_reporting/mappers.py b/api/organisations/usage_reporting/mappers.py index 38292a749d70..cb12d567f6b0 100644 --- a/api/organisations/usage_reporting/mappers.py +++ b/api/organisations/usage_reporting/mappers.py @@ -17,10 +17,9 @@ MAX_PROJECT_USAGE_ROWS = 5_000 -def map_signature_to_control_plane_auth_token(signature: str) -> str: - return ( - base64.urlsafe_b64encode(signature.encode("utf-8")).decode("ascii").rstrip("=") - ) +def map_signature_to_control_plane_auth_token(signature_b64: str) -> str: + raw_signature = base64.b64decode(signature_b64) + return base64.urlsafe_b64encode(raw_signature).decode("ascii").rstrip("=") def map_usage_data_to_total_api_calls(usage_data: list[UsageData]) -> int: diff --git a/api/organisations/usage_reporting/services.py b/api/organisations/usage_reporting/services.py index 6474bba2afea..7f5d41369d68 100644 --- a/api/organisations/usage_reporting/services.py +++ b/api/organisations/usage_reporting/services.py @@ -31,12 +31,12 @@ def push_snapshot( *, base_url: str, snapshot: UsageSnapshot, - signature: str, + signature_b64: str, ) -> None: url = f"{base_url.rstrip('/')}{USAGE_ENDPOINT_PATH}" headers = { "Authorization": ( - f"Bearer {map_signature_to_control_plane_auth_token(signature)}" + f"Bearer {map_signature_to_control_plane_auth_token(signature_b64)}" ), "Content-Type": "application/json", } @@ -69,7 +69,7 @@ def push_usage_snapshots() -> None: push_snapshot( base_url=base_url, snapshot=map_organisation_to_usage_snapshot(organisation), - signature=organisation.licence.signature, + signature_b64=organisation.licence.signature, ) except Exception: logger.exception("snapshot.errored") diff --git a/api/tests/unit/organisations/usage_reporting/test_services.py b/api/tests/unit/organisations/usage_reporting/test_services.py index d88a2490eba4..f27cbd1e003b 100644 --- a/api/tests/unit/organisations/usage_reporting/test_services.py +++ b/api/tests/unit/organisations/usage_reporting/test_services.py @@ -91,7 +91,7 @@ def test_push_snapshot__status_code__logs_expected_event( push_snapshot( base_url="https://cp.example.com/", snapshot=snapshot, - signature="sig", + signature_b64="c2ln", ) # Then @@ -105,21 +105,21 @@ def test_push_snapshot__valid_snapshot__sends_bearer_authed_post( # Given mocked_post = mocker.patch("organisations.usage_reporting.services.requests.post") mocked_post.return_value.status_code = 201 - signature = "abc+/=def==" + signature_b64 = "++++ABE=" # When push_snapshot( base_url="https://cp.example.com/", snapshot=snapshot, - signature=signature, + signature_b64=signature_b64, ) # Then mocked_post.assert_called_once() (url,), kwargs = mocked_post.call_args assert url == "https://cp.example.com/v1/public/usage" - # The unpadded base64url token the Control Plane receives on the wire - assert kwargs["headers"]["Authorization"] == "Bearer YWJjKy89ZGVmPT0" + # The signature's raw bytes, unpadded base64url-encoded for the wire. + assert kwargs["headers"]["Authorization"] == "Bearer ----ABE" assert kwargs["headers"]["Content-Type"] == "application/json" assert json.loads(kwargs["data"]) == { "timestamp": "2026-06-18T08:00:00Z", @@ -200,10 +200,10 @@ def test_push_usage_snapshots__licensed_organisations__pushes_each( # Then assert mocked_push.call_count == 2 mocked_push.assert_any_call( - base_url="https://cp.example.com", snapshot=snapshot, signature="sig-1" + base_url="https://cp.example.com", snapshot=snapshot, signature_b64="sig-1" ) mocked_push.assert_any_call( - base_url="https://cp.example.com", snapshot=snapshot, signature="sig-2" + base_url="https://cp.example.com", snapshot=snapshot, signature_b64="sig-2" ) @@ -233,5 +233,5 @@ def test_push_usage_snapshots__one_organisation_raises__continues( # Then - first organisation failed (logged) but second still pushed assert log.has("snapshot.errored", level="error") mocked_push.assert_called_once_with( - base_url="https://cp.example.com", snapshot=snapshot, signature="sig-2" + base_url="https://cp.example.com", snapshot=snapshot, signature_b64="sig-2" )