diff --git a/tests/conftest.py b/tests/conftest.py index 204daa776c18..c7a595615808 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -10,7 +10,6 @@ # See the License for the specific language governing permissions and # limitations under the License. -import json import os import os.path import re @@ -51,7 +50,7 @@ from warehouse.metrics import IMetricsService from warehouse.oidc import services as oidc_services from warehouse.oidc.interfaces import IOIDCPublisherService -from warehouse.oidc.utils import GITHUB_OIDC_ISSUER_URL +from warehouse.oidc.utils import ACTIVESTATE_OIDC_ISSUER_URL, GITHUB_OIDC_ISSUER_URL from warehouse.organizations import services as organization_services from warehouse.organizations.interfaces import IOrganizationService from warehouse.packaging import services as packaging_services @@ -169,7 +168,8 @@ def pyramid_services( token_service, user_service, project_service, - oidc_service, + github_oidc_service, + activestate_oidc_service, macaroon_service, ): services = _Services() @@ -184,7 +184,12 @@ def pyramid_services( services.register_service(token_service, ITokenService, None, name="email") services.register_service(user_service, IUserService, None, name="") services.register_service(project_service, IProjectService, None, name="") - services.register_service(oidc_service, IOIDCPublisherService, None, name="github") + services.register_service( + github_oidc_service, IOIDCPublisherService, None, name="github" + ) + services.register_service( + activestate_oidc_service, IOIDCPublisherService, None, name="activestate" + ) services.register_service(macaroon_service, IMacaroonService, None, name="") return services @@ -368,7 +373,7 @@ def project_service(db_session, metrics, ratelimiters=None): @pytest.fixture -def oidc_service(db_session): +def github_oidc_service(db_session): # We pretend to be a verifier for GitHub OIDC JWTs, for the purposes of testing. return oidc_services.NullOIDCPublisherService( db_session, @@ -381,7 +386,7 @@ def oidc_service(db_session): @pytest.fixture -def dummy_oidc_jwt(): +def dummy_github_oidc_jwt(): # { # "jti": "6e67b1cb-2b8d-4be5-91cb-757edb2ec970", # "sub": "repo:foo/bar", @@ -427,8 +432,55 @@ def dummy_oidc_jwt(): @pytest.fixture -def dummy_oidc_payload(dummy_oidc_jwt): - return json.dumps({"token": dummy_oidc_jwt}) +def dummy_activestate_oidc_jwt(): + # { + # "jti": "6e67b1cb-2b8d-4be5-91cb-757edb2ec970", + # "sub": "org:fakeorg:project:fakeproject", + # "aud": "pypi", + # "actor_id": "fake", + # "actor": "foo", + # "oraganization_id": "7e67b1cb-2b8d-4be5-91cb-757edb2ec970", + # "organization": "fakeorg", + # "project_visibility": "private", + # "project_id": "8e67b1cb-2b8d-4be5-91cb-757edb2ec970", + # "project_path": "fakeorg/fakeproject", + # "project": "fakeproject", + # "builder": "pypi_builder", + # "ingredient_name": "fakeingredient", + # "artifact_id": "9e67b1cb-2b8d-4be5-91cb-757edb2ec970", + # "iss":"https://platform.activestate.com/api/v1/oauth/oidc", + # "nbf": 1650663265, + # "exp": 1650664165, + # "iat": 1650663865 + # } + return ( + "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJqdGkiOiI2ZTY3YjFjYi0yYjhkLTRi" + "ZTUtOTFjYi03NTdlZGIyZWM5NzAiLCJzdWIiOiJvcmc6ZmFrZW9yZzpwcm9qZWN0OmZha" + "2Vwcm9qZWN0IiwiYXVkIjoicHlwaSIsImFjdG9yX2lkIjoiZmFrZSIsImFjdG9yIjoiZm" + "9vIiwib3JhZ2FuaXphdGlvbl9pZCI6IjdlNjdiMWNiLTJiOGQtNGJlNS05MWNiLTc1N2V" + "kYjJlYzk3MCIsIm9yZ2FuaXphdGlvbiI6ImZha2VvcmciLCJwcm9qZWN0X3Zpc2liaWxp" + "dHkiOiJwcml2YXRlIiwicHJvamVjdF9pZCI6IjhlNjdiMWNiLTJiOGQtNGJlNS05MWNiL" + "Tc1N2VkYjJlYzk3MCIsInByb2plY3RfcGF0aCI6ImZha2VvcmcvZmFrZXByb2plY3QiLC" + "Jwcm9qZWN0IjoiZmFrZXByb2plY3QiLCJidWlsZGVyIjoicHlwaV9idWlsZGVyIiwiaW5" + "ncmVkaWVudF9uYW1lIjoiZmFrZWluZ3JlZGllbnQiLCJhcnRpZmFjdF9pZCI6IjllNjdi" + "MWNiLTJiOGQtNGJlNS05MWNiLTc1N2VkYjJlYzk3MCIsImlzcyI6Imh0dHBzOi8vcGxhd" + "GZvcm0uYWN0aXZlc3RhdGUuY29tL2FwaS92MS9vYXV0aC9vaWRjIiwibmJmIjoxNjUwNj" + "YzMjY1LCJleHAiOjE2NTA2NjQxNjUsImlhdCI6MTY1MDY2Mzg2NX0.R4q-vWAFXHrBSBK" + "AZuHHIsGOkqlirPxEtLfjLIDiLr0" + ) + + +@pytest.fixture +def activestate_oidc_service(db_session): + # We pretend to be a verifier for GitHub OIDC JWTs, for the purposes of testing. + return oidc_services.NullOIDCPublisherService( + db_session, + pretend.stub(), + ACTIVESTATE_OIDC_ISSUER_URL, + pretend.stub(), + pretend.stub(), + pretend.stub(), + ) @pytest.fixture diff --git a/tests/unit/oidc/test_services.py b/tests/unit/oidc/test_services.py index 452c94188b65..9b03cd94a1cf 100644 --- a/tests/unit/oidc/test_services.py +++ b/tests/unit/oidc/test_services.py @@ -848,7 +848,7 @@ def test_find_publisher(self, monkeypatch): assert service.find_publisher(claims) == publisher - def test_find_publisher_full_pending(self, oidc_service): + def test_find_publisher_full_pending(self, github_oidc_service): pending_publisher = PendingGitHubPublisherFactory.create( project_name="does-not-exist", repository_name="bar", @@ -886,10 +886,12 @@ def test_find_publisher_full_pending(self, oidc_service): "iat": 1650663865, } - expected_pending_publisher = oidc_service.find_publisher(claims, pending=True) + expected_pending_publisher = github_oidc_service.find_publisher( + claims, pending=True + ) assert expected_pending_publisher == pending_publisher - def test_find_publisher_full(self, oidc_service): + def test_find_publisher_full(self, github_oidc_service): publisher = GitHubPublisherFactory.create( repository_name="bar", repository_owner="foo", @@ -926,7 +928,7 @@ def test_find_publisher_full(self, oidc_service): "iat": 1650663865, } - expected_publisher = oidc_service.find_publisher(claims, pending=False) + expected_publisher = github_oidc_service.find_publisher(claims, pending=False) assert expected_publisher == publisher def test_reify_publisher(self): diff --git a/tests/unit/oidc/test_utils.py b/tests/unit/oidc/test_utils.py index bfaafbe876ba..7dbc628ffc75 100644 --- a/tests/unit/oidc/test_utils.py +++ b/tests/unit/oidc/test_utils.py @@ -113,7 +113,7 @@ def test_find_publisher_by_issuer_google(db_request, sub, expected_id): @pytest.mark.parametrize( - "expected_id, sub, organization, project, actor_id, actor", + "expected_id, sub, organization, project, actor_id, actor, ingredient_name", [ ( uuid.UUID("aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa"), @@ -122,6 +122,7 @@ def test_find_publisher_by_issuer_google(db_request, sub, expected_id): "fakeproject1", "00000000-1000-8000-0000-000000000003", "fakeuser1", + "fakeingredient1", ), ( uuid.UUID("bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb"), @@ -130,6 +131,7 @@ def test_find_publisher_by_issuer_google(db_request, sub, expected_id): "fakeproject2", "00000000-1000-8000-0000-000000000006", "fakeuser2", + "fakeingredient2", ), ( uuid.UUID("cccccccc-cccc-cccc-cccc-cccccccccccc"), @@ -138,6 +140,7 @@ def test_find_publisher_by_issuer_google(db_request, sub, expected_id): "fakeproject3", "00000000-1000-8000-0000-000000000009", "fakeuser3", + "fakeingredient3", ), ], ) @@ -149,6 +152,7 @@ def test_find_publisher_by_issuer_activestate( project: str, actor_id: str, actor: str, + ingredient_name: str, ): ActiveStatePublisherFactory( id="aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa", @@ -156,6 +160,7 @@ def test_find_publisher_by_issuer_activestate( activestate_project_name="fakeproject1", actor_id="00000000-1000-8000-0000-000000000003", actor="fakeuser1", + ingredient="fakeingredient1", ) ActiveStatePublisherFactory( id="bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb", @@ -163,6 +168,7 @@ def test_find_publisher_by_issuer_activestate( activestate_project_name="fakeproject2", actor_id="00000000-1000-8000-0000-000000000006", actor="fakeuser2", + ingredient="fakeingredient2", ) ActiveStatePublisherFactory( id="cccccccc-cccc-cccc-cccc-cccccccccccc", @@ -170,6 +176,7 @@ def test_find_publisher_by_issuer_activestate( activestate_project_name="fakeproject3", actor_id="00000000-1000-8000-0000-000000000009", actor="fakeuser3", + ingredient="fakeingredient3", ) signed_claims = { @@ -178,6 +185,7 @@ def test_find_publisher_by_issuer_activestate( "project": project, "actor_id": actor_id, "actor": actor, + "ingredient_name": ingredient_name, } assert ( diff --git a/tests/unit/oidc/test_views.py b/tests/unit/oidc/test_views.py index 578607df1efa..ac30d2527f15 100644 --- a/tests/unit/oidc/test_views.py +++ b/tests/unit/oidc/test_views.py @@ -70,9 +70,17 @@ def test_oidc_audience(): assert response == {"audience": "fakeaudience"} -def test_mint_token_from_oidc_not_enabled(dummy_oidc_payload): +@pytest.mark.parametrize( + "token_fixture_name,service_name", + [ + ("dummy_github_oidc_jwt", "github"), + ("dummy_activestate_oidc_jwt", "activestate"), + ], +) +def test_mint_token_from_oidc_not_enabled(token_fixture_name, service_name, request): + token = request.getfixturevalue(token_fixture_name) request = pretend.stub( - body=dummy_oidc_payload, + body=json.dumps({"token": token}), response=pretend.stub(status=None), flags=pretend.stub(disallow_oidc=lambda *a: True), ) @@ -84,7 +92,7 @@ def test_mint_token_from_oidc_not_enabled(dummy_oidc_payload): "errors": [ { "code": "not-enabled", - "description": "github trusted publishing functionality not enabled", + "description": f"{service_name} trusted publishing functionality not enabled", # noqa } ], } @@ -171,7 +179,7 @@ def find_service(self, *a, **kw): def test_mint_token_from_oidc_jwt_decode_leaky_exception( - monkeypatch, dummy_oidc_payload + monkeypatch, dummy_github_oidc_jwt: str ): class Request: def __init__(self): @@ -180,7 +188,7 @@ def __init__(self): @property def body(self): - return dummy_oidc_payload + return json.dumps({"token": dummy_github_oidc_jwt}) def find_service(self, *a, **kw): return pretend.stub(increment=pretend.call_recorder(lambda s: None)) @@ -278,7 +286,9 @@ def test_mint_token_from_oidc_creates_expected_service( assert mint_token.calls == [pretend.call(oidc_service, token, request)] -def test_mint_token_from_trusted_publisher_verify_jwt_signature_fails(dummy_oidc_jwt): +def test_mint_token_from_trusted_publisher_verify_jwt_signature_fails( + dummy_github_oidc_jwt, +): oidc_service = pretend.stub( verify_jwt_signature=pretend.call_recorder(lambda token: None), ) @@ -288,7 +298,7 @@ def test_mint_token_from_trusted_publisher_verify_jwt_signature_fails(dummy_oidc flags=pretend.stub(disallow_oidc=lambda *a: False), ) - response = views.mint_token(oidc_service, dummy_oidc_jwt, request) + response = views.mint_token(oidc_service, dummy_github_oidc_jwt, request) assert request.response.status == 422 assert response == { "message": "Token request failed", @@ -300,10 +310,12 @@ def test_mint_token_from_trusted_publisher_verify_jwt_signature_fails(dummy_oidc ], } - assert oidc_service.verify_jwt_signature.calls == [pretend.call(dummy_oidc_jwt)] + assert oidc_service.verify_jwt_signature.calls == [ + pretend.call(dummy_github_oidc_jwt) + ] -def test_mint_token_trusted_publisher_lookup_fails(dummy_oidc_jwt): +def test_mint_token_trusted_publisher_lookup_fails(dummy_github_oidc_jwt): claims = pretend.stub() message = "some message" oidc_service = pretend.stub( @@ -318,7 +330,7 @@ def test_mint_token_trusted_publisher_lookup_fails(dummy_oidc_jwt): flags=pretend.stub(disallow_oidc=lambda *a: False), ) - response = views.mint_token(oidc_service, dummy_oidc_jwt, request) + response = views.mint_token(oidc_service, dummy_github_oidc_jwt, request) assert request.response.status == 422 assert response == { "message": "Token request failed", @@ -332,7 +344,9 @@ def test_mint_token_trusted_publisher_lookup_fails(dummy_oidc_jwt): ], } - assert oidc_service.verify_jwt_signature.calls == [pretend.call(dummy_oidc_jwt)] + assert oidc_service.verify_jwt_signature.calls == [ + pretend.call(dummy_github_oidc_jwt) + ] assert oidc_service.find_publisher.calls == [ pretend.call(claims, pending=True), pretend.call(claims, pending=False), @@ -340,7 +354,7 @@ def test_mint_token_trusted_publisher_lookup_fails(dummy_oidc_jwt): def test_mint_token_pending_publisher_project_already_exists( - db_request, dummy_oidc_jwt + db_request, dummy_github_oidc_jwt ): project = ProjectFactory.create() pending_publisher = PendingGitHubPublisherFactory.create( @@ -358,7 +372,7 @@ def test_mint_token_pending_publisher_project_already_exists( ) db_request.find_service = pretend.call_recorder(lambda *a, **kw: oidc_service) - resp = views.mint_token(oidc_service, dummy_oidc_jwt, db_request) + resp = views.mint_token(oidc_service, dummy_github_oidc_jwt, db_request) assert db_request.response.status_code == 422 assert resp == { "message": "Token request failed", @@ -370,14 +384,16 @@ def test_mint_token_pending_publisher_project_already_exists( ], } - assert oidc_service.verify_jwt_signature.calls == [pretend.call(dummy_oidc_jwt)] + assert oidc_service.verify_jwt_signature.calls == [ + pretend.call(dummy_github_oidc_jwt) + ] assert oidc_service.find_publisher.calls == [pretend.call(claims, pending=True)] def test_mint_token_from_oidc_pending_publisher_ok( monkeypatch, db_request, - dummy_oidc_payload, + dummy_github_oidc_jwt, ): user = UserFactory.create() pending_publisher = PendingGitHubPublisherFactory.create( @@ -391,7 +407,7 @@ def test_mint_token_from_oidc_pending_publisher_ok( ) db_request.flags.disallow_oidc = lambda f=None: False - db_request.body = dummy_oidc_payload + db_request.body = json.dumps({"token": dummy_github_oidc_jwt}) db_request.remote_addr = "0.0.0.0" ratelimiter = pretend.stub(clear=pretend.call_recorder(lambda id: None)) @@ -412,7 +428,7 @@ def test_mint_token_from_oidc_pending_publisher_ok( def test_mint_token_from_pending_trusted_publisher_invalidates_others( - monkeypatch, db_request, dummy_oidc_payload + monkeypatch, db_request, dummy_github_oidc_jwt ): time = pretend.stub(time=pretend.call_recorder(lambda: 0)) monkeypatch.setattr(views, "time", time) @@ -450,7 +466,7 @@ def test_mint_token_from_pending_trusted_publisher_invalidates_others( ) db_request.flags.oidc_enabled = lambda f: False - db_request.body = dummy_oidc_payload + db_request.body = json.dumps({"token": dummy_github_oidc_jwt}) db_request.remote_addr = "0.0.0.0" ratelimiter = pretend.stub(clear=pretend.call_recorder(lambda id: None)) @@ -487,7 +503,7 @@ def test_mint_token_from_pending_trusted_publisher_invalidates_others( ], ) def test_mint_token_no_pending_publisher_ok( - monkeypatch, db_request, claims_in_token, claims_input, dummy_oidc_jwt + monkeypatch, db_request, claims_in_token, claims_input, dummy_github_oidc_jwt ): time = pretend.stub(time=pretend.call_recorder(lambda: 0)) monkeypatch.setattr(views, "time", time) @@ -531,13 +547,15 @@ def find_service(iface, **kw): monkeypatch.setattr(db_request, "find_service", find_service) monkeypatch.setattr(db_request, "domain", "fakedomain") - response = views.mint_token(oidc_service, dummy_oidc_jwt, db_request) + response = views.mint_token(oidc_service, dummy_github_oidc_jwt, db_request) assert response == { "success": True, "token": "raw-macaroon", } - assert oidc_service.verify_jwt_signature.calls == [pretend.call(dummy_oidc_jwt)] + assert oidc_service.verify_jwt_signature.calls == [ + pretend.call(dummy_github_oidc_jwt) + ] assert oidc_service.find_publisher.calls == [ pretend.call(claims_in_token, pending=True), pretend.call(claims_in_token, pending=False), diff --git a/warehouse/oidc/__init__.py b/warehouse/oidc/__init__.py index eb979ca2e95b..91f15025e497 100644 --- a/warehouse/oidc/__init__.py +++ b/warehouse/oidc/__init__.py @@ -15,7 +15,11 @@ from warehouse.oidc.interfaces import IOIDCPublisherService from warehouse.oidc.services import OIDCPublisherServiceFactory from warehouse.oidc.tasks import compute_oidc_metrics -from warehouse.oidc.utils import GITHUB_OIDC_ISSUER_URL, GOOGLE_OIDC_ISSUER_URL +from warehouse.oidc.utils import ( + ACTIVESTATE_OIDC_ISSUER_URL, + GITHUB_OIDC_ISSUER_URL, + GOOGLE_OIDC_ISSUER_URL, +) def includeme(config): @@ -45,7 +49,7 @@ def includeme(config): config.register_service_factory( OIDCPublisherServiceFactory( publisher="activestate", - issuer_url=GOOGLE_OIDC_ISSUER_URL, + issuer_url=ACTIVESTATE_OIDC_ISSUER_URL, service_class=oidc_publisher_service_class, ), IOIDCPublisherService, diff --git a/warehouse/oidc/utils.py b/warehouse/oidc/utils.py index 8c71afceb637..690203faabea 100644 --- a/warehouse/oidc/utils.py +++ b/warehouse/oidc/utils.py @@ -64,7 +64,9 @@ } -def find_publisher_by_issuer(session, issuer_url, signed_claims, *, pending=False): +def find_publisher_by_issuer( + session, issuer_url: str, signed_claims: SignedClaims, *, pending: bool = False +) -> OIDCPublisher | PendingOIDCPublisher: """ Given an OIDC issuer URL and a dictionary of claims that have been verified for a token from that OIDC issuer, retrieve either an `OIDCPublisher` registered