From 1399e7d3f5fc26d935f5f4da857e9db7b378d14f Mon Sep 17 00:00:00 2001 From: Ee Durbin Date: Sun, 31 Mar 2024 10:00:43 -0400 Subject: [PATCH 01/21] require a verified email address for any account action --- warehouse/accounts/security_policy.py | 6 +- warehouse/manage/views/__init__.py | 106 ++++++++++++++---------- warehouse/routes.py | 5 ++ warehouse/templates/manage/account.html | 45 +++++++++- 4 files changed, 112 insertions(+), 50 deletions(-) diff --git a/warehouse/accounts/security_policy.py b/warehouse/accounts/security_policy.py index efcb99ec3fa8..f959cb22e8a5 100644 --- a/warehouse/accounts/security_policy.py +++ b/warehouse/accounts/security_policy.py @@ -185,7 +185,10 @@ def _permits_for_user_policy(acl, request, context, permission): isinstance(res, Allowed) and not request.identity.has_primary_verified_email and request.matched_route.name.startswith("manage") - and request.matched_route.name != "manage.account" + and request.matched_route.name != "manage.account.reverify-email" + and not ( + request.matched_route.name == "manage.account" and request.method == "GET" + ) ): return WarehouseDenied("unverified", reason="unverified_email") @@ -214,6 +217,7 @@ def _check_for_mfa(request, context) -> WarehouseDenied | None: "manage.account.totp-provision", "manage.account.two-factor", "manage.account.webauthn-provision", + "manage.account.reverify-email", ] if ( diff --git a/warehouse/manage/views/__init__.py b/warehouse/manage/views/__init__.py index ab47c0061ea1..ef2afd26e80d 100644 --- a/warehouse/manage/views/__init__.py +++ b/warehouse/manage/views/__init__.py @@ -333,51 +333,6 @@ def change_primary_email(self): return HTTPSeeOther(self.request.path) - @view_config(request_method="POST", request_param=["reverify_email_id"]) - def reverify_email(self): - try: - email = ( - self.request.db.query(Email) - .filter( - Email.id == int(self.request.POST["reverify_email_id"]), - Email.user_id == self.request.user.id, - ) - .one() - ) - except NoResultFound: - self.request.session.flash("Email address not found", queue="error") - return self.default_response - - if email.verified: - self.request.session.flash("Email is already verified", queue="error") - else: - verify_email_ratelimit = self.request.find_service( - IRateLimiter, name="email.verify" - ) - if verify_email_ratelimit.test(self.request.user.id): - send_email_verification_email(self.request, (self.request.user, email)) - verify_email_ratelimit.hit(self.request.user.id) - email.user.record_event( - tag=EventTag.Account.EmailReverify, - request=self.request, - additional={"email": email.email}, - ) - - self.request.session.flash( - f"Verification email for {email.email} resent", queue="success" - ) - else: - self.request.session.flash( - ( - "Too many incomplete attempts to verify email address(es) for " - f"{self.request.user.username}. Complete a pending " - "verification or wait before attempting again." - ), - queue="error", - ) - - return HTTPSeeOther(self.request.path) - @view_config(request_method="POST", request_param=ChangePasswordForm.__params__) def change_password(self): form = ChangePasswordForm( @@ -467,6 +422,67 @@ def delete_account(self): return logout(self.request) +@view_defaults( + route_name="manage.account.reverify-email", + renderer="manage/account.html", + uses_session=True, + require_csrf=True, + require_methods=False, + permission=Permissions.AccountManage, + has_translations=True, + require_reauth=True, +) +class ManageAccountReverifyEmailViews: + def __init__(self, request): + self.request = request + self.user_service = request.find_service(IUserService, context=None) + + @view_config(request_method="POST") + def reverify_email(self): + try: + email = ( + self.request.db.query(Email) + .filter( + Email.id == int(self.request.POST["reverify_email_id"]), + Email.user_id == self.request.user.id, + ) + .one() + ) + except NoResultFound: + self.request.session.flash("Email address not found", queue="error") + return self.default_response + + if email.verified: + self.request.session.flash("Email is already verified", queue="error") + else: + verify_email_ratelimit = self.request.find_service( + IRateLimiter, name="email.verify" + ) + if verify_email_ratelimit.test(self.request.user.id): + send_email_verification_email(self.request, (self.request.user, email)) + verify_email_ratelimit.hit(self.request.user.id) + email.user.record_event( + tag=EventTag.Account.EmailReverify, + request=self.request, + additional={"email": email.email}, + ) + + self.request.session.flash( + f"Verification email for {email.email} resent", queue="success" + ) + else: + self.request.session.flash( + ( + "Too many incomplete attempts to verify email address(es) for " + f"{self.request.user.username}. Complete a pending " + "verification or wait before attempting again." + ), + queue="error", + ) + + return HTTPSeeOther(self.request.route_path("manage.account")) + + @view_config( route_name="manage.account.two-factor", renderer="manage/account/two-factor.html", diff --git a/warehouse/routes.py b/warehouse/routes.py index f104927976a4..dbbe8f0a834a 100644 --- a/warehouse/routes.py +++ b/warehouse/routes.py @@ -210,6 +210,11 @@ def includeme(config): # Management (views for logged-in users) config.add_route("manage.account", "/manage/account/", domain=warehouse) + config.add_route( + "manage.account.reverify-email", + "/manage/account/reverify-email", + domain=warehouse, + ) config.add_route( "manage.account.publishing", "/manage/account/publishing/", domain=warehouse ) diff --git a/warehouse/templates/manage/account.html b/warehouse/templates/manage/account.html index a7604fa78c4a..a675cbd6b282 100644 --- a/warehouse/templates/manage/account.html +++ b/warehouse/templates/manage/account.html @@ -92,7 +92,7 @@
- +
From 126cbcab74ca9fe3a9fcdd135d860e111c462f22 Mon Sep 17 00:00:00 2001 From: Dustin Ingram Date: Sun, 31 Mar 2024 15:15:38 +0000 Subject: [PATCH 02/21] Separate unverified views into a different route entirely --- warehouse/accounts/security_policy.py | 6 +- warehouse/manage/views/__init__.py | 141 +++++++++++++------------- warehouse/routes.py | 8 ++ 3 files changed, 82 insertions(+), 73 deletions(-) diff --git a/warehouse/accounts/security_policy.py b/warehouse/accounts/security_policy.py index f959cb22e8a5..e8e79f3aed46 100644 --- a/warehouse/accounts/security_policy.py +++ b/warehouse/accounts/security_policy.py @@ -184,11 +184,7 @@ def _permits_for_user_policy(acl, request, context, permission): if ( isinstance(res, Allowed) and not request.identity.has_primary_verified_email - and request.matched_route.name.startswith("manage") - and request.matched_route.name != "manage.account.reverify-email" - and not ( - request.matched_route.name == "manage.account" and request.method == "GET" - ) + and not request.matched_route.name.startswith("manage.unverified-account") ): return WarehouseDenied("unverified", reason="unverified_email") diff --git a/warehouse/manage/views/__init__.py b/warehouse/manage/views/__init__.py index ef2afd26e80d..cf0f07e4d6b0 100644 --- a/warehouse/manage/views/__init__.py +++ b/warehouse/manage/views/__init__.py @@ -139,6 +139,78 @@ from warehouse.utils.project import confirm_project, destroy_docs, remove_project +class ManageAccountMixin: + + def __init__(self, request): + self.request = request + self.user_service = request.find_service(IUserService, context=None) + self.breach_service = request.find_service( + IPasswordBreachedService, context=None + ) + + @view_config(request_method="POST") + def reverify_email(self): + try: + email = ( + self.request.db.query(Email) + .filter( + Email.id == int(self.request.POST["reverify_email_id"]), + Email.user_id == self.request.user.id, + ) + .one() + ) + except NoResultFound: + self.request.session.flash("Email address not found", queue="error") + return self.default_response + + if email.verified: + self.request.session.flash("Email is already verified", queue="error") + else: + verify_email_ratelimit = self.request.find_service( + IRateLimiter, name="email.verify" + ) + if verify_email_ratelimit.test(self.request.user.id): + send_email_verification_email(self.request, (self.request.user, email)) + verify_email_ratelimit.hit(self.request.user.id) + email.user.record_event( + tag=EventTag.Account.EmailReverify, + request=self.request, + additional={"email": email.email}, + ) + + self.request.session.flash( + f"Verification email for {email.email} resent", queue="success" + ) + else: + self.request.session.flash( + ( + "Too many incomplete attempts to verify email address(es) for " + f"{self.request.user.username}. Complete a pending " + "verification or wait before attempting again." + ), + queue="error", + ) + + return HTTPSeeOther(self.request.route_path("manage.account")) + + +@view_defaults( + route_name="manage.unverified-account", + renderer="manage/unverified-account.html", + uses_session=True, + require_csrf=True, + require_methods=False, + permission=Permissions.AccountManage, + has_translations=True, + require_reauth=True, +) +class ManageUnverifiedAccountViews(ManageAccountMixin): + + @view_config(request_method="GET") + def manage_unverified_account(self): + return {} + + @view_defaults( route_name="manage.account", renderer="manage/account.html", @@ -149,13 +221,7 @@ has_translations=True, require_reauth=True, ) -class ManageAccountViews: - def __init__(self, request): - self.request = request - self.user_service = request.find_service(IUserService, context=None) - self.breach_service = request.find_service( - IPasswordBreachedService, context=None - ) +class ManageVerifiedAccountViews: @property def active_projects(self): @@ -422,67 +488,6 @@ def delete_account(self): return logout(self.request) -@view_defaults( - route_name="manage.account.reverify-email", - renderer="manage/account.html", - uses_session=True, - require_csrf=True, - require_methods=False, - permission=Permissions.AccountManage, - has_translations=True, - require_reauth=True, -) -class ManageAccountReverifyEmailViews: - def __init__(self, request): - self.request = request - self.user_service = request.find_service(IUserService, context=None) - - @view_config(request_method="POST") - def reverify_email(self): - try: - email = ( - self.request.db.query(Email) - .filter( - Email.id == int(self.request.POST["reverify_email_id"]), - Email.user_id == self.request.user.id, - ) - .one() - ) - except NoResultFound: - self.request.session.flash("Email address not found", queue="error") - return self.default_response - - if email.verified: - self.request.session.flash("Email is already verified", queue="error") - else: - verify_email_ratelimit = self.request.find_service( - IRateLimiter, name="email.verify" - ) - if verify_email_ratelimit.test(self.request.user.id): - send_email_verification_email(self.request, (self.request.user, email)) - verify_email_ratelimit.hit(self.request.user.id) - email.user.record_event( - tag=EventTag.Account.EmailReverify, - request=self.request, - additional={"email": email.email}, - ) - - self.request.session.flash( - f"Verification email for {email.email} resent", queue="success" - ) - else: - self.request.session.flash( - ( - "Too many incomplete attempts to verify email address(es) for " - f"{self.request.user.username}. Complete a pending " - "verification or wait before attempting again." - ), - queue="error", - ) - - return HTTPSeeOther(self.request.route_path("manage.account")) - - @view_config( route_name="manage.account.two-factor", renderer="manage/account/two-factor.html", diff --git a/warehouse/routes.py b/warehouse/routes.py index dbbe8f0a834a..728ff03d6dc9 100644 --- a/warehouse/routes.py +++ b/warehouse/routes.py @@ -209,6 +209,14 @@ def includeme(config): ) # Management (views for logged-in users) + config.add_route( + "manage.unverified-account", "/manage/unverified-account/", domain=warehouse + ) + config.add_route( + "manage.unverified-account.reverify-email", + "/manage/unverified-account/reverify-email", + domain=warehouse, + ) config.add_route("manage.account", "/manage/account/", domain=warehouse) config.add_route( "manage.account.reverify-email", From c8d399e2ddd22180042db700bb0c27b7136b1b65 Mon Sep 17 00:00:00 2001 From: Dustin Ingram Date: Sun, 31 Mar 2024 15:18:18 +0000 Subject: [PATCH 03/21] Use mixin for both --- warehouse/manage/views/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/warehouse/manage/views/__init__.py b/warehouse/manage/views/__init__.py index cf0f07e4d6b0..f98953d05679 100644 --- a/warehouse/manage/views/__init__.py +++ b/warehouse/manage/views/__init__.py @@ -221,7 +221,7 @@ def manage_unverified_account(self): has_translations=True, require_reauth=True, ) -class ManageVerifiedAccountViews: +class ManageVerifiedAccountViews(ManageAccountMixin): @property def active_projects(self): From ec9fb5ac4f73db03028cfccf181bcfb81dd3cb08 Mon Sep 17 00:00:00 2001 From: Ee Durbin Date: Sun, 31 Mar 2024 12:22:12 -0400 Subject: [PATCH 04/21] =?UTF-8?q?push=20this=20a=20bit=20more=20forward=20?= =?UTF-8?q?=F0=9F=A5=9A=F0=9F=90=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tests/unit/manage/test_views.py | 96 ++-- tests/unit/test_routes.py | 13 + warehouse/accounts/security_policy.py | 3 +- .../templates/manage/unverified-account.html | 473 ++++++++++++++++++ warehouse/views.py | 2 +- 5 files changed, 538 insertions(+), 49 deletions(-) create mode 100644 warehouse/templates/manage/unverified-account.html diff --git a/tests/unit/manage/test_views.py b/tests/unit/manage/test_views.py index 830d1b9b4a18..604153b4eae3 100644 --- a/tests/unit/manage/test_views.py +++ b/tests/unit/manage/test_views.py @@ -126,9 +126,11 @@ def test_default_response(self, monkeypatch, public_email, expected_public_email change_pass_cls = pretend.call_recorder(lambda **kw: change_pass_obj) monkeypatch.setattr(views, "ChangePasswordForm", change_pass_cls) - view = views.ManageAccountViews(request) + view = views.ManageVerifiedAccountViews(request) - monkeypatch.setattr(views.ManageAccountViews, "active_projects", pretend.stub()) + monkeypatch.setattr( + views.ManageVerifiedAccountViews, "active_projects", pretend.stub() + ) assert view.default_response == { "save_account_form": save_account_obj, @@ -183,7 +185,7 @@ def test_active_projects(self, db_request): RoleFactory.create(user=user, project=not_an_owner, role_name="Maintainer") RoleFactory.create(user=another_user, project=not_an_owner, role_name="Owner") - view = views.ManageAccountViews(db_request) + view = views.ManageVerifiedAccountViews(db_request) assert view.active_projects == [with_sole_owner] @@ -194,9 +196,9 @@ def test_manage_account(self, monkeypatch): find_service=lambda *a, **kw: user_service, user=pretend.stub(name=name) ) monkeypatch.setattr( - views.ManageAccountViews, "default_response", {"_": pretend.stub()} + views.ManageVerifiedAccountViews, "default_response", {"_": pretend.stub()} ) - view = views.ManageAccountViews(request) + view = views.ManageVerifiedAccountViews(request) assert view.manage_account() == view.default_response assert view.request == request @@ -224,9 +226,9 @@ def test_save_account(self, monkeypatch, pyramid_request): ) monkeypatch.setattr(views, "SaveAccountForm", lambda *a, **kw: save_account_obj) monkeypatch.setattr( - views.ManageAccountViews, "default_response", {"_": pretend.stub()} + views.ManageVerifiedAccountViews, "default_response", {"_": pretend.stub()} ) - view = views.ManageAccountViews(pyramid_request) + view = views.ManageVerifiedAccountViews(pyramid_request) assert isinstance(view.save_account(), HTTPSeeOther) assert pyramid_request.session.flash.calls == [ @@ -248,9 +250,9 @@ def test_save_account_validation_fails(self, monkeypatch): save_account_obj = pretend.stub(validate=lambda: False) monkeypatch.setattr(views, "SaveAccountForm", lambda *a, **kw: save_account_obj) monkeypatch.setattr( - views.ManageAccountViews, "default_response", {"_": pretend.stub()} + views.ManageVerifiedAccountViews, "default_response", {"_": pretend.stub()} ) - view = views.ManageAccountViews(request) + view = views.ManageVerifiedAccountViews(request) assert view.save_account() == { **view.default_response, @@ -298,9 +300,9 @@ def test_add_email(self, monkeypatch, pyramid_request): ) monkeypatch.setattr( - views.ManageAccountViews, "default_response", {"_": pretend.stub()} + views.ManageVerifiedAccountViews, "default_response", {"_": pretend.stub()} ) - view = views.ManageAccountViews(pyramid_request) + view = views.ManageVerifiedAccountViews(pyramid_request) assert isinstance(view.add_email(), HTTPSeeOther) assert user_service.add_email.calls == [ @@ -351,9 +353,9 @@ def test_add_email_validation_fails(self, monkeypatch): monkeypatch.setattr(views, "Email", email_cls) monkeypatch.setattr( - views.ManageAccountViews, "default_response", {"_": pretend.stub()} + views.ManageVerifiedAccountViews, "default_response", {"_": pretend.stub()} ) - view = views.ManageAccountViews(request) + view = views.ManageVerifiedAccountViews(request) assert view.add_email() == { **view.default_response, @@ -388,9 +390,9 @@ def test_delete_email(self, monkeypatch): path="request-path", ) monkeypatch.setattr( - views.ManageAccountViews, "default_response", {"_": pretend.stub()} + views.ManageVerifiedAccountViews, "default_response", {"_": pretend.stub()} ) - view = views.ManageAccountViews(request) + view = views.ManageVerifiedAccountViews(request) assert isinstance(view.delete_email(), HTTPSeeOther) assert request.session.flash.calls == [ @@ -423,9 +425,9 @@ def raise_no_result(): session=pretend.stub(flash=pretend.call_recorder(lambda *a, **kw: None)), ) monkeypatch.setattr( - views.ManageAccountViews, "default_response", {"_": pretend.stub()} + views.ManageVerifiedAccountViews, "default_response", {"_": pretend.stub()} ) - view = views.ManageAccountViews(request) + view = views.ManageVerifiedAccountViews(request) assert view.delete_email() == view.default_response assert request.session.flash.calls == [ @@ -448,9 +450,9 @@ def test_delete_email_is_primary(self, monkeypatch): session=pretend.stub(flash=pretend.call_recorder(lambda *a, **kw: None)), ) monkeypatch.setattr( - views.ManageAccountViews, "default_response", {"_": pretend.stub()} + views.ManageVerifiedAccountViews, "default_response", {"_": pretend.stub()} ) - view = views.ManageAccountViews(request) + view = views.ManageVerifiedAccountViews(request) assert view.delete_email() == view.default_response assert request.session.flash.calls == [ @@ -471,9 +473,9 @@ def test_change_primary_email(self, monkeypatch, db_request): db_request.POST = {"primary_email_id": str(new_primary.id)} db_request.session.flash = pretend.call_recorder(lambda *a, **kw: None) monkeypatch.setattr( - views.ManageAccountViews, "default_response", {"_": pretend.stub()} + views.ManageVerifiedAccountViews, "default_response", {"_": pretend.stub()} ) - view = views.ManageAccountViews(db_request) + view = views.ManageVerifiedAccountViews(db_request) send_email = pretend.call_recorder(lambda *a, **kw: None) monkeypatch.setattr(views, "send_primary_email_change_email", send_email) @@ -509,9 +511,9 @@ def test_change_primary_email_without_current(self, monkeypatch, db_request): db_request.POST = {"primary_email_id": str(new_primary.id)} db_request.session.flash = pretend.call_recorder(lambda *a, **kw: None) monkeypatch.setattr( - views.ManageAccountViews, "default_response", {"_": pretend.stub()} + views.ManageVerifiedAccountViews, "default_response", {"_": pretend.stub()} ) - view = views.ManageAccountViews(db_request) + view = views.ManageVerifiedAccountViews(db_request) send_email = pretend.call_recorder(lambda *a: None) monkeypatch.setattr(views, "send_primary_email_change_email", send_email) @@ -542,9 +544,9 @@ def test_change_primary_email_not_found(self, monkeypatch, db_request): db_request.POST = {"primary_email_id": str(missing_email_id)} db_request.session.flash = pretend.call_recorder(lambda *a, **kw: None) monkeypatch.setattr( - views.ManageAccountViews, "default_response", {"_": pretend.stub()} + views.ManageVerifiedAccountViews, "default_response", {"_": pretend.stub()} ) - view = views.ManageAccountViews(db_request) + view = views.ManageVerifiedAccountViews(db_request) assert view.change_primary_email() == view.default_response assert db_request.session.flash.calls == [ @@ -582,9 +584,9 @@ def test_reverify_email(self, monkeypatch): send_email = pretend.call_recorder(lambda *a: None) monkeypatch.setattr(views, "send_email_verification_email", send_email) monkeypatch.setattr( - views.ManageAccountViews, "default_response", {"_": pretend.stub()} + views.ManageVerifiedAccountViews, "default_response", {"_": pretend.stub()} ) - view = views.ManageAccountViews(request) + view = views.ManageVerifiedAccountViews(request) assert isinstance(view.reverify_email(), HTTPSeeOther) assert request.session.flash.calls == [ @@ -628,9 +630,9 @@ def test_reverify_email_ratelimit_exceeded(self, monkeypatch): send_email = pretend.call_recorder(lambda *a: None) monkeypatch.setattr(views, "send_email_verification_email", send_email) monkeypatch.setattr( - views.ManageAccountViews, "default_response", {"_": pretend.stub()} + views.ManageVerifiedAccountViews, "default_response", {"_": pretend.stub()} ) - view = views.ManageAccountViews(request) + view = views.ManageVerifiedAccountViews(request) assert isinstance(view.reverify_email(), HTTPSeeOther) assert request.session.flash.calls == [ @@ -664,9 +666,9 @@ def raise_no_result(): send_email = pretend.call_recorder(lambda *a: None) monkeypatch.setattr(views, "send_email_verification_email", send_email) monkeypatch.setattr( - views.ManageAccountViews, "default_response", {"_": pretend.stub()} + views.ManageVerifiedAccountViews, "default_response", {"_": pretend.stub()} ) - view = views.ManageAccountViews(request) + view = views.ManageVerifiedAccountViews(request) assert view.reverify_email() == view.default_response assert request.session.flash.calls == [ @@ -692,9 +694,9 @@ def test_reverify_email_already_verified(self, monkeypatch): send_email = pretend.call_recorder(lambda *a: None) monkeypatch.setattr(views, "send_email_verification_email", send_email) monkeypatch.setattr( - views.ManageAccountViews, "default_response", {"_": pretend.stub()} + views.ManageVerifiedAccountViews, "default_response", {"_": pretend.stub()} ) - view = views.ManageAccountViews(request) + view = views.ManageVerifiedAccountViews(request) assert isinstance(view.reverify_email(), HTTPSeeOther) assert request.session.flash.calls == [ @@ -743,9 +745,9 @@ def test_change_password(self, monkeypatch): send_email = pretend.call_recorder(lambda *a: None) monkeypatch.setattr(views, "send_password_change_email", send_email) monkeypatch.setattr( - views.ManageAccountViews, "default_response", {"_": pretend.stub()} + views.ManageVerifiedAccountViews, "default_response", {"_": pretend.stub()} ) - view = views.ManageAccountViews(request) + view = views.ManageVerifiedAccountViews(request) assert isinstance(view.change_password(), HTTPSeeOther) assert request.session.flash.calls == [ @@ -792,9 +794,9 @@ def test_change_password_validation_fails(self, monkeypatch): send_email = pretend.call_recorder(lambda *a: None) monkeypatch.setattr(views, "send_password_change_email", send_email) monkeypatch.setattr( - views.ManageAccountViews, "default_response", {"_": pretend.stub()} + views.ManageVerifiedAccountViews, "default_response", {"_": pretend.stub()} ) - view = views.ManageAccountViews(request) + view = views.ManageVerifiedAccountViews(request) assert view.change_password() == { **view.default_response, @@ -820,16 +822,16 @@ def test_delete_account(self, monkeypatch, db_request): monkeypatch.setattr(views, "ConfirmPasswordForm", confirm_password_cls) monkeypatch.setattr( - views.ManageAccountViews, "default_response", pretend.stub() + views.ManageVerifiedAccountViews, "default_response", pretend.stub() ) - monkeypatch.setattr(views.ManageAccountViews, "active_projects", []) + monkeypatch.setattr(views.ManageVerifiedAccountViews, "active_projects", []) send_email = pretend.call_recorder(lambda *a: None) monkeypatch.setattr(views, "send_account_deletion_email", send_email) logout_response = pretend.stub() logout = pretend.call_recorder(lambda *a: logout_response) monkeypatch.setattr(views, "logout", logout) - view = views.ManageAccountViews(db_request) + view = views.ManageVerifiedAccountViews(db_request) assert view.delete_account() == logout_response @@ -853,10 +855,10 @@ def test_delete_account_no_confirm(self, monkeypatch): ) monkeypatch.setattr( - views.ManageAccountViews, "default_response", pretend.stub() + views.ManageVerifiedAccountViews, "default_response", pretend.stub() ) - view = views.ManageAccountViews(request) + view = views.ManageVerifiedAccountViews(request) assert view.delete_account() == view.default_response assert request.session.flash.calls == [ @@ -878,10 +880,10 @@ def test_delete_account_wrong_confirm(self, monkeypatch): monkeypatch.setattr(views, "ConfirmPasswordForm", confirm_password_cls) monkeypatch.setattr( - views.ManageAccountViews, "default_response", pretend.stub() + views.ManageVerifiedAccountViews, "default_response", pretend.stub() ) - view = views.ManageAccountViews(request) + view = views.ManageVerifiedAccountViews(request) assert view.delete_account() == view.default_response assert request.session.flash.calls == [ @@ -906,13 +908,13 @@ def test_delete_account_has_active_projects(self, monkeypatch): monkeypatch.setattr(views, "ConfirmPasswordForm", confirm_password_cls) monkeypatch.setattr( - views.ManageAccountViews, "default_response", pretend.stub() + views.ManageVerifiedAccountViews, "default_response", pretend.stub() ) monkeypatch.setattr( - views.ManageAccountViews, "active_projects", [pretend.stub()] + views.ManageVerifiedAccountViews, "active_projects", [pretend.stub()] ) - view = views.ManageAccountViews(request) + view = views.ManageVerifiedAccountViews(request) assert view.delete_account() == view.default_response assert request.session.flash.calls == [ diff --git a/tests/unit/test_routes.py b/tests/unit/test_routes.py index b8b509ebc577..205f5054b059 100644 --- a/tests/unit/test_routes.py +++ b/tests/unit/test_routes.py @@ -211,7 +211,20 @@ def add_policy(name, filename): "/account/verify-project-role/", domain=warehouse, ), + pretend.call( + "manage.unverified-account", "/manage/unverified-account/", domain=warehouse + ), + pretend.call( + "manage.unverified-account.reverify-email", + "/manage/unverified-account/reverify-email", + domain=warehouse, + ), pretend.call("manage.account", "/manage/account/", domain=warehouse), + pretend.call( + "manage.account.reverify-email", + "/manage/account/reverify-email", + domain=warehouse, + ), pretend.call( "manage.account.publishing", "/manage/account/publishing/", domain=warehouse ), diff --git a/warehouse/accounts/security_policy.py b/warehouse/accounts/security_policy.py index e8e79f3aed46..fd3dbac6fb99 100644 --- a/warehouse/accounts/security_policy.py +++ b/warehouse/accounts/security_policy.py @@ -213,7 +213,8 @@ def _check_for_mfa(request, context) -> WarehouseDenied | None: "manage.account.totp-provision", "manage.account.two-factor", "manage.account.webauthn-provision", - "manage.account.reverify-email", + "manage.unverified-account", + "manage.unverified-account.reverify-email", ] if ( diff --git a/warehouse/templates/manage/unverified-account.html b/warehouse/templates/manage/unverified-account.html new file mode 100644 index 000000000000..93c2ddcbc8da --- /dev/null +++ b/warehouse/templates/manage/unverified-account.html @@ -0,0 +1,473 @@ +{# + # Licensed under the Apache License, Version 2.0 (the "License"); + # you may not use this file except in compliance with the License. + # You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License is distributed on an "AS IS" BASIS, + # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + # See the License for the specific language governing permissions and + # limitations under the License. +-#} +{% extends "manage_base.html" %} + +{% set user = request.user %} +{% set title = gettext("Account settings") %} + +{% set active_tab = 'account' %} + +{% block title %} + {{ title }} +{% endblock %} + +{% macro email_verification_label(email) -%} +{% if email.verified %} + {% if email.transient_bounces %} + + + {% trans %}Verified*{% endtrans %} + + {% trans %}*Intermittent delivery problems may lead to verification loss{% endtrans %} + {% else %} + + + {% trans %}Verified{% endtrans %} + + {% endif %} +{% else %} + {% if email.unverify_reason.value == "spam complaint" %} + + + {% trans %}Unverified*{% endtrans %} + + {% trans %}*Email from PyPI being treated as spam{% endtrans %} + {% elif email.unverify_reason.value == "hard bounce" %} + + + {% trans %}Unverified*{% endtrans %} + + {% trans %}*Hard failure during delivery{% endtrans %} + {% elif email.unverify_reason.value == "soft bounce" %} + + + {% trans %}Unverified*{% endtrans %} + + {% trans %}*Too many delivery problems{% endtrans %} + {% else %} + + + {% trans %}Unverified{% endtrans %} + + {% endif %} +{% endif %} +{% endmacro %} + +{% macro email_row(email) -%} + + + {{ email.email }} + + + + {% if email.primary %} + {% trans %}Primary{% endtrans %} + {% endif %} + {{ email_verification_label(email) }} + + + + {% if not email.verified or not email.primary %} + + {% endif %} + + +{%- endmacro %} + +{% macro api_row(macaroon) -%} + + + {% trans %}Name{% endtrans %} + {{ macaroon.description }} + + + {% trans %}Scope{% endtrans %} + {% if macaroon.permissions_caveat.permissions == 'user' %} + {% trans %}All projects{% endtrans %} + {% else %} + {% for project in macaroon.permissions_caveat.get("permissions")['projects'] %} + {{ project }} + {% endfor %} + {% endif %} + + + {% trans %}Created{% endtrans %} + {{ humanize(macaroon.created) }} + + + {% trans %}Last used{% endtrans %} + {{ humanize(macaroon.last_used) if macaroon.last_used else gettext("Never") }} + + + + + {# modal to remove token #} + {% set slug="remove-API-token--" + macaroon.id | string %} + {% set title=gettext("Remove API token") + " - " + macaroon.description %} + {% set action=request.route_path('manage.account.token') %} + {% set confirm_button_label=gettext("Remove API token") %} + {% set extra_fields %} + + {% endset %} + {% set token_warning_text %} +

{% trans %}Applications or scripts using this token will no longer have access to PyPI.{% endtrans %}

+ {% endset %} + {{ confirm_password_modal(title=title, confirm_button_label=confirm_button_label, slug=slug, extra_fields=extra_fields, action=action, custom_warning_text=token_warning_text) }} + {# modal to view token ID #} + + + +{%- endmacro %} + +{% block main %} +

{{ title }}

+ +
+

{% trans %}Security history{% endtrans %}

+ + {% set recent_events = user.recent_events.all() %} + {% if recent_events|length > 0 %} + + {% macro caveat_detail(caveat) -%} + {% if "permissions" in caveat %} + {% if caveat.permissions == "user" %} + {% trans %}Token scope: entire account{% endtrans %} + {% else %} + {% trans project_name=caveat.permissions.projects[0] %}Token scope: Project {{ project_name }}{% endtrans %} + {% endif %} + {% elif "exp" in caveat %} + {% trans exp=humanize(caveat.exp) %}Expires: {{ exp }}{% endtrans %} + {% endif %} + {%- endmacro %} + + {% macro event_summary(event) -%} + {% if event.tag == EventTag.Account.AccountCreate %} + {% trans %}Account created{% endtrans %} + + {% elif event.tag == EventTag.Account.LoginSuccess %} + {% trans %}Logged in{% endtrans %}
+ + {% trans %}Two factor method:{% endtrans %} + {% if event.additional.two_factor_method == None %} + {% trans %}None{% endtrans %} + {% elif event.additional.two_factor_method == "webauthn" %} + {% if event.additional.two_factor_label %}"{{ event.additional.two_factor_label }}" - {% endif %}{% trans %}Security device (WebAuthn){% endtrans %} + {% elif event.additional.two_factor_method == "totp" %} + {% trans %}Authentication application (TOTP){% endtrans %} + {% elif event.additional.two_factor_method == "recovery-code" %} + {% trans %}Recovery code{% endtrans %} + {% endif %} + + + {% elif event.tag == EventTag.Account.LoginFailure %} + {% trans %}Login failed{% endtrans %} + {% if event.additional.auth_method %} + {% if event.additional.auth_method == "basic" %} + {% trans %}- Basic Auth (Upload endpoint){% endtrans %} + {% endif %} + {% endif %} +
+ + {% trans %}Reason:{% endtrans %} + {% if event.additional.reason == "invalid_password" %} + {% trans %}Incorrect Password{% endtrans %} + {% elif event.additional.reason == "invalid_totp" %} + {% trans %}Invalid two factor (TOTP){% endtrans %} + {% elif event.additional.reason == "invalid_webauthn" %} + {% trans %}Invalid two factor (WebAuthn){% endtrans %} + {% elif event.additional.reason == "invalid_recovery_code" %} + {% trans %}Invalid two factor (Recovery code){% endtrans %} + {% elif event.additional.reason == "burned_recovery_code" %} + {% trans %}Invalid two factor (Recovery code){% endtrans %} + {% else %} + {{ event.additional.reason }} + {% endif %} + + + {% elif event.tag == "account:reauthenticate:failure" %} + {% trans %}Session reauthentication failed{% endtrans %}
+ + {% trans %}Reason:{% endtrans %} + {% if event.additional.reason == "invalid_password" %} + {% trans %}Incorrect Password{% endtrans %} + {% else %} + {{ event.additional.reason }} + {% endif %} + + + {% elif event.tag == EventTag.Account.EmailAdd %} + {% trans %}Email added to account{% endtrans %}
+ {{ event.additional.email }} + {% elif event.tag == EventTag.Account.EmailRemove %} + {% trans %}Email removed from account{% endtrans %}
+ {{ event.additional.email }} + {% elif event.tag == EventTag.Account.EmailVerified %} + {% trans %}Email verified{% endtrans %}
+ {{ event.additional.email }} + {% elif event.tag == EventTag.Account.EmailReverify %} + {% trans %}Email reverified{% endtrans %}
+ {{ event.additional.email }} + {% elif event.tag == EventTag.Account.EmailPrimaryChange %} + {% if event.additional.old_primary %} + {% trans %}Primary email changed{% endtrans %}
+ + {% trans %}Old primary email:{% endtrans %} {{ event.additional.old_primary }}
+ {% trans %}New primary email:{% endtrans %} {{ event.additional.new_primary }} +
+ {% else %} + {% trans %}Primary email set{% endtrans %}
+ + {{ event.additional.new_primary }} + + {% endif %} + {% elif event.tag == EventTag.Account.EmailSent %} + {% trans %}Email sent{% endtrans %}
+ + {% trans %}From:{% endtrans %} {{ event.additional.from_ }}
+ {% trans %}To:{% endtrans %} {{ event.additional.to }}
+ {% trans %}Subject:{% endtrans %} {{event.additional.subject}} +
+ + {% elif event.tag == EventTag.Account.PasswordResetRequest %} + {% trans %}Password reset requested{% endtrans %} + {% elif event.tag == EventTag.Account.PasswordResetAttempt %} + {% trans %}Password reset attempted{% endtrans %} + {% elif event.tag == EventTag.Account.PasswordReset %} + {% trans %}Password successfully reset{% endtrans %} + {% elif event.tag == EventTag.Account.PasswordChange %} + {% trans %}Password successfully changed{% endtrans %} + + {% elif event.tag == EventTag.Account.PendingOIDCPublisherAdded %} + Pending trusted publisher added + {% trans %}Project:{% endtrans %} {{ event.additional.project }} + {{ oidc_audit_event(event) }} + + {% elif event.tag == EventTag.Account.PendingOIDCPublisherRemoved %} + Pending trusted publisher removed + {% trans %}Project:{% endtrans %} {{ event.additional.project }} + {{ oidc_audit_event(event) }} + {% elif event.tag == EventTag.Account.TwoFactorMethodAdded %} + {% trans %}Two factor authentication added{% endtrans %}
+ + {% if event.additional.method == "webauthn" %} + {% trans %}Method: Security device (WebAuthn){% endtrans %}
+ {% trans %}Device name:{% endtrans %} {{ event.additional.label }} + {% elif event.additional.method == "totp" %} + {% trans %}Method: Authentication application (TOTP){% endtrans %} + {% endif %} +
+ {% elif event.tag == EventTag.Account.TwoFactorMethodRemoved %} + {% trans %}Two factor authentication removed{% endtrans %}
+ + {% if event.additional.method == "webauthn" %} + {% trans %}Method: Security device (WebAuthn){% endtrans %}
+ {% trans %}Device name:{% endtrans %} {{ event.additional.label }} + {% elif event.additional.method == "totp" %} + {% trans %}Method: Authentication application (TOTP){% endtrans %} + {% endif %} +
+ + {% elif event.tag == EventTag.Account.RecoveryCodesGenerated %} + {% trans %}Recovery codes generated{% endtrans %}
+ + + {% elif event.tag == EventTag.Account.RecoveryCodesRegenerated %} + {% trans %}Recovery codes regenerated{% endtrans %}
+ + + {% elif event.tag == EventTag.Account.RecoveryCodesUsed %} + {% trans %}Recovery code used for login{% endtrans %}
+ + + + + {% elif event.tag == EventTag.Account.APITokenAdded %} + {% trans %}API token added{% endtrans %}
+ + {% trans %}Token name:{% endtrans %} {{ event.additional.description }}
+ {# + NOTE: Old events contain a single caveat dictionary, rather than a list of caveats. + + This check can be deleted roughly 90 days after merge, since events older than + 90 days are not presented to the user. + #} + {% if event.additional.caveats is mapping %} + {{ caveat_detail(event.additional.caveats) }} + {% else %} + {% for caveat in event.additional.caveats %} + {{ caveat_detail(caveat) }} + {% endfor %} + {% endif %} +
+ + {% elif event.tag == EventTag.Account.APITokenRemoved %} + {% trans %}API token removed{% endtrans %}
+ {% trans %}Unique identifier:{% endtrans %} {{ event.additional.macaroon_id }} + + {% elif event.tag == EventTag.Account.APITokenRemovedLeak %} + {% trans %}API token automatically removed for security reasons{% endtrans %}
+ + {% trans %}Token name:{% endtrans %} {{ event.additional.description }}
+ {% trans %}Unique identifier:{% endtrans %} {{ event.additional.macaroon_id }}
+ {% if event.additional.permissions == "user" %} + {% trans %}Token scope: entire account{% endtrans %} + {% else %} + {% trans project_name=event.additional.permissions.projects[0] %}Token scope: Project {{ project_name }}{% endtrans %} + {% endif %}
+ {% trans public_url=event.additional.public_url %}Reason: Token found at public url{% endtrans %} +
+ + {% elif event.tag == EventTag.Account.OrganizationRoleInvite %} + + {% trans href=request.route_path('organizations.profile', organization=event.additional.organization_name), organization_name=event.additional.organization_name, role_name=event.additional.role_name|lower %}Invited to join {{ organization_name }}{% endtrans %} + + {% elif event.tag == EventTag.Account.OrganizationRoleDeclineInvite %} + + {% trans href=request.route_path('organizations.profile', organization=event.additional.organization_name), organization_name=event.additional.organization_name, role_name=event.additional.role_name|lower %}Invitation to join {{ organization_name }} declined{% endtrans %} + + {% elif event.tag == EventTag.Account.OrganizationRoleRevokeInvite %} + + {% trans href=request.route_path('organizations.profile', organization=event.additional.organization_name), organization_name=event.additional.organization_name, role_name=event.additional.role_name|lower %}Invitation to join {{ organization_name }} revoked{% endtrans %} + + {% elif event.tag == EventTag.Account.OrganizationRoleExpireInvite %} + + {% trans href=request.route_path('organizations.profile', organization=event.additional.organization_name), organization_name=event.additional.organization_name, role_name=event.additional.role_name|lower %}Invitation to join {{ organization_name }} expired{% endtrans %} + + + {% else %} + {{ event.tag }} + {% endif %} + {%- endmacro %} + +

+ {% trans faq_url=request.help_url(_anchor='suspicious-activity')%} + Events appear here as security-related actions occur on your account. If you notice anything suspicious, please secure your account as soon as possible. + {% endtrans %} +

+ + + + + + + + + {% for event in recent_events %} + + + + + + {% endfor %} + +
{% trans %}Recent account activity{% endtrans %}
{% trans %}Event{% endtrans %}{% trans %}Time{% endtrans %}{% trans %}Additional Info{% endtrans %}
{{ event_summary(event) }} + {% trans %}Date / time{% endtrans %} + {{ humanize(event.time, time="true") }} + + {% trans %}Location Info{% endtrans %} + {{ "Redacted" if event.additional.redact_ip else event.location_info }}
+ {% trans %}Device Info{% endtrans %} + {{ event.user_agent_info }} +
+ {% else %} +

{% trans %}Events will appear here as security-related actions occur on your account.{% endtrans %}

+ {% endif %} +
+{% endblock %} + +{% block extra_js %} +{% endblock %} diff --git a/warehouse/views.py b/warehouse/views.py index 6442831ea777..ef620fa68e53 100644 --- a/warehouse/views.py +++ b/warehouse/views.py @@ -146,7 +146,7 @@ def forbidden(exc, request): queue="error", ) url = request.route_url( - "manage.account", + "manage.unverified-account", _query={REDIRECT_FIELD_NAME: request.path_qs}, ) return HTTPSeeOther(url) From a69c5b8849221d92f2e84055f6cbd544230136cc Mon Sep 17 00:00:00 2001 From: Dustin Ingram Date: Mon, 1 Apr 2024 12:50:30 +0000 Subject: [PATCH 05/21] Add emails to unverified template --- .../templates/manage/unverified-account.html | 44 ++++++++++++++++++- 1 file changed, 42 insertions(+), 2 deletions(-) diff --git a/warehouse/templates/manage/unverified-account.html b/warehouse/templates/manage/unverified-account.html index 93c2ddcbc8da..7c80ba478d9d 100644 --- a/warehouse/templates/manage/unverified-account.html +++ b/warehouse/templates/manage/unverified-account.html @@ -14,7 +14,7 @@ {% extends "manage_base.html" %} {% set user = request.user %} -{% set title = gettext("Account settings") %} +{% set title = gettext("Activate your account") %} {% set active_tab = 'account' %} @@ -89,7 +89,7 @@ {% endif %} From 4d25de92c7444c353a979d42f11f9cbb78199269 Mon Sep 17 00:00:00 2001 From: Dustin Ingram Date: Mon, 1 Apr 2024 13:16:24 +0000 Subject: [PATCH 09/21] Update language --- warehouse/templates/manage/unverified-account.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/warehouse/templates/manage/unverified-account.html b/warehouse/templates/manage/unverified-account.html index 0df5fec667c4..a6f32e08866f 100644 --- a/warehouse/templates/manage/unverified-account.html +++ b/warehouse/templates/manage/unverified-account.html @@ -204,7 +204,7 @@

{{ title }}

{% trans %}Account emails{% endtrans %}

- {% trans %}You can associate several emails with your account. You can use any Verified email to recover your account, but only your Primary email will receive notifications.{% endtrans %} + {% trans %} You must have at least one Verified email address to activate your account.{% endtrans %}

{# Sort the emails as follows: From 190b022faea2f901e160f8541e8a2fc03c8e0a7d Mon Sep 17 00:00:00 2001 From: Dustin Ingram Date: Mon, 1 Apr 2024 13:16:48 +0000 Subject: [PATCH 10/21] Update translations --- warehouse/locale/messages.pot | 296 ++++++++++++++++++---------------- 1 file changed, 156 insertions(+), 140 deletions(-) diff --git a/warehouse/locale/messages.pot b/warehouse/locale/messages.pot index 0b5530743a1f..15fd24ae7f90 100644 --- a/warehouse/locale/messages.pot +++ b/warehouse/locale/messages.pot @@ -147,7 +147,7 @@ msgstr "" msgid "Successful WebAuthn assertion" msgstr "" -#: warehouse/accounts/views.py:570 warehouse/manage/views/__init__.py:859 +#: warehouse/accounts/views.py:570 warehouse/manage/views/__init__.py:862 msgid "Recovery code accepted. The supplied code cannot be used again." msgstr "" @@ -281,7 +281,7 @@ msgid "You are now ${role} of the '${project_name}' project." msgstr "" #: warehouse/accounts/views.py:1557 warehouse/accounts/views.py:1800 -#: warehouse/manage/views/__init__.py:1236 +#: warehouse/manage/views/__init__.py:1239 msgid "" "Trusted publishing is temporarily disabled. See https://pypi.org/help" "#admin-intervention for details." @@ -301,19 +301,19 @@ msgstr "" msgid "You can't register more than 3 pending trusted publishers at once." msgstr "" -#: warehouse/accounts/views.py:1623 warehouse/manage/views/__init__.py:1271 -#: warehouse/manage/views/__init__.py:1384 -#: warehouse/manage/views/__init__.py:1496 -#: warehouse/manage/views/__init__.py:1606 +#: warehouse/accounts/views.py:1623 warehouse/manage/views/__init__.py:1274 +#: warehouse/manage/views/__init__.py:1387 +#: warehouse/manage/views/__init__.py:1499 +#: warehouse/manage/views/__init__.py:1609 msgid "" "There have been too many attempted trusted publisher registrations. Try " "again later." msgstr "" -#: warehouse/accounts/views.py:1634 warehouse/manage/views/__init__.py:1285 -#: warehouse/manage/views/__init__.py:1398 -#: warehouse/manage/views/__init__.py:1510 -#: warehouse/manage/views/__init__.py:1620 +#: warehouse/accounts/views.py:1634 warehouse/manage/views/__init__.py:1288 +#: warehouse/manage/views/__init__.py:1401 +#: warehouse/manage/views/__init__.py:1513 +#: warehouse/manage/views/__init__.py:1623 msgid "The trusted publisher could not be registered" msgstr "" @@ -423,130 +423,130 @@ msgstr "" msgid "This team name has already been used. Choose a different team name." msgstr "" -#: warehouse/manage/views/__init__.py:271 +#: warehouse/manage/views/__init__.py:274 msgid "Account details updated" msgstr "" -#: warehouse/manage/views/__init__.py:301 +#: warehouse/manage/views/__init__.py:304 msgid "Email ${email_address} added - check your email for a verification link" msgstr "" -#: warehouse/manage/views/__init__.py:807 +#: warehouse/manage/views/__init__.py:810 msgid "Recovery codes already generated" msgstr "" -#: warehouse/manage/views/__init__.py:808 +#: warehouse/manage/views/__init__.py:811 msgid "Generating new recovery codes will invalidate your existing codes." msgstr "" -#: warehouse/manage/views/__init__.py:917 +#: warehouse/manage/views/__init__.py:920 msgid "Verify your email to create an API token." msgstr "" -#: warehouse/manage/views/__init__.py:1017 +#: warehouse/manage/views/__init__.py:1020 msgid "API Token does not exist." msgstr "" -#: warehouse/manage/views/__init__.py:1049 +#: warehouse/manage/views/__init__.py:1052 msgid "Invalid credentials. Try again" msgstr "" -#: warehouse/manage/views/__init__.py:1252 +#: warehouse/manage/views/__init__.py:1255 msgid "" "GitHub-based trusted publishing is temporarily disabled. See " "https://pypi.org/help#admin-intervention for details." msgstr "" -#: warehouse/manage/views/__init__.py:1365 +#: warehouse/manage/views/__init__.py:1368 msgid "" "GitLab-based trusted publishing is temporarily disabled. See " "https://pypi.org/help#admin-intervention for details." msgstr "" -#: warehouse/manage/views/__init__.py:1477 +#: warehouse/manage/views/__init__.py:1480 msgid "" "Google-based trusted publishing is temporarily disabled. See " "https://pypi.org/help#admin-intervention for details." msgstr "" -#: warehouse/manage/views/__init__.py:1586 +#: warehouse/manage/views/__init__.py:1589 msgid "" "ActiveState-based trusted publishing is temporarily disabled. See " "https://pypi.org/help#admin-intervention for details." msgstr "" -#: warehouse/manage/views/__init__.py:1821 -#: warehouse/manage/views/__init__.py:2122 -#: warehouse/manage/views/__init__.py:2230 +#: warehouse/manage/views/__init__.py:1824 +#: warehouse/manage/views/__init__.py:2125 +#: warehouse/manage/views/__init__.py:2233 msgid "" "Project deletion temporarily disabled. See https://pypi.org/help#admin-" "intervention for details." msgstr "" -#: warehouse/manage/views/__init__.py:1953 -#: warehouse/manage/views/__init__.py:2038 -#: warehouse/manage/views/__init__.py:2139 -#: warehouse/manage/views/__init__.py:2239 +#: warehouse/manage/views/__init__.py:1956 +#: warehouse/manage/views/__init__.py:2041 +#: warehouse/manage/views/__init__.py:2142 +#: warehouse/manage/views/__init__.py:2242 msgid "Confirm the request" msgstr "" -#: warehouse/manage/views/__init__.py:1965 +#: warehouse/manage/views/__init__.py:1968 msgid "Could not yank release - " msgstr "" -#: warehouse/manage/views/__init__.py:2050 +#: warehouse/manage/views/__init__.py:2053 msgid "Could not un-yank release - " msgstr "" -#: warehouse/manage/views/__init__.py:2151 +#: warehouse/manage/views/__init__.py:2154 msgid "Could not delete release - " msgstr "" -#: warehouse/manage/views/__init__.py:2251 +#: warehouse/manage/views/__init__.py:2254 msgid "Could not find file" msgstr "" -#: warehouse/manage/views/__init__.py:2255 +#: warehouse/manage/views/__init__.py:2258 msgid "Could not delete file - " msgstr "" -#: warehouse/manage/views/__init__.py:2405 +#: warehouse/manage/views/__init__.py:2408 msgid "Team '${team_name}' already has ${role_name} role for project" msgstr "" -#: warehouse/manage/views/__init__.py:2512 +#: warehouse/manage/views/__init__.py:2515 msgid "User '${username}' already has ${role_name} role for project" msgstr "" -#: warehouse/manage/views/__init__.py:2579 +#: warehouse/manage/views/__init__.py:2582 msgid "${username} is now ${role} of the '${project_name}' project." msgstr "" -#: warehouse/manage/views/__init__.py:2611 +#: warehouse/manage/views/__init__.py:2614 msgid "" "User '${username}' does not have a verified primary email address and " "cannot be added as a ${role_name} for project" msgstr "" -#: warehouse/manage/views/__init__.py:2624 +#: warehouse/manage/views/__init__.py:2627 #: warehouse/manage/views/organizations.py:878 msgid "User '${username}' already has an active invite. Please try again later." msgstr "" -#: warehouse/manage/views/__init__.py:2689 +#: warehouse/manage/views/__init__.py:2692 #: warehouse/manage/views/organizations.py:943 msgid "Invitation sent to '${username}'" msgstr "" -#: warehouse/manage/views/__init__.py:2722 +#: warehouse/manage/views/__init__.py:2725 msgid "Could not find role invitation." msgstr "" -#: warehouse/manage/views/__init__.py:2733 +#: warehouse/manage/views/__init__.py:2736 msgid "Invitation already expired." msgstr "" -#: warehouse/manage/views/__init__.py:2765 +#: warehouse/manage/views/__init__.py:2768 #: warehouse/manage/views/organizations.py:1130 msgid "Invitation revoked from '${username}'." msgstr "" @@ -1650,7 +1650,7 @@ msgstr "" #: warehouse/templates/accounts/register.html:47 #: warehouse/templates/manage/account.html:139 #: warehouse/templates/manage/account.html:517 -#: warehouse/templates/manage/unverified-account.html:136 +#: warehouse/templates/manage/unverified-account.html:112 msgid "Name" msgstr "" @@ -1660,6 +1660,7 @@ msgstr "" #: warehouse/templates/accounts/register.html:73 #: warehouse/templates/manage/account.html:348 +#: warehouse/templates/manage/unverified-account.html:221 msgid "Email address" msgstr "" @@ -2808,9 +2809,9 @@ msgstr "" #: warehouse/templates/manage/project/release.html:143 #: warehouse/templates/manage/project/releases.html:178 #: warehouse/templates/manage/project/settings.html:69 -#: warehouse/templates/manage/unverified-account.html:196 -#: warehouse/templates/manage/unverified-account.html:198 -#: warehouse/templates/manage/unverified-account.html:208 +#: warehouse/templates/manage/unverified-account.html:172 +#: warehouse/templates/manage/unverified-account.html:174 +#: warehouse/templates/manage/unverified-account.html:184 #: warehouse/templates/search/results.html:198 msgid "Close" msgstr "" @@ -2840,7 +2841,6 @@ msgstr "" #: warehouse/templates/manage/account/two-factor.html:17 #: warehouse/templates/manage/manage_base.html:253 #: warehouse/templates/manage/manage_base.html:287 -#: warehouse/templates/manage/unverified-account.html:17 msgid "Account settings" msgstr "" @@ -2882,7 +2882,7 @@ msgstr "" #: warehouse/templates/manage/account.html:206 #: warehouse/templates/manage/account/recovery_codes-provision.html:58 #: warehouse/templates/manage/account/totp-provision.html:57 -#: warehouse/templates/manage/unverified-account.html:203 +#: warehouse/templates/manage/unverified-account.html:179 #: warehouse/templates/packaging/detail.html:158 #: warehouse/templates/pages/classifiers.html:38 msgid "Copy to clipboard" @@ -2894,7 +2894,7 @@ msgstr "" #: warehouse/templates/manage/account.html:207 #: warehouse/templates/manage/account/recovery_codes-provision.html:59 #: warehouse/templates/manage/account/totp-provision.html:58 -#: warehouse/templates/manage/unverified-account.html:204 +#: warehouse/templates/manage/unverified-account.html:180 #: warehouse/templates/pages/classifiers.html:39 msgid "Copy" msgstr "" @@ -3005,7 +3005,7 @@ msgstr "" #: warehouse/templates/manage/organization/history.html:23 #: warehouse/templates/manage/project/history.html:23 #: warehouse/templates/manage/team/history.html:23 -#: warehouse/templates/manage/unverified-account.html:220 +#: warehouse/templates/manage/unverified-account.html:236 msgid "Security history" msgstr "" @@ -3194,7 +3194,7 @@ msgstr "" #: warehouse/templates/manage/project/release.html:86 #: warehouse/templates/manage/project/releases.html:68 #: warehouse/templates/manage/unverified-account.html:84 -#: warehouse/templates/manage/unverified-account.html:160 +#: warehouse/templates/manage/unverified-account.html:136 msgid "Options" msgstr "" @@ -3212,67 +3212,63 @@ msgid "Resend verification email" msgstr "" #: warehouse/templates/manage/account.html:110 -#: warehouse/templates/manage/unverified-account.html:107 msgid "Set this email address as primary" msgstr "" #: warehouse/templates/manage/account.html:112 -#: warehouse/templates/manage/unverified-account.html:109 msgid "Set as primary" msgstr "" #: warehouse/templates/manage/account.html:122 -#: warehouse/templates/manage/unverified-account.html:119 msgid "Remove this email address" msgstr "" #: warehouse/templates/manage/account.html:124 -#: warehouse/templates/manage/unverified-account.html:121 msgid "Remove email" msgstr "" #: warehouse/templates/manage/account.html:143 #: warehouse/templates/manage/account.html:518 #: warehouse/templates/manage/account/token.html:148 -#: warehouse/templates/manage/unverified-account.html:140 +#: warehouse/templates/manage/unverified-account.html:116 msgid "Scope" msgstr "" #: warehouse/templates/manage/account.html:145 -#: warehouse/templates/manage/unverified-account.html:142 +#: warehouse/templates/manage/unverified-account.html:118 msgid "All projects" msgstr "" #: warehouse/templates/manage/account.html:153 #: warehouse/templates/manage/account.html:519 -#: warehouse/templates/manage/unverified-account.html:150 +#: warehouse/templates/manage/unverified-account.html:126 msgid "Created" msgstr "" #: warehouse/templates/manage/account.html:157 #: warehouse/templates/manage/account.html:520 -#: warehouse/templates/manage/unverified-account.html:154 +#: warehouse/templates/manage/unverified-account.html:130 msgid "Last used" msgstr "" #: warehouse/templates/manage/account.html:158 -#: warehouse/templates/manage/unverified-account.html:155 +#: warehouse/templates/manage/unverified-account.html:131 msgid "Never" msgstr "" #: warehouse/templates/manage/account.html:162 -#: warehouse/templates/manage/unverified-account.html:159 +#: warehouse/templates/manage/unverified-account.html:135 msgid "View token options" msgstr "" #: warehouse/templates/manage/account.html:172 #: warehouse/templates/manage/account/token.html:57 -#: warehouse/templates/manage/unverified-account.html:169 +#: warehouse/templates/manage/unverified-account.html:145 msgid "Remove token" msgstr "" #: warehouse/templates/manage/account.html:178 -#: warehouse/templates/manage/unverified-account.html:175 +#: warehouse/templates/manage/unverified-account.html:151 msgid "View unique identifier" msgstr "" @@ -3280,21 +3276,21 @@ msgstr "" #: warehouse/templates/manage/account.html:188 #: warehouse/templates/manage/account/token.html:60 #: warehouse/templates/manage/account/token.html:61 -#: warehouse/templates/manage/unverified-account.html:183 -#: warehouse/templates/manage/unverified-account.html:185 +#: warehouse/templates/manage/unverified-account.html:159 +#: warehouse/templates/manage/unverified-account.html:161 msgid "Remove API token" msgstr "" #: warehouse/templates/manage/account.html:193 #: warehouse/templates/manage/account/token.html:66 -#: warehouse/templates/manage/unverified-account.html:190 +#: warehouse/templates/manage/unverified-account.html:166 msgid "" "Applications or scripts using this token will no longer have access to " "PyPI." msgstr "" #: warehouse/templates/manage/account.html:204 -#: warehouse/templates/manage/unverified-account.html:201 +#: warehouse/templates/manage/unverified-account.html:177 #, python-format msgid "Unique identifier for API token \"%(token_description)s\"" msgstr "" @@ -3366,6 +3362,7 @@ msgid "Update account" msgstr "" #: warehouse/templates/manage/account.html:332 +#: warehouse/templates/manage/unverified-account.html:205 msgid "Account emails" msgstr "" @@ -3379,10 +3376,12 @@ msgid "" msgstr "" #: warehouse/templates/manage/account.html:345 +#: warehouse/templates/manage/unverified-account.html:218 msgid "Emails associated with your account" msgstr "" #: warehouse/templates/manage/account.html:349 +#: warehouse/templates/manage/unverified-account.html:222 msgid "Status" msgstr "" @@ -3462,209 +3461,209 @@ msgstr "" #: warehouse/templates/manage/account.html:550 #: warehouse/templates/manage/account.html:729 -#: warehouse/templates/manage/unverified-account.html:228 -#: warehouse/templates/manage/unverified-account.html:407 +#: warehouse/templates/manage/unverified-account.html:244 +#: warehouse/templates/manage/unverified-account.html:423 msgid "Token scope: entire account" msgstr "" #: warehouse/templates/manage/account.html:552 #: warehouse/templates/manage/account.html:731 -#: warehouse/templates/manage/unverified-account.html:230 -#: warehouse/templates/manage/unverified-account.html:409 +#: warehouse/templates/manage/unverified-account.html:246 +#: warehouse/templates/manage/unverified-account.html:425 #, python-format msgid "Token scope: Project %(project_name)s" msgstr "" #: warehouse/templates/manage/account.html:555 -#: warehouse/templates/manage/unverified-account.html:233 +#: warehouse/templates/manage/unverified-account.html:249 #, python-format msgid "Expires: %(exp)s" msgstr "" #: warehouse/templates/manage/account.html:561 -#: warehouse/templates/manage/unverified-account.html:239 +#: warehouse/templates/manage/unverified-account.html:255 msgid "Account created" msgstr "" #: warehouse/templates/manage/account.html:564 -#: warehouse/templates/manage/unverified-account.html:242 +#: warehouse/templates/manage/unverified-account.html:258 msgid "Logged in" msgstr "" #: warehouse/templates/manage/account.html:566 -#: warehouse/templates/manage/unverified-account.html:244 +#: warehouse/templates/manage/unverified-account.html:260 msgid "Two factor method:" msgstr "" #: warehouse/templates/manage/account.html:568 #: warehouse/templates/manage/project/release.html:77 -#: warehouse/templates/manage/unverified-account.html:246 +#: warehouse/templates/manage/unverified-account.html:262 msgid "None" msgstr "" #: warehouse/templates/manage/account.html:570 #: warehouse/templates/manage/manage_base.html:92 -#: warehouse/templates/manage/unverified-account.html:248 +#: warehouse/templates/manage/unverified-account.html:264 msgid "Security device (WebAuthn)" msgstr "" #: warehouse/templates/manage/account.html:572 #: warehouse/templates/manage/manage_base.html:70 -#: warehouse/templates/manage/unverified-account.html:250 +#: warehouse/templates/manage/unverified-account.html:266 msgid "" "Authentication application (TOTP)" msgstr "" #: warehouse/templates/manage/account.html:574 -#: warehouse/templates/manage/unverified-account.html:252 +#: warehouse/templates/manage/unverified-account.html:268 msgid "Recovery code" msgstr "" #: warehouse/templates/manage/account.html:579 -#: warehouse/templates/manage/unverified-account.html:257 +#: warehouse/templates/manage/unverified-account.html:273 msgid "Login failed" msgstr "" #: warehouse/templates/manage/account.html:582 -#: warehouse/templates/manage/unverified-account.html:260 +#: warehouse/templates/manage/unverified-account.html:276 msgid "- Basic Auth (Upload endpoint)" msgstr "" #: warehouse/templates/manage/account.html:587 #: warehouse/templates/manage/account.html:606 #: warehouse/templates/manage/project/history.html:272 -#: warehouse/templates/manage/unverified-account.html:265 -#: warehouse/templates/manage/unverified-account.html:284 +#: warehouse/templates/manage/unverified-account.html:281 +#: warehouse/templates/manage/unverified-account.html:300 msgid "Reason:" msgstr "" #: warehouse/templates/manage/account.html:589 #: warehouse/templates/manage/account.html:608 -#: warehouse/templates/manage/unverified-account.html:267 -#: warehouse/templates/manage/unverified-account.html:286 +#: warehouse/templates/manage/unverified-account.html:283 +#: warehouse/templates/manage/unverified-account.html:302 msgid "Incorrect Password" msgstr "" #: warehouse/templates/manage/account.html:591 -#: warehouse/templates/manage/unverified-account.html:269 +#: warehouse/templates/manage/unverified-account.html:285 msgid "Invalid two factor (TOTP)" msgstr "" #: warehouse/templates/manage/account.html:593 -#: warehouse/templates/manage/unverified-account.html:271 +#: warehouse/templates/manage/unverified-account.html:287 msgid "Invalid two factor (WebAuthn)" msgstr "" #: warehouse/templates/manage/account.html:595 #: warehouse/templates/manage/account.html:597 -#: warehouse/templates/manage/unverified-account.html:273 -#: warehouse/templates/manage/unverified-account.html:275 +#: warehouse/templates/manage/unverified-account.html:289 +#: warehouse/templates/manage/unverified-account.html:291 msgid "Invalid two factor (Recovery code)" msgstr "" #: warehouse/templates/manage/account.html:604 -#: warehouse/templates/manage/unverified-account.html:282 +#: warehouse/templates/manage/unverified-account.html:298 msgid "Session reauthentication failed" msgstr "" #: warehouse/templates/manage/account.html:615 -#: warehouse/templates/manage/unverified-account.html:293 +#: warehouse/templates/manage/unverified-account.html:309 msgid "Email added to account" msgstr "" #: warehouse/templates/manage/account.html:618 -#: warehouse/templates/manage/unverified-account.html:296 +#: warehouse/templates/manage/unverified-account.html:312 msgid "Email removed from account" msgstr "" #: warehouse/templates/manage/account.html:621 -#: warehouse/templates/manage/unverified-account.html:299 +#: warehouse/templates/manage/unverified-account.html:315 msgid "Email verified" msgstr "" #: warehouse/templates/manage/account.html:624 -#: warehouse/templates/manage/unverified-account.html:302 +#: warehouse/templates/manage/unverified-account.html:318 msgid "Email reverified" msgstr "" #: warehouse/templates/manage/account.html:628 -#: warehouse/templates/manage/unverified-account.html:306 +#: warehouse/templates/manage/unverified-account.html:322 msgid "Primary email changed" msgstr "" #: warehouse/templates/manage/account.html:630 -#: warehouse/templates/manage/unverified-account.html:308 +#: warehouse/templates/manage/unverified-account.html:324 msgid "Old primary email:" msgstr "" #: warehouse/templates/manage/account.html:631 -#: warehouse/templates/manage/unverified-account.html:309 +#: warehouse/templates/manage/unverified-account.html:325 msgid "New primary email:" msgstr "" #: warehouse/templates/manage/account.html:634 -#: warehouse/templates/manage/unverified-account.html:312 +#: warehouse/templates/manage/unverified-account.html:328 msgid "Primary email set" msgstr "" #: warehouse/templates/manage/account.html:640 -#: warehouse/templates/manage/unverified-account.html:318 +#: warehouse/templates/manage/unverified-account.html:334 msgid "Email sent" msgstr "" #: warehouse/templates/manage/account.html:642 -#: warehouse/templates/manage/unverified-account.html:320 +#: warehouse/templates/manage/unverified-account.html:336 msgid "From:" msgstr "" #: warehouse/templates/manage/account.html:643 -#: warehouse/templates/manage/unverified-account.html:321 +#: warehouse/templates/manage/unverified-account.html:337 msgid "To:" msgstr "" #: warehouse/templates/manage/account.html:644 -#: warehouse/templates/manage/unverified-account.html:322 +#: warehouse/templates/manage/unverified-account.html:338 msgid "Subject:" msgstr "" #: warehouse/templates/manage/account.html:648 -#: warehouse/templates/manage/unverified-account.html:326 +#: warehouse/templates/manage/unverified-account.html:342 msgid "Password reset requested" msgstr "" #: warehouse/templates/manage/account.html:650 -#: warehouse/templates/manage/unverified-account.html:328 +#: warehouse/templates/manage/unverified-account.html:344 msgid "Password reset attempted" msgstr "" #: warehouse/templates/manage/account.html:652 -#: warehouse/templates/manage/unverified-account.html:330 +#: warehouse/templates/manage/unverified-account.html:346 msgid "Password successfully reset" msgstr "" #: warehouse/templates/manage/account.html:654 -#: warehouse/templates/manage/unverified-account.html:332 +#: warehouse/templates/manage/unverified-account.html:348 msgid "Password successfully changed" msgstr "" #: warehouse/templates/manage/account.html:658 #: warehouse/templates/manage/account.html:663 #: warehouse/templates/manage/account/token.html:158 -#: warehouse/templates/manage/unverified-account.html:336 -#: warehouse/templates/manage/unverified-account.html:341 +#: warehouse/templates/manage/unverified-account.html:352 +#: warehouse/templates/manage/unverified-account.html:357 msgid "Project:" msgstr "" #: warehouse/templates/manage/account.html:666 -#: warehouse/templates/manage/unverified-account.html:344 +#: warehouse/templates/manage/unverified-account.html:360 msgid "Two factor authentication added" msgstr "" #: warehouse/templates/manage/account.html:669 #: warehouse/templates/manage/account.html:679 -#: warehouse/templates/manage/unverified-account.html:347 -#: warehouse/templates/manage/unverified-account.html:357 +#: warehouse/templates/manage/unverified-account.html:363 +#: warehouse/templates/manage/unverified-account.html:373 msgid "" "Method: Security device (WebAuthn)" @@ -3672,42 +3671,42 @@ msgstr "" #: warehouse/templates/manage/account.html:670 #: warehouse/templates/manage/account.html:680 -#: warehouse/templates/manage/unverified-account.html:348 -#: warehouse/templates/manage/unverified-account.html:358 +#: warehouse/templates/manage/unverified-account.html:364 +#: warehouse/templates/manage/unverified-account.html:374 msgid "Device name:" msgstr "" #: warehouse/templates/manage/account.html:672 #: warehouse/templates/manage/account.html:682 -#: warehouse/templates/manage/unverified-account.html:350 -#: warehouse/templates/manage/unverified-account.html:360 +#: warehouse/templates/manage/unverified-account.html:366 +#: warehouse/templates/manage/unverified-account.html:376 msgid "" "Method: Authentication application (TOTP)" msgstr "" #: warehouse/templates/manage/account.html:676 -#: warehouse/templates/manage/unverified-account.html:354 +#: warehouse/templates/manage/unverified-account.html:370 msgid "Two factor authentication removed" msgstr "" #: warehouse/templates/manage/account.html:687 -#: warehouse/templates/manage/unverified-account.html:365 +#: warehouse/templates/manage/unverified-account.html:381 msgid "Recovery codes generated" msgstr "" #: warehouse/templates/manage/account.html:691 -#: warehouse/templates/manage/unverified-account.html:369 +#: warehouse/templates/manage/unverified-account.html:385 msgid "Recovery codes regenerated" msgstr "" #: warehouse/templates/manage/account.html:695 -#: warehouse/templates/manage/unverified-account.html:373 +#: warehouse/templates/manage/unverified-account.html:389 msgid "Recovery code used for login" msgstr "" #: warehouse/templates/manage/account.html:701 -#: warehouse/templates/manage/unverified-account.html:379 +#: warehouse/templates/manage/unverified-account.html:395 msgid "API token added" msgstr "" @@ -3715,61 +3714,61 @@ msgstr "" #: warehouse/templates/manage/account.html:726 #: warehouse/templates/manage/project/history.html:263 #: warehouse/templates/manage/project/history.html:270 -#: warehouse/templates/manage/unverified-account.html:381 -#: warehouse/templates/manage/unverified-account.html:404 +#: warehouse/templates/manage/unverified-account.html:397 +#: warehouse/templates/manage/unverified-account.html:420 msgid "Token name:" msgstr "" #: warehouse/templates/manage/account.html:720 #: warehouse/templates/manage/project/history.html:265 -#: warehouse/templates/manage/unverified-account.html:398 +#: warehouse/templates/manage/unverified-account.html:414 msgid "API token removed" msgstr "" #: warehouse/templates/manage/account.html:721 #: warehouse/templates/manage/account.html:727 -#: warehouse/templates/manage/unverified-account.html:399 -#: warehouse/templates/manage/unverified-account.html:405 +#: warehouse/templates/manage/unverified-account.html:415 +#: warehouse/templates/manage/unverified-account.html:421 msgid "Unique identifier:" msgstr "" #: warehouse/templates/manage/account.html:724 -#: warehouse/templates/manage/unverified-account.html:402 +#: warehouse/templates/manage/unverified-account.html:418 msgid "API token automatically removed for security reasons" msgstr "" #: warehouse/templates/manage/account.html:733 -#: warehouse/templates/manage/unverified-account.html:411 +#: warehouse/templates/manage/unverified-account.html:427 #, python-format msgid "Reason: Token found at public url" msgstr "" #: warehouse/templates/manage/account.html:738 -#: warehouse/templates/manage/unverified-account.html:416 +#: warehouse/templates/manage/unverified-account.html:432 #, python-format msgid "Invited to join %(organization_name)s" msgstr "" #: warehouse/templates/manage/account.html:742 -#: warehouse/templates/manage/unverified-account.html:420 +#: warehouse/templates/manage/unverified-account.html:436 #, python-format msgid "Invitation to join %(organization_name)s declined" msgstr "" #: warehouse/templates/manage/account.html:746 -#: warehouse/templates/manage/unverified-account.html:424 +#: warehouse/templates/manage/unverified-account.html:440 #, python-format msgid "Invitation to join %(organization_name)s revoked" msgstr "" #: warehouse/templates/manage/account.html:750 -#: warehouse/templates/manage/unverified-account.html:428 +#: warehouse/templates/manage/unverified-account.html:444 #, python-format msgid "Invitation to join %(organization_name)s expired" msgstr "" #: warehouse/templates/manage/account.html:759 -#: warehouse/templates/manage/unverified-account.html:437 +#: warehouse/templates/manage/unverified-account.html:453 #, python-format msgid "" "Events appear here as security-related actions occur on your account. If " @@ -3778,7 +3777,7 @@ msgid "" msgstr "" #: warehouse/templates/manage/account.html:764 -#: warehouse/templates/manage/unverified-account.html:442 +#: warehouse/templates/manage/unverified-account.html:458 msgid "Recent account activity" msgstr "" @@ -3786,7 +3785,7 @@ msgstr "" #: warehouse/templates/manage/organization/history.html:201 #: warehouse/templates/manage/project/history.html:304 #: warehouse/templates/manage/team/history.html:108 -#: warehouse/templates/manage/unverified-account.html:444 +#: warehouse/templates/manage/unverified-account.html:460 msgid "Event" msgstr "" @@ -3797,25 +3796,25 @@ msgstr "" #: warehouse/templates/manage/project/history.html:314 #: warehouse/templates/manage/team/history.html:109 #: warehouse/templates/manage/team/history.html:118 -#: warehouse/templates/manage/unverified-account.html:445 +#: warehouse/templates/manage/unverified-account.html:461 msgid "Time" msgstr "" #: warehouse/templates/manage/account.html:768 #: warehouse/templates/manage/organization/history.html:203 #: warehouse/templates/manage/team/history.html:110 -#: warehouse/templates/manage/unverified-account.html:446 +#: warehouse/templates/manage/unverified-account.html:462 msgid "Additional Info" msgstr "" #: warehouse/templates/manage/account.html:775 -#: warehouse/templates/manage/unverified-account.html:453 +#: warehouse/templates/manage/unverified-account.html:469 msgid "Date / time" msgstr "" #: warehouse/templates/manage/account.html:779 #: warehouse/templates/manage/organization/history.html:215 -#: warehouse/templates/manage/unverified-account.html:457 +#: warehouse/templates/manage/unverified-account.html:473 msgid "Location Info" msgstr "" @@ -3823,12 +3822,12 @@ msgstr "" #: warehouse/templates/manage/organization/history.html:217 #: warehouse/templates/manage/project/history.html:320 #: warehouse/templates/manage/team/history.html:124 -#: warehouse/templates/manage/unverified-account.html:459 +#: warehouse/templates/manage/unverified-account.html:475 msgid "Device Info" msgstr "" #: warehouse/templates/manage/account.html:789 -#: warehouse/templates/manage/unverified-account.html:467 +#: warehouse/templates/manage/unverified-account.html:483 msgid "Events will appear here as security-related actions occur on your account." msgstr "" @@ -4451,6 +4450,23 @@ msgid "" "rel=\"noopener\">Python Packaging User Guide" msgstr "" +#: warehouse/templates/manage/unverified-account.html:17 +msgid "Activate your account" +msgstr "" + +#: warehouse/templates/manage/unverified-account.html:197 +msgid "" +"You must verify a primary email address before making any other changes " +"to your account." +msgstr "" + +#: warehouse/templates/manage/unverified-account.html:207 +msgid "" +"You must have at least one Verified email " +"address to activate your account." +msgstr "" + #: warehouse/templates/manage/account/publishing.html:27 #: warehouse/templates/manage/project/publishing.html:25 #, python-format From 4a17f3a58c63e421c5a497a175b8a44825c87f34 Mon Sep 17 00:00:00 2001 From: Dustin Ingram Date: Mon, 1 Apr 2024 13:25:01 +0000 Subject: [PATCH 11/21] Revert template changes --- warehouse/templates/manage/account.html | 43 ++----------------------- 1 file changed, 3 insertions(+), 40 deletions(-) diff --git a/warehouse/templates/manage/account.html b/warehouse/templates/manage/account.html index 33e26e446628..a7604fa78c4a 100644 --- a/warehouse/templates/manage/account.html +++ b/warehouse/templates/manage/account.html @@ -260,18 +260,6 @@

{% trans %}Account details{% endtrans %}

- {% if not user.has_primary_verified_email %} -
-

- {% trans href='#account-emails' %} - Verify your primary email address - before before updating your account. - {% endtrans %} -

-
- {% endif %} - - {% set disabled = not user.has_primary_verified_email %}
{{ form_errors(save_account_form) }} @@ -321,7 +309,7 @@

{% trans %}Account details{% endtrans %}

{% endtrans %}

- +
@@ -357,18 +345,6 @@

{% trans %}Account emails{% endtrans %}

- {% if not user.has_primary_verified_email %} -
-

- {% trans href='#account-emails' %} - Verify your primary email address - before before adding or removing addresses from your account. - {% endtrans %} -

-
- {% endif %} - - {% set disabled = not user.has_primary_verified_email %}
@@ -390,7 +366,7 @@

{% trans %}Account emails{% endtrans %}

{{ field_errors(add_email_form.email) }}
- +
@@ -399,19 +375,6 @@

{% trans %}Account emails{% endtrans %}

{% trans %}Change password{% endtrans %}

{{ form_error_anchor(change_password_form) }} - - {% if not user.has_primary_verified_email %} -
-

- {% trans href='#account-emails' %} - Verify your primary email address - before before changing your password. - {% endtrans %} -

-
- {% endif %} - - {% set disabled = not user.has_primary_verified_email %}
{{ form_errors(change_password_form) }} @@ -496,7 +459,7 @@

{% trans %}Change password{% endtrans %}

- +
From 1a8839c4f1f93024d6758f43757b6c8b1a4d2778 Mon Sep 17 00:00:00 2001 From: Dustin Ingram Date: Mon, 1 Apr 2024 13:54:21 +0000 Subject: [PATCH 12/21] Testing --- tests/unit/manage/test_views.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/tests/unit/manage/test_views.py b/tests/unit/manage/test_views.py index 34f5b2f8dc38..c03860583a08 100644 --- a/tests/unit/manage/test_views.py +++ b/tests/unit/manage/test_views.py @@ -95,6 +95,21 @@ ) +class TestManageUnverifiedAccount: + + def test_manage_account(self, monkeypatch): + user_service = pretend.stub() + name = pretend.stub() + request = pretend.stub( + find_service=lambda *a, **kw: user_service, user=pretend.stub(name=name) + ) + view = views.ManageUnverifiedAccountViews(request) + + assert view.manage_unverified_account() == {} + assert view.request == request + assert view.user_service == user_service + + class TestManageAccount: @pytest.mark.parametrize( "public_email, expected_public_email", From 542164f3718c928cb9cdc5ae05fbced8541fa26e Mon Sep 17 00:00:00 2001 From: Dustin Ingram Date: Mon, 1 Apr 2024 13:54:29 +0000 Subject: [PATCH 13/21] Branch the redirect --- tests/unit/manage/test_views.py | 46 +++++++++++++++++++++++------- warehouse/manage/views/__init__.py | 5 +++- 2 files changed, 39 insertions(+), 12 deletions(-) diff --git a/tests/unit/manage/test_views.py b/tests/unit/manage/test_views.py index c03860583a08..b2c593a37c97 100644 --- a/tests/unit/manage/test_views.py +++ b/tests/unit/manage/test_views.py @@ -569,13 +569,27 @@ def test_change_primary_email_not_found(self, monkeypatch, db_request): ] assert old_primary.primary - def test_reverify_email(self, monkeypatch): + @pytest.mark.parametrize( + "has_primary_verified_email, expected_redirect", + [ + (True, "manage.account"), + (False, "manage.unverified-account"), + ], + ) + def test_reverify_email( + self, monkeypatch, has_primary_verified_email, expected_redirect + ): + user = pretend.stub( + id=pretend.stub(), + username="username", + name="Name", + record_event=pretend.call_recorder(lambda *a, **kw: None), + has_primary_verified_email=has_primary_verified_email, + ) email = pretend.stub( verified=False, email="email_address", - user=pretend.stub( - record_event=pretend.call_recorder(lambda *a, **kw: None) - ), + user=user, ) request = pretend.stub( @@ -592,7 +606,7 @@ def test_reverify_email(self, monkeypatch): hit=pretend.call_recorder(lambda user_id: None), ) }.get(svc, pretend.stub()), - user=pretend.stub(id=pretend.stub(), username="username", name="Name"), + user=user, remote_addr="0.0.0.0", path="request-path", route_path=pretend.call_recorder(lambda *a, **kw: "/foo/bar/"), @@ -609,21 +623,28 @@ def test_reverify_email(self, monkeypatch): pretend.call("Verification email for email_address resent", queue="success") ] assert send_email.calls == [pretend.call(request, (request.user, email))] - assert email.user.record_event.calls == [ + assert user.record_event.calls == [ pretend.call( tag=EventTag.Account.EmailReverify, request=request, additional={"email": email.email}, ) ] + assert request.route_path.calls == [pretend.call(expected_redirect)] def test_reverify_email_ratelimit_exceeded(self, monkeypatch): + user = pretend.stub( + id=pretend.stub(), + username="username", + name="Name", + record_event=pretend.call_recorder(lambda *a, **kw: None), + has_primary_verified_email=True, + ) + email = pretend.stub( verified=False, email="email_address", - user=pretend.stub( - record_event=pretend.call_recorder(lambda *a, **kw: None) - ), + user=user, ) request = pretend.stub( @@ -639,7 +660,7 @@ def test_reverify_email_ratelimit_exceeded(self, monkeypatch): test=pretend.call_recorder(lambda user_id: False), ) }.get(svc, pretend.stub()), - user=pretend.stub(id=pretend.stub(), username="username", name="Name"), + user=user, remote_addr="0.0.0.0", path="request-path", route_path=pretend.call_recorder(lambda *a, **kw: "/foo/bar/"), @@ -705,7 +726,10 @@ def test_reverify_email_already_verified(self, monkeypatch): ), session=pretend.stub(flash=pretend.call_recorder(lambda *a, **kw: None)), find_service=lambda *a, **kw: pretend.stub(), - user=pretend.stub(id=pretend.stub()), + user=pretend.stub( + id=pretend.stub(), + has_primary_verified_email=True, + ), path="request-path", route_path=pretend.call_recorder(lambda *a, **kw: "/foo/bar/"), ) diff --git a/warehouse/manage/views/__init__.py b/warehouse/manage/views/__init__.py index 1fcafb8eab83..debdc03c225d 100644 --- a/warehouse/manage/views/__init__.py +++ b/warehouse/manage/views/__init__.py @@ -192,7 +192,10 @@ def reverify_email(self): queue="error", ) - return HTTPSeeOther(self.request.route_path("manage.account")) + if self.request.user.has_primary_verified_email: + return HTTPSeeOther(self.request.route_path("manage.account")) + else: + return HTTPSeeOther(self.request.route_path("manage.unverified-account")) @view_defaults( From fd369033bb1a7187d0843dd941faf3e4ea622db5 Mon Sep 17 00:00:00 2001 From: Dustin Ingram Date: Mon, 1 Apr 2024 14:02:26 +0000 Subject: [PATCH 14/21] Update translations ya dummy --- warehouse/locale/messages.pot | 349 ++++++++++++++++------------------ 1 file changed, 164 insertions(+), 185 deletions(-) diff --git a/warehouse/locale/messages.pot b/warehouse/locale/messages.pot index 15fd24ae7f90..4a476968b3df 100644 --- a/warehouse/locale/messages.pot +++ b/warehouse/locale/messages.pot @@ -147,7 +147,7 @@ msgstr "" msgid "Successful WebAuthn assertion" msgstr "" -#: warehouse/accounts/views.py:570 warehouse/manage/views/__init__.py:862 +#: warehouse/accounts/views.py:570 warehouse/manage/views/__init__.py:865 msgid "Recovery code accepted. The supplied code cannot be used again." msgstr "" @@ -281,7 +281,7 @@ msgid "You are now ${role} of the '${project_name}' project." msgstr "" #: warehouse/accounts/views.py:1557 warehouse/accounts/views.py:1800 -#: warehouse/manage/views/__init__.py:1239 +#: warehouse/manage/views/__init__.py:1242 msgid "" "Trusted publishing is temporarily disabled. See https://pypi.org/help" "#admin-intervention for details." @@ -301,19 +301,19 @@ msgstr "" msgid "You can't register more than 3 pending trusted publishers at once." msgstr "" -#: warehouse/accounts/views.py:1623 warehouse/manage/views/__init__.py:1274 -#: warehouse/manage/views/__init__.py:1387 -#: warehouse/manage/views/__init__.py:1499 -#: warehouse/manage/views/__init__.py:1609 +#: warehouse/accounts/views.py:1623 warehouse/manage/views/__init__.py:1277 +#: warehouse/manage/views/__init__.py:1390 +#: warehouse/manage/views/__init__.py:1502 +#: warehouse/manage/views/__init__.py:1612 msgid "" "There have been too many attempted trusted publisher registrations. Try " "again later." msgstr "" -#: warehouse/accounts/views.py:1634 warehouse/manage/views/__init__.py:1288 -#: warehouse/manage/views/__init__.py:1401 -#: warehouse/manage/views/__init__.py:1513 -#: warehouse/manage/views/__init__.py:1623 +#: warehouse/accounts/views.py:1634 warehouse/manage/views/__init__.py:1291 +#: warehouse/manage/views/__init__.py:1404 +#: warehouse/manage/views/__init__.py:1516 +#: warehouse/manage/views/__init__.py:1626 msgid "The trusted publisher could not be registered" msgstr "" @@ -423,130 +423,130 @@ msgstr "" msgid "This team name has already been used. Choose a different team name." msgstr "" -#: warehouse/manage/views/__init__.py:274 +#: warehouse/manage/views/__init__.py:277 msgid "Account details updated" msgstr "" -#: warehouse/manage/views/__init__.py:304 +#: warehouse/manage/views/__init__.py:307 msgid "Email ${email_address} added - check your email for a verification link" msgstr "" -#: warehouse/manage/views/__init__.py:810 +#: warehouse/manage/views/__init__.py:813 msgid "Recovery codes already generated" msgstr "" -#: warehouse/manage/views/__init__.py:811 +#: warehouse/manage/views/__init__.py:814 msgid "Generating new recovery codes will invalidate your existing codes." msgstr "" -#: warehouse/manage/views/__init__.py:920 +#: warehouse/manage/views/__init__.py:923 msgid "Verify your email to create an API token." msgstr "" -#: warehouse/manage/views/__init__.py:1020 +#: warehouse/manage/views/__init__.py:1023 msgid "API Token does not exist." msgstr "" -#: warehouse/manage/views/__init__.py:1052 +#: warehouse/manage/views/__init__.py:1055 msgid "Invalid credentials. Try again" msgstr "" -#: warehouse/manage/views/__init__.py:1255 +#: warehouse/manage/views/__init__.py:1258 msgid "" "GitHub-based trusted publishing is temporarily disabled. See " "https://pypi.org/help#admin-intervention for details." msgstr "" -#: warehouse/manage/views/__init__.py:1368 +#: warehouse/manage/views/__init__.py:1371 msgid "" "GitLab-based trusted publishing is temporarily disabled. See " "https://pypi.org/help#admin-intervention for details." msgstr "" -#: warehouse/manage/views/__init__.py:1480 +#: warehouse/manage/views/__init__.py:1483 msgid "" "Google-based trusted publishing is temporarily disabled. See " "https://pypi.org/help#admin-intervention for details." msgstr "" -#: warehouse/manage/views/__init__.py:1589 +#: warehouse/manage/views/__init__.py:1592 msgid "" "ActiveState-based trusted publishing is temporarily disabled. See " "https://pypi.org/help#admin-intervention for details." msgstr "" -#: warehouse/manage/views/__init__.py:1824 -#: warehouse/manage/views/__init__.py:2125 -#: warehouse/manage/views/__init__.py:2233 +#: warehouse/manage/views/__init__.py:1827 +#: warehouse/manage/views/__init__.py:2128 +#: warehouse/manage/views/__init__.py:2236 msgid "" "Project deletion temporarily disabled. See https://pypi.org/help#admin-" "intervention for details." msgstr "" -#: warehouse/manage/views/__init__.py:1956 -#: warehouse/manage/views/__init__.py:2041 -#: warehouse/manage/views/__init__.py:2142 -#: warehouse/manage/views/__init__.py:2242 +#: warehouse/manage/views/__init__.py:1959 +#: warehouse/manage/views/__init__.py:2044 +#: warehouse/manage/views/__init__.py:2145 +#: warehouse/manage/views/__init__.py:2245 msgid "Confirm the request" msgstr "" -#: warehouse/manage/views/__init__.py:1968 +#: warehouse/manage/views/__init__.py:1971 msgid "Could not yank release - " msgstr "" -#: warehouse/manage/views/__init__.py:2053 +#: warehouse/manage/views/__init__.py:2056 msgid "Could not un-yank release - " msgstr "" -#: warehouse/manage/views/__init__.py:2154 +#: warehouse/manage/views/__init__.py:2157 msgid "Could not delete release - " msgstr "" -#: warehouse/manage/views/__init__.py:2254 +#: warehouse/manage/views/__init__.py:2257 msgid "Could not find file" msgstr "" -#: warehouse/manage/views/__init__.py:2258 +#: warehouse/manage/views/__init__.py:2261 msgid "Could not delete file - " msgstr "" -#: warehouse/manage/views/__init__.py:2408 +#: warehouse/manage/views/__init__.py:2411 msgid "Team '${team_name}' already has ${role_name} role for project" msgstr "" -#: warehouse/manage/views/__init__.py:2515 +#: warehouse/manage/views/__init__.py:2518 msgid "User '${username}' already has ${role_name} role for project" msgstr "" -#: warehouse/manage/views/__init__.py:2582 +#: warehouse/manage/views/__init__.py:2585 msgid "${username} is now ${role} of the '${project_name}' project." msgstr "" -#: warehouse/manage/views/__init__.py:2614 +#: warehouse/manage/views/__init__.py:2617 msgid "" "User '${username}' does not have a verified primary email address and " "cannot be added as a ${role_name} for project" msgstr "" -#: warehouse/manage/views/__init__.py:2627 +#: warehouse/manage/views/__init__.py:2630 #: warehouse/manage/views/organizations.py:878 msgid "User '${username}' already has an active invite. Please try again later." msgstr "" -#: warehouse/manage/views/__init__.py:2692 +#: warehouse/manage/views/__init__.py:2695 #: warehouse/manage/views/organizations.py:943 msgid "Invitation sent to '${username}'" msgstr "" -#: warehouse/manage/views/__init__.py:2725 +#: warehouse/manage/views/__init__.py:2728 msgid "Could not find role invitation." msgstr "" -#: warehouse/manage/views/__init__.py:2736 +#: warehouse/manage/views/__init__.py:2739 msgid "Invitation already expired." msgstr "" -#: warehouse/manage/views/__init__.py:2768 +#: warehouse/manage/views/__init__.py:2771 #: warehouse/manage/views/organizations.py:1130 msgid "Invitation revoked from '${username}'." msgstr "" @@ -1020,7 +1020,7 @@ msgstr "" #: warehouse/templates/base.html:191 #: warehouse/templates/includes/flash-messages.html:30 #: warehouse/templates/includes/session-notifications.html:20 -#: warehouse/templates/manage/account.html:827 +#: warehouse/templates/manage/account.html:790 #: warehouse/templates/manage/manage_base.html:340 #: warehouse/templates/manage/manage_base.html:399 #: warehouse/templates/manage/organization/settings.html:218 @@ -1310,7 +1310,7 @@ msgstr "" #: warehouse/templates/accounts/register.html:179 #: warehouse/templates/accounts/reset-password.html:71 #: warehouse/templates/accounts/reset-password.html:77 -#: warehouse/templates/manage/account.html:479 +#: warehouse/templates/manage/account.html:442 #: warehouse/templates/re-auth.html:18 warehouse/templates/re-auth.html:77 msgid "Confirm password" msgstr "" @@ -1339,12 +1339,12 @@ msgstr "" #: warehouse/templates/accounts/reset-password.html:40 #: warehouse/templates/accounts/reset-password.html:73 #: warehouse/templates/accounts/two-factor.html:101 -#: warehouse/templates/manage/account.html:282 -#: warehouse/templates/manage/account.html:306 -#: warehouse/templates/manage/account.html:378 -#: warehouse/templates/manage/account.html:423 -#: warehouse/templates/manage/account.html:448 -#: warehouse/templates/manage/account.html:475 +#: warehouse/templates/manage/account.html:270 +#: warehouse/templates/manage/account.html:294 +#: warehouse/templates/manage/account.html:354 +#: warehouse/templates/manage/account.html:386 +#: warehouse/templates/manage/account.html:411 +#: warehouse/templates/manage/account.html:438 #: warehouse/templates/manage/account/publishing.html:40 #: warehouse/templates/manage/account/publishing.html:55 #: warehouse/templates/manage/account/publishing.html:70 @@ -1649,7 +1649,7 @@ msgstr "" #: warehouse/templates/accounts/register.html:47 #: warehouse/templates/manage/account.html:139 -#: warehouse/templates/manage/account.html:517 +#: warehouse/templates/manage/account.html:480 #: warehouse/templates/manage/unverified-account.html:112 msgid "Name" msgstr "" @@ -1659,13 +1659,13 @@ msgid "Your name" msgstr "" #: warehouse/templates/accounts/register.html:73 -#: warehouse/templates/manage/account.html:348 +#: warehouse/templates/manage/account.html:336 #: warehouse/templates/manage/unverified-account.html:221 msgid "Email address" msgstr "" #: warehouse/templates/accounts/register.html:79 -#: warehouse/templates/manage/account.html:382 +#: warehouse/templates/manage/account.html:358 msgid "Your email address" msgstr "" @@ -1679,7 +1679,7 @@ msgstr "" #: warehouse/templates/accounts/register.html:143 #: warehouse/templates/accounts/reset-password.html:44 -#: warehouse/templates/manage/account.html:428 +#: warehouse/templates/manage/account.html:391 msgid "Show passwords" msgstr "" @@ -1739,7 +1739,7 @@ msgid "Reset your password" msgstr "" #: warehouse/templates/accounts/reset-password.html:48 -#: warehouse/templates/manage/account.html:454 +#: warehouse/templates/manage/account.html:417 msgid "Select a new password" msgstr "" @@ -3001,7 +3001,7 @@ msgstr "" #: warehouse/templates/includes/manage/manage-organization-menu.html:41 #: warehouse/templates/includes/manage/manage-project-menu.html:37 #: warehouse/templates/includes/manage/manage-team-menu.html:34 -#: warehouse/templates/manage/account.html:542 +#: warehouse/templates/manage/account.html:505 #: warehouse/templates/manage/organization/history.html:23 #: warehouse/templates/manage/project/history.html:23 #: warehouse/templates/manage/team/history.html:23 @@ -3228,7 +3228,7 @@ msgid "Remove email" msgstr "" #: warehouse/templates/manage/account.html:143 -#: warehouse/templates/manage/account.html:518 +#: warehouse/templates/manage/account.html:481 #: warehouse/templates/manage/account/token.html:148 #: warehouse/templates/manage/unverified-account.html:116 msgid "Scope" @@ -3240,13 +3240,13 @@ msgid "All projects" msgstr "" #: warehouse/templates/manage/account.html:153 -#: warehouse/templates/manage/account.html:519 +#: warehouse/templates/manage/account.html:482 #: warehouse/templates/manage/unverified-account.html:126 msgid "Created" msgstr "" #: warehouse/templates/manage/account.html:157 -#: warehouse/templates/manage/account.html:520 +#: warehouse/templates/manage/account.html:483 #: warehouse/templates/manage/unverified-account.html:130 msgid "Last used" msgstr "" @@ -3326,47 +3326,40 @@ msgid "" "changed." msgstr "" -#: warehouse/templates/manage/account.html:266 -#, python-format -msgid "" -"Verify your primary email address before before " -"updating your account." -msgstr "" - -#: warehouse/templates/manage/account.html:280 +#: warehouse/templates/manage/account.html:268 msgid "Full name" msgstr "" -#: warehouse/templates/manage/account.html:286 +#: warehouse/templates/manage/account.html:274 msgid "No name set" msgstr "" -#: warehouse/templates/manage/account.html:297 +#: warehouse/templates/manage/account.html:285 #, python-format msgid "Displayed on your public profile" msgstr "" -#: warehouse/templates/manage/account.html:304 +#: warehouse/templates/manage/account.html:292 msgid "️Public email" msgstr "" -#: warehouse/templates/manage/account.html:319 +#: warehouse/templates/manage/account.html:307 #, python-format msgid "" "One of your verified emails can be displayed on your public profile to logged-in users." msgstr "" -#: warehouse/templates/manage/account.html:324 +#: warehouse/templates/manage/account.html:312 msgid "Update account" msgstr "" -#: warehouse/templates/manage/account.html:332 +#: warehouse/templates/manage/account.html:320 #: warehouse/templates/manage/unverified-account.html:205 msgid "Account emails" msgstr "" -#: warehouse/templates/manage/account.html:334 +#: warehouse/templates/manage/account.html:322 msgid "" "You can associate several emails with your account. You can use any Verify your primary email address before before " -"adding or removing addresses from your account." -msgstr "" - -#: warehouse/templates/manage/account.html:376 -#: warehouse/templates/manage/account.html:393 +#: warehouse/templates/manage/account.html:352 +#: warehouse/templates/manage/account.html:369 msgid "Add email" msgstr "" -#: warehouse/templates/manage/account.html:400 +#: warehouse/templates/manage/account.html:376 msgid "Change password" msgstr "" -#: warehouse/templates/manage/account.html:406 -#, python-format -msgid "" -"Verify your primary email address before before " -"changing your password." -msgstr "" - -#: warehouse/templates/manage/account.html:421 +#: warehouse/templates/manage/account.html:384 msgid "Old password" msgstr "" -#: warehouse/templates/manage/account.html:432 +#: warehouse/templates/manage/account.html:395 msgid "Your current password" msgstr "" -#: warehouse/templates/manage/account.html:446 +#: warehouse/templates/manage/account.html:409 msgid "New password" msgstr "" -#: warehouse/templates/manage/account.html:473 +#: warehouse/templates/manage/account.html:436 msgid "Confirm new password" msgstr "" -#: warehouse/templates/manage/account.html:499 +#: warehouse/templates/manage/account.html:462 msgid "Update password" msgstr "" -#: warehouse/templates/manage/account.html:509 +#: warehouse/templates/manage/account.html:472 #: warehouse/templates/manage/project/settings.html:43 msgid "API tokens" msgstr "" -#: warehouse/templates/manage/account.html:510 +#: warehouse/templates/manage/account.html:473 #: warehouse/templates/manage/project/settings.html:44 msgid "" "API tokens provide an alternative way to authenticate when uploading " "packages to PyPI." msgstr "" -#: warehouse/templates/manage/account.html:510 +#: warehouse/templates/manage/account.html:473 msgid "Learn more about API tokens" msgstr "" -#: warehouse/templates/manage/account.html:514 +#: warehouse/templates/manage/account.html:477 msgid "Active API tokens for this account" msgstr "" -#: warehouse/templates/manage/account.html:532 +#: warehouse/templates/manage/account.html:495 msgid "Add API token" msgstr "" -#: warehouse/templates/manage/account.html:534 +#: warehouse/templates/manage/account.html:497 #, python-format msgid "" "Verify your primary email address to add API " "tokens to your account." msgstr "" -#: warehouse/templates/manage/account.html:550 -#: warehouse/templates/manage/account.html:729 +#: warehouse/templates/manage/account.html:513 +#: warehouse/templates/manage/account.html:692 #: warehouse/templates/manage/unverified-account.html:244 #: warehouse/templates/manage/unverified-account.html:423 msgid "Token scope: entire account" msgstr "" -#: warehouse/templates/manage/account.html:552 -#: warehouse/templates/manage/account.html:731 +#: warehouse/templates/manage/account.html:515 +#: warehouse/templates/manage/account.html:694 #: warehouse/templates/manage/unverified-account.html:246 #: warehouse/templates/manage/unverified-account.html:425 #, python-format msgid "Token scope: Project %(project_name)s" msgstr "" -#: warehouse/templates/manage/account.html:555 +#: warehouse/templates/manage/account.html:518 #: warehouse/templates/manage/unverified-account.html:249 #, python-format msgid "Expires: %(exp)s" msgstr "" -#: warehouse/templates/manage/account.html:561 +#: warehouse/templates/manage/account.html:524 #: warehouse/templates/manage/unverified-account.html:255 msgid "Account created" msgstr "" -#: warehouse/templates/manage/account.html:564 +#: warehouse/templates/manage/account.html:527 #: warehouse/templates/manage/unverified-account.html:258 msgid "Logged in" msgstr "" -#: warehouse/templates/manage/account.html:566 +#: warehouse/templates/manage/account.html:529 #: warehouse/templates/manage/unverified-account.html:260 msgid "Two factor method:" msgstr "" -#: warehouse/templates/manage/account.html:568 +#: warehouse/templates/manage/account.html:531 #: warehouse/templates/manage/project/release.html:77 #: warehouse/templates/manage/unverified-account.html:262 msgid "None" msgstr "" -#: warehouse/templates/manage/account.html:570 +#: warehouse/templates/manage/account.html:533 #: warehouse/templates/manage/manage_base.html:92 #: warehouse/templates/manage/unverified-account.html:264 msgid "Security device (WebAuthn)" msgstr "" -#: warehouse/templates/manage/account.html:572 +#: warehouse/templates/manage/account.html:535 #: warehouse/templates/manage/manage_base.html:70 #: warehouse/templates/manage/unverified-account.html:266 msgid "" @@ -3515,153 +3494,153 @@ msgid "" "password\">TOTP)" msgstr "" -#: warehouse/templates/manage/account.html:574 +#: warehouse/templates/manage/account.html:537 #: warehouse/templates/manage/unverified-account.html:268 msgid "Recovery code" msgstr "" -#: warehouse/templates/manage/account.html:579 +#: warehouse/templates/manage/account.html:542 #: warehouse/templates/manage/unverified-account.html:273 msgid "Login failed" msgstr "" -#: warehouse/templates/manage/account.html:582 +#: warehouse/templates/manage/account.html:545 #: warehouse/templates/manage/unverified-account.html:276 msgid "- Basic Auth (Upload endpoint)" msgstr "" -#: warehouse/templates/manage/account.html:587 -#: warehouse/templates/manage/account.html:606 +#: warehouse/templates/manage/account.html:550 +#: warehouse/templates/manage/account.html:569 #: warehouse/templates/manage/project/history.html:272 #: warehouse/templates/manage/unverified-account.html:281 #: warehouse/templates/manage/unverified-account.html:300 msgid "Reason:" msgstr "" -#: warehouse/templates/manage/account.html:589 -#: warehouse/templates/manage/account.html:608 +#: warehouse/templates/manage/account.html:552 +#: warehouse/templates/manage/account.html:571 #: warehouse/templates/manage/unverified-account.html:283 #: warehouse/templates/manage/unverified-account.html:302 msgid "Incorrect Password" msgstr "" -#: warehouse/templates/manage/account.html:591 +#: warehouse/templates/manage/account.html:554 #: warehouse/templates/manage/unverified-account.html:285 msgid "Invalid two factor (TOTP)" msgstr "" -#: warehouse/templates/manage/account.html:593 +#: warehouse/templates/manage/account.html:556 #: warehouse/templates/manage/unverified-account.html:287 msgid "Invalid two factor (WebAuthn)" msgstr "" -#: warehouse/templates/manage/account.html:595 -#: warehouse/templates/manage/account.html:597 +#: warehouse/templates/manage/account.html:558 +#: warehouse/templates/manage/account.html:560 #: warehouse/templates/manage/unverified-account.html:289 #: warehouse/templates/manage/unverified-account.html:291 msgid "Invalid two factor (Recovery code)" msgstr "" -#: warehouse/templates/manage/account.html:604 +#: warehouse/templates/manage/account.html:567 #: warehouse/templates/manage/unverified-account.html:298 msgid "Session reauthentication failed" msgstr "" -#: warehouse/templates/manage/account.html:615 +#: warehouse/templates/manage/account.html:578 #: warehouse/templates/manage/unverified-account.html:309 msgid "Email added to account" msgstr "" -#: warehouse/templates/manage/account.html:618 +#: warehouse/templates/manage/account.html:581 #: warehouse/templates/manage/unverified-account.html:312 msgid "Email removed from account" msgstr "" -#: warehouse/templates/manage/account.html:621 +#: warehouse/templates/manage/account.html:584 #: warehouse/templates/manage/unverified-account.html:315 msgid "Email verified" msgstr "" -#: warehouse/templates/manage/account.html:624 +#: warehouse/templates/manage/account.html:587 #: warehouse/templates/manage/unverified-account.html:318 msgid "Email reverified" msgstr "" -#: warehouse/templates/manage/account.html:628 +#: warehouse/templates/manage/account.html:591 #: warehouse/templates/manage/unverified-account.html:322 msgid "Primary email changed" msgstr "" -#: warehouse/templates/manage/account.html:630 +#: warehouse/templates/manage/account.html:593 #: warehouse/templates/manage/unverified-account.html:324 msgid "Old primary email:" msgstr "" -#: warehouse/templates/manage/account.html:631 +#: warehouse/templates/manage/account.html:594 #: warehouse/templates/manage/unverified-account.html:325 msgid "New primary email:" msgstr "" -#: warehouse/templates/manage/account.html:634 +#: warehouse/templates/manage/account.html:597 #: warehouse/templates/manage/unverified-account.html:328 msgid "Primary email set" msgstr "" -#: warehouse/templates/manage/account.html:640 +#: warehouse/templates/manage/account.html:603 #: warehouse/templates/manage/unverified-account.html:334 msgid "Email sent" msgstr "" -#: warehouse/templates/manage/account.html:642 +#: warehouse/templates/manage/account.html:605 #: warehouse/templates/manage/unverified-account.html:336 msgid "From:" msgstr "" -#: warehouse/templates/manage/account.html:643 +#: warehouse/templates/manage/account.html:606 #: warehouse/templates/manage/unverified-account.html:337 msgid "To:" msgstr "" -#: warehouse/templates/manage/account.html:644 +#: warehouse/templates/manage/account.html:607 #: warehouse/templates/manage/unverified-account.html:338 msgid "Subject:" msgstr "" -#: warehouse/templates/manage/account.html:648 +#: warehouse/templates/manage/account.html:611 #: warehouse/templates/manage/unverified-account.html:342 msgid "Password reset requested" msgstr "" -#: warehouse/templates/manage/account.html:650 +#: warehouse/templates/manage/account.html:613 #: warehouse/templates/manage/unverified-account.html:344 msgid "Password reset attempted" msgstr "" -#: warehouse/templates/manage/account.html:652 +#: warehouse/templates/manage/account.html:615 #: warehouse/templates/manage/unverified-account.html:346 msgid "Password successfully reset" msgstr "" -#: warehouse/templates/manage/account.html:654 +#: warehouse/templates/manage/account.html:617 #: warehouse/templates/manage/unverified-account.html:348 msgid "Password successfully changed" msgstr "" -#: warehouse/templates/manage/account.html:658 -#: warehouse/templates/manage/account.html:663 +#: warehouse/templates/manage/account.html:621 +#: warehouse/templates/manage/account.html:626 #: warehouse/templates/manage/account/token.html:158 #: warehouse/templates/manage/unverified-account.html:352 #: warehouse/templates/manage/unverified-account.html:357 msgid "Project:" msgstr "" -#: warehouse/templates/manage/account.html:666 +#: warehouse/templates/manage/account.html:629 #: warehouse/templates/manage/unverified-account.html:360 msgid "Two factor authentication added" msgstr "" -#: warehouse/templates/manage/account.html:669 -#: warehouse/templates/manage/account.html:679 +#: warehouse/templates/manage/account.html:632 +#: warehouse/templates/manage/account.html:642 #: warehouse/templates/manage/unverified-account.html:363 #: warehouse/templates/manage/unverified-account.html:373 msgid "" @@ -3669,15 +3648,15 @@ msgid "" "authentication\">WebAuthn)" msgstr "" -#: warehouse/templates/manage/account.html:670 -#: warehouse/templates/manage/account.html:680 +#: warehouse/templates/manage/account.html:633 +#: warehouse/templates/manage/account.html:643 #: warehouse/templates/manage/unverified-account.html:364 #: warehouse/templates/manage/unverified-account.html:374 msgid "Device name:" msgstr "" -#: warehouse/templates/manage/account.html:672 -#: warehouse/templates/manage/account.html:682 +#: warehouse/templates/manage/account.html:635 +#: warehouse/templates/manage/account.html:645 #: warehouse/templates/manage/unverified-account.html:366 #: warehouse/templates/manage/unverified-account.html:376 msgid "" @@ -3685,33 +3664,33 @@ msgid "" "password\">TOTP)" msgstr "" -#: warehouse/templates/manage/account.html:676 +#: warehouse/templates/manage/account.html:639 #: warehouse/templates/manage/unverified-account.html:370 msgid "Two factor authentication removed" msgstr "" -#: warehouse/templates/manage/account.html:687 +#: warehouse/templates/manage/account.html:650 #: warehouse/templates/manage/unverified-account.html:381 msgid "Recovery codes generated" msgstr "" -#: warehouse/templates/manage/account.html:691 +#: warehouse/templates/manage/account.html:654 #: warehouse/templates/manage/unverified-account.html:385 msgid "Recovery codes regenerated" msgstr "" -#: warehouse/templates/manage/account.html:695 +#: warehouse/templates/manage/account.html:658 #: warehouse/templates/manage/unverified-account.html:389 msgid "Recovery code used for login" msgstr "" -#: warehouse/templates/manage/account.html:701 +#: warehouse/templates/manage/account.html:664 #: warehouse/templates/manage/unverified-account.html:395 msgid "API token added" msgstr "" -#: warehouse/templates/manage/account.html:703 -#: warehouse/templates/manage/account.html:726 +#: warehouse/templates/manage/account.html:666 +#: warehouse/templates/manage/account.html:689 #: warehouse/templates/manage/project/history.html:263 #: warehouse/templates/manage/project/history.html:270 #: warehouse/templates/manage/unverified-account.html:397 @@ -3719,55 +3698,55 @@ msgstr "" msgid "Token name:" msgstr "" -#: warehouse/templates/manage/account.html:720 +#: warehouse/templates/manage/account.html:683 #: warehouse/templates/manage/project/history.html:265 #: warehouse/templates/manage/unverified-account.html:414 msgid "API token removed" msgstr "" -#: warehouse/templates/manage/account.html:721 -#: warehouse/templates/manage/account.html:727 +#: warehouse/templates/manage/account.html:684 +#: warehouse/templates/manage/account.html:690 #: warehouse/templates/manage/unverified-account.html:415 #: warehouse/templates/manage/unverified-account.html:421 msgid "Unique identifier:" msgstr "" -#: warehouse/templates/manage/account.html:724 +#: warehouse/templates/manage/account.html:687 #: warehouse/templates/manage/unverified-account.html:418 msgid "API token automatically removed for security reasons" msgstr "" -#: warehouse/templates/manage/account.html:733 +#: warehouse/templates/manage/account.html:696 #: warehouse/templates/manage/unverified-account.html:427 #, python-format msgid "Reason: Token found at public url" msgstr "" -#: warehouse/templates/manage/account.html:738 +#: warehouse/templates/manage/account.html:701 #: warehouse/templates/manage/unverified-account.html:432 #, python-format msgid "Invited to join %(organization_name)s" msgstr "" -#: warehouse/templates/manage/account.html:742 +#: warehouse/templates/manage/account.html:705 #: warehouse/templates/manage/unverified-account.html:436 #, python-format msgid "Invitation to join %(organization_name)s declined" msgstr "" -#: warehouse/templates/manage/account.html:746 +#: warehouse/templates/manage/account.html:709 #: warehouse/templates/manage/unverified-account.html:440 #, python-format msgid "Invitation to join %(organization_name)s revoked" msgstr "" -#: warehouse/templates/manage/account.html:750 +#: warehouse/templates/manage/account.html:713 #: warehouse/templates/manage/unverified-account.html:444 #, python-format msgid "Invitation to join %(organization_name)s expired" msgstr "" -#: warehouse/templates/manage/account.html:759 +#: warehouse/templates/manage/account.html:722 #: warehouse/templates/manage/unverified-account.html:453 #, python-format msgid "" @@ -3776,12 +3755,12 @@ msgid "" "your account as soon as possible." msgstr "" -#: warehouse/templates/manage/account.html:764 +#: warehouse/templates/manage/account.html:727 #: warehouse/templates/manage/unverified-account.html:458 msgid "Recent account activity" msgstr "" -#: warehouse/templates/manage/account.html:766 +#: warehouse/templates/manage/account.html:729 #: warehouse/templates/manage/organization/history.html:201 #: warehouse/templates/manage/project/history.html:304 #: warehouse/templates/manage/team/history.html:108 @@ -3789,7 +3768,7 @@ msgstr "" msgid "Event" msgstr "" -#: warehouse/templates/manage/account.html:767 +#: warehouse/templates/manage/account.html:730 #: warehouse/templates/manage/organization/history.html:202 #: warehouse/templates/manage/organization/history.html:211 #: warehouse/templates/manage/project/history.html:305 @@ -3800,25 +3779,25 @@ msgstr "" msgid "Time" msgstr "" -#: warehouse/templates/manage/account.html:768 +#: warehouse/templates/manage/account.html:731 #: warehouse/templates/manage/organization/history.html:203 #: warehouse/templates/manage/team/history.html:110 #: warehouse/templates/manage/unverified-account.html:462 msgid "Additional Info" msgstr "" -#: warehouse/templates/manage/account.html:775 +#: warehouse/templates/manage/account.html:738 #: warehouse/templates/manage/unverified-account.html:469 msgid "Date / time" msgstr "" -#: warehouse/templates/manage/account.html:779 +#: warehouse/templates/manage/account.html:742 #: warehouse/templates/manage/organization/history.html:215 #: warehouse/templates/manage/unverified-account.html:473 msgid "Location Info" msgstr "" -#: warehouse/templates/manage/account.html:781 +#: warehouse/templates/manage/account.html:744 #: warehouse/templates/manage/organization/history.html:217 #: warehouse/templates/manage/project/history.html:320 #: warehouse/templates/manage/team/history.html:124 @@ -3826,20 +3805,20 @@ msgstr "" msgid "Device Info" msgstr "" -#: warehouse/templates/manage/account.html:789 +#: warehouse/templates/manage/account.html:752 #: warehouse/templates/manage/unverified-account.html:483 msgid "Events will appear here as security-related actions occur on your account." msgstr "" -#: warehouse/templates/manage/account.html:796 +#: warehouse/templates/manage/account.html:759 msgid "Delete account" msgstr "" -#: warehouse/templates/manage/account.html:799 +#: warehouse/templates/manage/account.html:762 msgid "Cannot delete account" msgstr "" -#: warehouse/templates/manage/account.html:801 +#: warehouse/templates/manage/account.html:764 #, python-format msgid "" "Your account is currently the sole owner of %(count)s " @@ -3850,7 +3829,7 @@ msgid_plural "" msgstr[0] "" msgstr[1] "" -#: warehouse/templates/manage/account.html:806 +#: warehouse/templates/manage/account.html:769 msgid "" "You must transfer ownership or delete this project before you can delete " "your account." @@ -3860,14 +3839,14 @@ msgid_plural "" msgstr[0] "" msgstr[1] "" -#: warehouse/templates/manage/account.html:816 +#: warehouse/templates/manage/account.html:779 #, python-format msgid "" "transfer ownership or delete project" msgstr "" -#: warehouse/templates/manage/account.html:825 +#: warehouse/templates/manage/account.html:788 #: warehouse/templates/manage/account/token.html:166 #: warehouse/templates/manage/organization/settings.html:216 #: warehouse/templates/manage/organization/settings.html:278 @@ -3875,11 +3854,11 @@ msgstr "" msgid "Proceed with caution!" msgstr "" -#: warehouse/templates/manage/account.html:828 +#: warehouse/templates/manage/account.html:791 msgid "You will not be able to recover your account after you delete it" msgstr "" -#: warehouse/templates/manage/account.html:830 +#: warehouse/templates/manage/account.html:793 msgid "Delete your PyPI account" msgstr "" From 5085f78c7888e7ff471e9d98a78047b1eac4c317 Mon Sep 17 00:00:00 2001 From: Dustin Ingram Date: Mon, 1 Apr 2024 15:17:40 +0000 Subject: [PATCH 15/21] Permit accounts without verified email to verify email --- warehouse/accounts/security_policy.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/warehouse/accounts/security_policy.py b/warehouse/accounts/security_policy.py index ac9684a3b447..4f0b32f68b60 100644 --- a/warehouse/accounts/security_policy.py +++ b/warehouse/accounts/security_policy.py @@ -185,6 +185,7 @@ def _permits_for_user_policy(acl, request, context, permission): isinstance(res, Allowed) and not request.identity.has_primary_verified_email and not request.matched_route.name.startswith("manage.unverified-account") + and not request.matched_route.name.startswith("manage.verify-email") ): return WarehouseDenied("unverified", reason="unverified_email") @@ -214,6 +215,7 @@ def _check_for_mfa(request, context) -> WarehouseDenied | None: "manage.account.two-factor", "manage.account.webauthn-provision", "manage.unverified-account", + "manage.verify-email", ] if ( From 168233f0ef0c1afd3d3a165efb149f89c4596c20 Mon Sep 17 00:00:00 2001 From: Dustin Ingram Date: Mon, 1 Apr 2024 15:20:39 +0000 Subject: [PATCH 16/21] Don't need startswith --- warehouse/accounts/security_policy.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/warehouse/accounts/security_policy.py b/warehouse/accounts/security_policy.py index 4f0b32f68b60..003eda40bf94 100644 --- a/warehouse/accounts/security_policy.py +++ b/warehouse/accounts/security_policy.py @@ -184,8 +184,8 @@ def _permits_for_user_policy(acl, request, context, permission): if ( isinstance(res, Allowed) and not request.identity.has_primary_verified_email - and not request.matched_route.name.startswith("manage.unverified-account") - and not request.matched_route.name.startswith("manage.verify-email") + and request.matched_route.name + not in {"manage.unverified-account", "manage.verify-email"} ): return WarehouseDenied("unverified", reason="unverified_email") From e23d0cc5013072961fdb40c00b70df5652a7adf3 Mon Sep 17 00:00:00 2001 From: Dustin Ingram Date: Mon, 1 Apr 2024 15:21:33 +0000 Subject: [PATCH 17/21] Fix typo --- warehouse/accounts/security_policy.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/warehouse/accounts/security_policy.py b/warehouse/accounts/security_policy.py index 003eda40bf94..1b0c99bdca4b 100644 --- a/warehouse/accounts/security_policy.py +++ b/warehouse/accounts/security_policy.py @@ -185,7 +185,7 @@ def _permits_for_user_policy(acl, request, context, permission): isinstance(res, Allowed) and not request.identity.has_primary_verified_email and request.matched_route.name - not in {"manage.unverified-account", "manage.verify-email"} + not in {"manage.unverified-account", "accounts.verify-email"} ): return WarehouseDenied("unverified", reason="unverified_email") From c225b42da5e06df820afdf10e47ce863a71a8fc6 Mon Sep 17 00:00:00 2001 From: Dustin Ingram Date: Mon, 1 Apr 2024 15:23:41 +0000 Subject: [PATCH 18/21] Fix typo --- warehouse/accounts/security_policy.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/warehouse/accounts/security_policy.py b/warehouse/accounts/security_policy.py index 1b0c99bdca4b..6465f82139cc 100644 --- a/warehouse/accounts/security_policy.py +++ b/warehouse/accounts/security_policy.py @@ -215,7 +215,7 @@ def _check_for_mfa(request, context) -> WarehouseDenied | None: "manage.account.two-factor", "manage.account.webauthn-provision", "manage.unverified-account", - "manage.verify-email", + "account.verify-email", ] if ( From 0a4a0fc828b3ad4441241d0ef1237cfc65362c67 Mon Sep 17 00:00:00 2001 From: Dustin Ingram Date: Mon, 1 Apr 2024 15:34:55 +0000 Subject: [PATCH 19/21] Remove unnecessary routes --- warehouse/accounts/security_policy.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/warehouse/accounts/security_policy.py b/warehouse/accounts/security_policy.py index 6465f82139cc..1bd2c8cae30f 100644 --- a/warehouse/accounts/security_policy.py +++ b/warehouse/accounts/security_policy.py @@ -214,8 +214,6 @@ def _check_for_mfa(request, context) -> WarehouseDenied | None: "manage.account.totp-provision", "manage.account.two-factor", "manage.account.webauthn-provision", - "manage.unverified-account", - "account.verify-email", ] if ( From 7b345ad0f2f877bf8551cc5b2425ea1615103edd Mon Sep 17 00:00:00 2001 From: Dustin Ingram Date: Mon, 1 Apr 2024 15:39:59 +0000 Subject: [PATCH 20/21] Revert "Remove unnecessary routes" This reverts commit 0a4a0fc828b3ad4441241d0ef1237cfc65362c67. --- warehouse/accounts/security_policy.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/warehouse/accounts/security_policy.py b/warehouse/accounts/security_policy.py index 1bd2c8cae30f..6465f82139cc 100644 --- a/warehouse/accounts/security_policy.py +++ b/warehouse/accounts/security_policy.py @@ -214,6 +214,8 @@ def _check_for_mfa(request, context) -> WarehouseDenied | None: "manage.account.totp-provision", "manage.account.two-factor", "manage.account.webauthn-provision", + "manage.unverified-account", + "account.verify-email", ] if ( From 06b76015e0cae7e39813b40129e862e00a54ff2a Mon Sep 17 00:00:00 2001 From: Dustin Ingram Date: Mon, 1 Apr 2024 15:40:19 +0000 Subject: [PATCH 21/21] Fix typogit diff! --- warehouse/accounts/security_policy.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/warehouse/accounts/security_policy.py b/warehouse/accounts/security_policy.py index 6465f82139cc..7a8af53acc58 100644 --- a/warehouse/accounts/security_policy.py +++ b/warehouse/accounts/security_policy.py @@ -215,7 +215,7 @@ def _check_for_mfa(request, context) -> WarehouseDenied | None: "manage.account.two-factor", "manage.account.webauthn-provision", "manage.unverified-account", - "account.verify-email", + "accounts.verify-email", ] if (