diff --git a/tests/unit/oidc/test_views.py b/tests/unit/oidc/test_views.py index 2878e138f8a9..8c9ff16d2cc8 100644 --- a/tests/unit/oidc/test_views.py +++ b/tests/unit/oidc/test_views.py @@ -18,11 +18,16 @@ import pytest from tests.common.db.accounts import UserFactory -from tests.common.db.oidc import GitHubPublisherFactory, PendingGitHubPublisherFactory +from tests.common.db.oidc import ( + GitHubPublisherFactory, + GitLabPublisherFactory, + PendingGitHubPublisherFactory, +) from tests.common.db.packaging import ProhibitedProjectFactory, ProjectFactory from warehouse.events.tags import EventTag from warehouse.macaroons import caveats from warehouse.macaroons.interfaces import IMacaroonService +from warehouse.metrics import IMetricsService from warehouse.oidc import errors, views from warehouse.oidc.interfaces import IOIDCPublisherService from warehouse.packaging import services @@ -677,3 +682,99 @@ def test_mint_token_with_invalid_name_fails( assert err["description"] == ( f"The name {pending_publisher.project_name!r} is invalid." ) + + +@pytest.mark.parametrize( + ("claims_in_token", "is_reusable", "is_github"), + [ + ( + { + "ref": "someref", + "sha": "somesha", + "workflow_ref": "org/repo/.github/workflows/parent.yml@someref", + "job_workflow_ref": "org2/repo2/.github/workflows/reusable.yml@v1", + }, + True, + True, + ), + ( + { + "ref": "someref", + "sha": "somesha", + "workflow_ref": "org/repo/.github/workflows/workflow.yml@someref", + "job_workflow_ref": "org/repo/.github/workflows/workflow.yml@someref", + }, + False, + True, + ), + ( + { + "ref": "someref", + "sha": "somesha", + }, + False, + False, + ), + ], +) +def test_mint_token_github_reusable_workflow_metrics( + monkeypatch, + db_request, + claims_in_token, + is_reusable, + is_github, + dummy_github_oidc_jwt, + metrics, +): + time = pretend.stub(time=pretend.call_recorder(lambda: 0)) + monkeypatch.setattr(views, "time", time) + + project = pretend.stub( + id="fakeprojectid", + record_event=pretend.call_recorder(lambda **kw: None), + ) + + publisher = GitHubPublisherFactory() if is_github else GitLabPublisherFactory() + monkeypatch.setattr(publisher.__class__, "projects", [project]) + publisher.publisher_url = pretend.call_recorder(lambda **kw: "https://fake/url") + # NOTE: Can't set __str__ using pretend.stub() + monkeypatch.setattr(publisher.__class__, "__str__", lambda s: "fakespecifier") + + def _find_publisher(claims, pending=False): + if pending: + return None + else: + return publisher + + oidc_service = pretend.stub( + verify_jwt_signature=pretend.call_recorder(lambda token: claims_in_token), + find_publisher=pretend.call_recorder(_find_publisher), + ) + + db_macaroon = pretend.stub(description="fakemacaroon") + macaroon_service = pretend.stub( + create_macaroon=pretend.call_recorder( + lambda *a, **kw: ("raw-macaroon", db_macaroon) + ) + ) + + def find_service(iface, **kw): + if iface == IOIDCPublisherService: + return oidc_service + elif iface == IMacaroonService: + return macaroon_service + elif iface == IMetricsService: + return metrics + assert False, iface + + monkeypatch.setattr(db_request, "find_service", find_service) + monkeypatch.setattr(db_request, "domain", "fakedomain") + + views.mint_token(oidc_service, dummy_github_oidc_jwt, db_request) + + if is_reusable: + assert metrics.increment.calls == [ + pretend.call("warehouse.oidc.mint_token.github_reusable_workflow"), + ] + else: + assert not metrics.increment.calls diff --git a/warehouse/oidc/views.py b/warehouse/oidc/views.py index 46a932ef26d9..cd207090b475 100644 --- a/warehouse/oidc/views.py +++ b/warehouse/oidc/views.py @@ -30,7 +30,7 @@ from warehouse.metrics.interfaces import IMetricsService from warehouse.oidc.errors import InvalidPublisherError from warehouse.oidc.interfaces import IOIDCPublisherService -from warehouse.oidc.models import OIDCPublisher, PendingOIDCPublisher +from warehouse.oidc.models import GitHubPublisher, OIDCPublisher, PendingOIDCPublisher from warehouse.oidc.services import OIDCPublisherService from warehouse.oidc.utils import OIDC_ISSUER_ADMIN_FLAGS, OIDC_ISSUER_SERVICE_NAMES from warehouse.packaging.interfaces import IProjectService @@ -284,4 +284,22 @@ def mint_token( "publisher_url": publisher.publisher_url(), }, ) + + # NOTE: This is for temporary metrics collection of GitHub Trusted Publishers + # that use reusable workflows. Since support for reusable workflows is accidental + # and not correctly implemented, we need to understand how widely it's being + # used before changing its behavior. + # ref: https://github.com/pypi/warehouse/pull/16364 + if isinstance(publisher, GitHubPublisher) and claims: + job_workflow_ref = claims.get("job_workflow_ref") + workflow_ref = claims.get("workflow_ref") + + # When using reusable workflows, `job_workflow_ref` contains the reusable ( + # called) workflow and `workflow_ref` contains the parent (caller) workflow. + # With non-reusable workflows they are the same, so we count reusable + # workflows by checking if they are different. + if job_workflow_ref and workflow_ref and job_workflow_ref != workflow_ref: + metrics = request.find_service(IMetricsService, context=None) + metrics.increment("warehouse.oidc.mint_token.github_reusable_workflow") + return {"success": True, "token": serialized}