Repository: pypi/warehouse

Issue:
Provide the project name in the email for migrating to API tokens for uploading to PyPI
**What's the problem this feature will solve?**
In the email from PyPI with subject **[PyPI] Migrate to API tokens for uploading to PyPI**, it tells us that a package was uploaded to PyPI using basic authentication instead of API token.

It does not tell us which package was uploaded. Packages in our org are uploaded via various in-house tooling, so we're still tracking down which tool/package was this.

I'd imagine there might be other maintainers who also use some form of automation in uploading their packages. In this case, adding the name of the package to the email would be useful in helping to track down the issue so we can improve the automation process.

**Describe the solution you'd like**
Include the name of the package in the email, to help maintainers figure out which package where this happened.

Suggested wording change:
> During your recent upload or upload attempt to PyPI **for the package {{ package_name }}**, we noticed you used basic authentication (username & password). However, your account has two-factor authentication (2FA) enabled.


**Additional context**
We received an email from PyPI, saying that there was a recent upload to PyPI using basic authentication (username & password).

Here's a copy of the email:

<details>
# What?

During your recent upload or upload attempt to PyPI, we noticed you used basic authentication (username & password). However, your account has two-factor authentication (2FA) enabled.

In the near future, PyPI will begin prohibiting uploads using basic authentication for accounts with two-factor authentication enabled. Instead, we will require API tokens to be used.

# What should I do?

First, generate an API token for your account or project at https://pypi.org/manage/account/token/. Then, use this token when publishing instead of your username and password. See https://pypi.org/help/#apitoken for help using API tokens to publish.
</details>

It would be great if the package name was included in the email. We're also wondering if there was a reason that the package name was not included, in case we're missing some context here.


Discussion:

Resolving patch:
diff --git a/warehouse/email/__init__.py b/warehouse/email/__init__.py
--- a/warehouse/email/__init__.py
+++ b/warehouse/email/__init__.py
@@ -304,7 +304,7 @@ def send_token_compromised_email_leak(request, user, *, public_url, origin):
     allow_unverified=True,
     repeat_window=datetime.timedelta(days=1),
 )
-def send_basic_auth_with_two_factor_email(request, user):
+def send_basic_auth_with_two_factor_email(request, user, *, project_name):
     return {}
 
 
diff --git a/warehouse/forklift/legacy.py b/warehouse/forklift/legacy.py
--- a/warehouse/forklift/legacy.py
+++ b/warehouse/forklift/legacy.py
@@ -947,7 +947,9 @@ def file_upload(request):
         and request.user.has_two_factor
     ):
         # Eventually, raise here to disable basic auth with 2FA enabled
-        send_basic_auth_with_two_factor_email(request, request.user)
+        send_basic_auth_with_two_factor_email(
+            request, request.user, project_name=project.name
+        )
 
     # Update name if it differs but is still equivalent. We don't need to check if
     # they are equivalent when normalized because that's already been done when we


Test patch:
diff --git a/tests/unit/email/test_init.py b/tests/unit/email/test_init.py
--- a/tests/unit/email/test_init.py
+++ b/tests/unit/email/test_init.py
@@ -1294,8 +1294,10 @@ def test_basic_auth_with_2fa_email(
         )
         pyramid_request.user = stub_user
         pyramid_request.registry.settings = {"mail.sender": "noreply@example.com"}
-
-        result = email.send_basic_auth_with_two_factor_email(pyramid_request, stub_user)
+        project_name = "exampleproject"
+        result = email.send_basic_auth_with_two_factor_email(
+            pyramid_request, stub_user, project_name=project_name
+        )
 
         assert result == {}
         assert pyramid_request.task.calls == [pretend.call(send_email)]
diff --git a/tests/unit/forklift/test_legacy.py b/tests/unit/forklift/test_legacy.py
--- a/tests/unit/forklift/test_legacy.py
+++ b/tests/unit/forklift/test_legacy.py
@@ -2649,7 +2649,9 @@ def test_upload_succeeds_with_2fa_enabled(
 
         legacy.file_upload(db_request)
 
-        assert send_email.calls == [pretend.call(db_request, user)]
+        assert send_email.calls == [
+            pretend.call(db_request, user, project_name=project.name)
+        ]
 
     @pytest.mark.parametrize(
         "plat",