diff --git a/tests/unit/accounts/test_core.py b/tests/unit/accounts/test_core.py index f30d20963392..94361629e3e2 100644 --- a/tests/unit/accounts/test_core.py +++ b/tests/unit/accounts/test_core.py @@ -34,11 +34,7 @@ TokenServiceFactory, database_login_factory, ) -from warehouse.errors import ( - BasicAuthBreachedPassword, - BasicAuthFailedPassword, - BasicAuthTwoFactorEnabled, -) +from warehouse.errors import BasicAuthBreachedPassword, BasicAuthFailedPassword from warehouse.events.tags import EventTag from warehouse.oidc.models import OIDCPublisher from warehouse.rate_limiting import IRateLimiter, RateLimit @@ -80,7 +76,6 @@ def test_with_invalid_password(self, pyramid_request, pyramid_services): lambda userid, password, tags=None: False ), is_disabled=pretend.call_recorder(lambda user_id: (False, None)), - has_two_factor=pretend.call_recorder(lambda uid: False), ) pyramid_services.register_service(service, IUserService, None) pyramid_services.register_service( @@ -217,7 +212,6 @@ def test_with_valid_password(self, monkeypatch, pyramid_request, pyramid_service ), update_user=pretend.call_recorder(lambda userid, last_login: None), is_disabled=pretend.call_recorder(lambda user_id: (False, None)), - has_two_factor=pretend.call_recorder(lambda uid: False), ) breach_service = pretend.stub( check_password=pretend.call_recorder(lambda pw, tags=None: False) @@ -238,7 +232,6 @@ def test_with_valid_password(self, monkeypatch, pyramid_request, pyramid_service assert service.find_userid.calls == [pretend.call("myuser")] assert service.get_user.calls == [pretend.call(2)] assert service.is_disabled.calls == [pretend.call(2)] - assert service.has_two_factor.calls == [pretend.call(2)] assert service.check_password.calls == [ pretend.call( 2, @@ -259,52 +252,6 @@ def test_with_valid_password(self, monkeypatch, pyramid_request, pyramid_service ) ] - def test_via_basic_auth_2fa_enforced( - self, monkeypatch, pyramid_request, pyramid_services - ): - send_email = pretend.call_recorder(lambda *a, **kw: None) - - user = pretend.stub(id=2, username="multiFactor") - service = pretend.stub( - get_user=pretend.call_recorder(lambda user_id: user), - find_userid=pretend.call_recorder(lambda username: 2), - check_password=pretend.call_recorder( - lambda userid, password, tags=None: True - ), - is_disabled=pretend.call_recorder(lambda user_id: (False, None)), - disable_password=pretend.call_recorder( - lambda user_id, request, reason=None: None - ), - has_two_factor=pretend.call_recorder(lambda uid: True), - ) - breach_service = pretend.stub( - check_password=pretend.call_recorder(lambda pw, tags=None: False), - ) - - pyramid_services.register_service(service, IUserService, None) - pyramid_services.register_service( - breach_service, IPasswordBreachedService, None - ) - - pyramid_request.matched_route = pretend.stub(name="forklift.legacy.file_upload") - - with pytest.raises(BasicAuthTwoFactorEnabled) as excinfo: - _basic_auth_check("multiFactor", "mypass", pyramid_request) - - assert excinfo.value.status == ( - "401 User multiFactor has two factor auth enabled, " - "an API Token or Trusted Publisher must be used " - "to upload in place of password." - ) - assert service.find_userid.calls == [pretend.call("multiFactor")] - assert service.get_user.calls == [pretend.call(2)] - assert service.is_disabled.calls == [pretend.call(2)] - assert service.has_two_factor.calls == [pretend.call(2)] - assert service.check_password.calls == [] - assert breach_service.check_password.calls == [] - assert service.disable_password.calls == [] - assert send_email.calls == [] - def test_via_basic_auth_compromised( self, monkeypatch, pyramid_request, pyramid_services ): @@ -324,7 +271,6 @@ def test_via_basic_auth_compromised( disable_password=pretend.call_recorder( lambda user_id, request, reason=None: None ), - has_two_factor=pretend.call_recorder(lambda uid: False), ) breach_service = pretend.stub( check_password=pretend.call_recorder(lambda pw, tags=None: True), diff --git a/tests/unit/forklift/test_legacy.py b/tests/unit/forklift/test_legacy.py index 2711d6634ec5..1d752dcb39a5 100644 --- a/tests/unit/forklift/test_legacy.py +++ b/tests/unit/forklift/test_legacy.py @@ -35,6 +35,7 @@ from warehouse.admin.flags import AdminFlag, AdminFlagValue from warehouse.classifiers.models import Classifier +from warehouse.errors import BasicAuthTwoFactorEnabled from warehouse.forklift import legacy from warehouse.metrics import IMetricsService from warehouse.packaging.interfaces import IFileStorage, IProjectService @@ -2556,7 +2557,7 @@ def test_upload_fails_without_oidc_publisher_permission( "See /the/help/url/ for more information." ).format(project.name) - def test_upload_succeeds_with_2fa_enabled( + def test_basic_auth_upload_fails_with_2fa_enabled( self, pyramid_config, db_request, metrics, monkeypatch ): user = UserFactory.create(totp_secret=b"secret") @@ -2593,8 +2594,17 @@ def test_upload_succeeds_with_2fa_enabled( IMetricsService: metrics, }.get(svc) - legacy.file_upload(db_request) + with pytest.raises(BasicAuthTwoFactorEnabled) as excinfo: + legacy.file_upload(db_request) + + resp = excinfo.value + assert resp.status_code == 401 + assert resp.status == ( + f"401 User { user.username } has two factor auth enabled, " + "an API Token or Trusted Publisher must be used to upload " + "in place of password." + ) assert send_email.calls == [ pretend.call(db_request, user, project_name=project.name) ] diff --git a/warehouse/accounts/security_policy.py b/warehouse/accounts/security_policy.py index c784d8a28e6c..16dc2d6775e9 100644 --- a/warehouse/accounts/security_policy.py +++ b/warehouse/accounts/security_policy.py @@ -29,7 +29,6 @@ BasicAuthAccountFrozen, BasicAuthBreachedPassword, BasicAuthFailedPassword, - BasicAuthTwoFactorEnabled, WarehouseDenied, ) from warehouse.events.tags import EventTag @@ -76,15 +75,6 @@ def _basic_auth_check(username, password, request): raise _format_exc_status(BasicAuthAccountFrozen(), "Account is frozen.") else: raise _format_exc_status(HTTPUnauthorized(), "Account is disabled.") - elif login_service.has_two_factor(user.id): - raise _format_exc_status( - BasicAuthTwoFactorEnabled(), - ( - f"User {user.username} has two factor auth enabled, " - "an API Token or Trusted Publisher must be used to upload " - "in place of password." - ), - ) elif login_service.check_password( user.id, password, diff --git a/warehouse/forklift/legacy.py b/warehouse/forklift/legacy.py index c16c1d7d922f..636b8db2036b 100644 --- a/warehouse/forklift/legacy.py +++ b/warehouse/forklift/legacy.py @@ -52,6 +52,7 @@ send_basic_auth_with_two_factor_email, send_gpg_signature_uploaded_email, ) +from warehouse.errors import BasicAuthTwoFactorEnabled from warehouse.events.tags import EventTag from warehouse.metrics import IMetricsService from warehouse.packaging.interfaces import IFileStorage, IProjectService @@ -1017,10 +1018,17 @@ def file_upload(request): request.authentication_method == AuthenticationMethod.BASIC_AUTH 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, project_name=project.name ) + raise _exc_with_message( + BasicAuthTwoFactorEnabled, + ( + f"User { request.user.username } has two factor auth enabled, " + "an API Token or Trusted Publisher must be used to upload " + "in place of password." + ), + ) # 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 diff --git a/warehouse/locale/messages.pot b/warehouse/locale/messages.pot index 25fb07e11b31..19dbf73d45a8 100644 --- a/warehouse/locale/messages.pot +++ b/warehouse/locale/messages.pot @@ -1744,17 +1744,17 @@ msgstr "" #, python-format msgid "" "During your recent upload or upload attempt of %(project_name)s to " -"%(site)s, we noticed you used basic authentication (username & " -"password). However, your account has two-factor authentication (2FA) " -"enabled." +"%(site)s, we noticed you attempted to use basic authentication (username " +"& password). However, your account has two-factor authentication " +"(2FA) enabled." msgstr "" #: warehouse/templates/email/basic-auth-with-2fa/body.html:22 #, python-format msgid "" -"In the near future, %(site)s will begin prohibiting uploads using basic " -"authentication for accounts with two-factor authentication enabled. " -"Instead, we will require API tokens to be used." +"%(site)s now prohibits uploads using basic authentication for accounts " +"with two-factor authentication enabled. Instead, we require API tokens or" +" Trusted Publishers to be used instead." msgstr "" #: warehouse/templates/email/basic-auth-with-2fa/body.html:25 @@ -1767,12 +1767,17 @@ msgstr "" #: warehouse/templates/email/basic-auth-with-2fa/body.html:27 #, python-format msgid "" -"First, generate an API token for your account or project at " -"%(new_token_url)s. Then, use this token when publishing instead of your " +"If using API tokens, generate an API token for your account or project at" +" %(new_token_url)s. Then, use this token when publishing instead of your " "username and password. See %(token_help_url)s for help using API tokens " "to publish." msgstr "" +#: warehouse/templates/email/basic-auth-with-2fa/body.html:30 +#, python-format +msgid "If using Trusted Publishers (preferred), see %(tp_url)s to get started." +msgstr "" + #: warehouse/templates/email/canceled-as-invited-organization-member/body.html:19 #, python-format msgid "" diff --git a/warehouse/templates/email/basic-auth-with-2fa/body.html b/warehouse/templates/email/basic-auth-with-2fa/body.html index a7aebf3fc473..668e8cde11e1 100644 --- a/warehouse/templates/email/basic-auth-with-2fa/body.html +++ b/warehouse/templates/email/basic-auth-with-2fa/body.html @@ -16,14 +16,17 @@ {% block content %}
- {% trans site=request.registry.settings["site.name"] %}During your recent upload or upload attempt of {{ project_name }} to {{ site }}, we noticed you used basic authentication (username & password). However, your account has two-factor authentication (2FA) enabled.{% endtrans %} + {% trans site=request.registry.settings["site.name"] %}During your recent upload or upload attempt of {{ project_name }} to {{ site }}, we noticed you attempted to use basic authentication (username & password). However, your account has two-factor authentication (2FA) enabled.{% endtrans %}
- {% trans site=request.registry.settings["site.name"] %}In the near future, {{ site }} will begin prohibiting uploads using basic authentication for accounts with two-factor authentication enabled. Instead, we will require API tokens to be used.{% endtrans %} + {% trans site=request.registry.settings["site.name"] %}{{ site }} now prohibits uploads using basic authentication for accounts with two-factor authentication enabled. Instead, we require API tokens or Trusted Publishers to be used instead.{% endtrans %}
- {% trans new_token_url=request.route_url('manage.account.token', _host=request.registry.settings.get('warehouse.domain')), token_help_url=request.help_url(_anchor='apitoken') %}First, generate an API token for your account or project at {{ new_token_url }}. Then, use this token when publishing instead of your username and password. See {{ token_help_url }} for help using API tokens to publish.{% endtrans %} + {% trans new_token_url=request.route_url('manage.account.token', _host=request.registry.settings.get('warehouse.domain')), token_help_url=request.help_url(_anchor='apitoken') %}If using API tokens, generate an API token for your account or project at {{ new_token_url }}. Then, use this token when publishing instead of your username and password. See {{ token_help_url }} for help using API tokens to publish.{% endtrans %} +
++{% trans tp_url="https://docs.pypi.org/trusted-publishers/" %}If using Trusted Publishers (preferred), see {{ tp_url }} to get started.{% endtrans %}
{% endblock %} diff --git a/warehouse/templates/email/basic-auth-with-2fa/body.txt b/warehouse/templates/email/basic-auth-with-2fa/body.txt index 31768a8ab164..bd31699ba714 100644 --- a/warehouse/templates/email/basic-auth-with-2fa/body.txt +++ b/warehouse/templates/email/basic-auth-with-2fa/body.txt @@ -16,12 +16,14 @@ {% block content %} # {% trans %}What?{% endtrans %} -{% trans site=request.registry.settings["site.name"] %}During your recent upload or upload attempt of {{ project_name }} to {{ site }}, we noticed you used basic authentication (username & password). However, your account has two-factor authentication (2FA) enabled.{% endtrans %} +{% trans site=request.registry.settings["site.name"] %}During your recent upload or upload attempt of {{ project_name }} to {{ site }}, we noticed you attempted to use basic authentication (username & password). However, your account has two-factor authentication (2FA) enabled.{% endtrans %} -{% trans site=request.registry.settings["site.name"] %}In the near future, {{ site }} will begin prohibiting uploads using basic authentication for accounts with two-factor authentication enabled. Instead, we will require API tokens to be used.{% endtrans %} +{% trans site=request.registry.settings["site.name"] %}{{ site }} now prohibits uploads using basic authentication for accounts with two-factor authentication enabled. Instead, we require API tokens or Trusted Publishers to be used instead.{% endtrans %} # {% trans %}What should I do?{% endtrans %} -{% trans new_token_url=request.route_url('manage.account.token', _host=request.registry.settings.get('warehouse.domain')), token_help_url=request.help_url(_anchor='apitoken') %}First, generate an API token for your account or project at {{ new_token_url }}. Then, use this token when publishing instead of your username and password. See {{ token_help_url }} for help using API tokens to publish.{% endtrans %} +{% trans new_token_url=request.route_url('manage.account.token', _host=request.registry.settings.get('warehouse.domain')), token_help_url=request.help_url(_anchor='apitoken') %}If using API tokens, generate an API token for your account or project at {{ new_token_url }}. Then, use this token when publishing instead of your username and password. See {{ token_help_url }} for help using API tokens to publish.{% endtrans %} + +{% trans tp_url="https://docs.pypi.org/trusted-publishers/" %}If using Trusted Publishers (preferred), see {{ tp_url }} to get started.{% endtrans %} {% endblock %} diff --git a/warehouse/templates/email/basic-auth-with-2fa/subject.txt b/warehouse/templates/email/basic-auth-with-2fa/subject.txt index 0ccf94b52c19..2029e3a94b2e 100644 --- a/warehouse/templates/email/basic-auth-with-2fa/subject.txt +++ b/warehouse/templates/email/basic-auth-with-2fa/subject.txt @@ -14,4 +14,4 @@ {% extends "email/_base/subject.txt" %} -{% block subject %}{% trans site=request.registry.settings["site.name"] %}Migrate to API tokens for uploading to {{ site }}{% endtrans %}{% endblock %} +{% block subject %}{% trans site=request.registry.settings["site.name"] %}Migrate authentication for uploading to {{ site }}{% endtrans %}{% endblock %}