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 @@
{% if not email.verified %}
-
+ {% if not user.has_primary_verified_email %}
+
+ {% endif %}
+
+ {% set disabled = not user.has_primary_verified_email %}
@@ -345,6 +357,18 @@ {% trans %}Account emails{% endtrans %}
+ {% if not user.has_primary_verified_email %}
+
+ {% endif %}
+
+ {% set disabled = not user.has_primary_verified_email %}
@@ -375,6 +399,19 @@ {% trans %}Account emails{% endtrans %}
{% trans %}Change password{% endtrans %}
{{ form_error_anchor(change_password_form) }}
+
+ {% if not user.has_primary_verified_email %}
+
+ {% endif %}
+
+ {% set disabled = not user.has_primary_verified_email %}
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 %}
+
+
+ {% trans %}Options{% endtrans %}
+
+
+
+
+
+ {% if not email.verified %}
+
+
+
+ {% endif %}
+ {% if not email.primary and email.verified %}
+
+
+
+ {% endif %}
+ {% if user.emails|length > 1 and not email.primary %}
+
+
+
+ {% endif %}
+
+
+ {% 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") }}
+
+
+
+
+ {% trans %}Options{% endtrans %}
+
+
+
+
+
+
+
+ {# 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 #}
+
+
+
+
+ {% trans %}Close{% endtrans %}
+
+
+
{% trans token_description=macaroon.description %}Unique identifier for API token "{{ token_description }}"{% endtrans %}
+
{{ macaroon.id }}
+
+ {% trans %}Copy{% endtrans %}
+
+
+
+
+
+
+
+{%- 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 %}
+
+
+ {% trans %}Recent account activity{% endtrans %}
+
+ {% trans %}Event{% endtrans %}
+ {% trans %}Time{% endtrans %}
+ {% trans %}Additional Info{% endtrans %}
+
+
+ {% for event in recent_events %}
+
+ {{ 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 }}
+
+
+ {% endfor %}
+
+
+ {% 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 @@
{% if not email.verified %}
-
-
+
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 (