Skip to content

Commit 7a61568

Browse files
authored
Ensure email comparisons are case-insensitive, emails stored as lowercase (#2084)
- Add a custom EmailStr type which lowercases the full e-mail, not just the domain. - Ensure EmailStr is used throughout wherever e-mails are used, both for invites and user models - Tests: update to check for lowercase email responses, e-mails returned from APIs are always lowercase - Tests: remove tests where '@' was ur-lencoded, should not be possible since POSTing JSON and no url-decoding is done/expected. E-mails should have '@' present. - Fixes #2083 where invites were rejected due to case differences - CI: pin pymongo dependency due to latest releases update, update python used for CI - bump to 1.11.7
1 parent 1f919de commit 7a61568

15 files changed

+46
-33
lines changed

.github/workflows/k3d-ci.yaml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -81,9 +81,9 @@ jobs:
8181
helm upgrade --install -f ./chart/values.yaml -f ./chart/test/test.yaml btrix ./chart/
8282
8383
- name: Install Python
84-
uses: actions/setup-python@v3
84+
uses: actions/setup-python@v5
8585
with:
86-
python-version: '3.9'
86+
python-version: 3.x
8787

8888
- name: Install Python Libs
8989
run: pip install -r ./backend/test-requirements.txt

.github/workflows/ui-tests-playwright.yml

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,11 +18,11 @@ jobs:
1818
working-directory: ./frontend
1919
steps:
2020
- name: Checkout
21-
uses: actions/checkout@v3
21+
uses: actions/checkout@v4
2222
- name: Setup Node
23-
uses: actions/setup-node@v3
23+
uses: actions/setup-node@v4
2424
with:
25-
node-version: '18'
25+
node-version: '20'
2626
cache: 'yarn'
2727
cache-dependency-path: frontend/yarn.lock
2828
- name: Install dependencies
@@ -43,7 +43,7 @@ jobs:
4343
id: build-frontend
4444
- name: Run Playwright tests
4545
run: cd frontend && yarn playwright test
46-
- uses: actions/upload-artifact@v2
46+
- uses: actions/upload-artifact@v4
4747
if: always()
4848
with:
4949
name: playwright-report

backend/btrixcloud/invites.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313

1414
from .pagination import DEFAULT_PAGE_SIZE
1515
from .models import (
16+
EmailStr,
1617
UserRole,
1718
InvitePending,
1819
InviteRequest,
@@ -133,7 +134,10 @@ async def add_existing_user_invite(
133134
)
134135

135136
async def get_valid_invite(
136-
self, invite_token: UUID, email: Optional[str], userid: Optional[UUID] = None
137+
self,
138+
invite_token: UUID,
139+
email: Optional[EmailStr],
140+
userid: Optional[UUID] = None,
137141
) -> InvitePending:
138142
"""Retrieve a valid invite data from db, or throw if invalid"""
139143
token_hash = get_hash(invite_token)
@@ -156,7 +160,7 @@ async def remove_invite(self, invite_token: UUID) -> None:
156160
await self.invites.delete_one({"_id": invite_token})
157161

158162
async def remove_invite_by_email(
159-
self, email: str, oid: Optional[UUID] = None
163+
self, email: EmailStr, oid: Optional[UUID] = None
160164
) -> Any:
161165
"""remove invite from invite list by email"""
162166
query: dict[str, object] = {"email": email}

backend/btrixcloud/models.py

Lines changed: 17 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,8 @@
1515
Field,
1616
HttpUrl as HttpUrlNonStr,
1717
AnyHttpUrl as AnyHttpUrlNonStr,
18-
EmailStr,
18+
EmailStr as CasedEmailStr,
19+
validate_email,
1920
RootModel,
2021
BeforeValidator,
2122
TypeAdapter,
@@ -47,6 +48,15 @@
4748
]
4849

4950

51+
# pylint: disable=too-few-public-methods
52+
class EmailStr(CasedEmailStr):
53+
"""EmailStr type that lowercases the full email"""
54+
55+
@classmethod
56+
def _validate(cls, value: CasedEmailStr, /) -> CasedEmailStr:
57+
return validate_email(value)[1].lower()
58+
59+
5060
# pylint: disable=invalid-name, too-many-lines
5161
# ============================================================================
5262
class UserRole(IntEnum):
@@ -70,11 +80,11 @@ class InvitePending(BaseMongoModel):
7080
id: UUID
7181
created: datetime
7282
tokenHash: str
73-
inviterEmail: str
83+
inviterEmail: EmailStr
7484
fromSuperuser: Optional[bool] = False
7585
oid: Optional[UUID] = None
7686
role: UserRole = UserRole.VIEWER
77-
email: Optional[str] = ""
87+
email: Optional[EmailStr] = None
7888
# set if existing user
7989
userid: Optional[UUID] = None
8090

@@ -84,21 +94,21 @@ class InviteOut(BaseModel):
8494
"""Single invite output model"""
8595

8696
created: datetime
87-
inviterEmail: str
97+
inviterEmail: EmailStr
8898
inviterName: str
8999
oid: Optional[UUID] = None
90100
orgName: Optional[str] = None
91101
orgSlug: Optional[str] = None
92102
role: UserRole = UserRole.VIEWER
93-
email: Optional[str] = ""
103+
email: Optional[EmailStr] = None
94104
firstOrgAdmin: Optional[bool] = None
95105

96106

97107
# ============================================================================
98108
class InviteRequest(BaseModel):
99109
"""Request to invite another user"""
100110

101-
email: str
111+
email: EmailStr
102112

103113

104114
# ============================================================================
@@ -1179,7 +1189,7 @@ class SubscriptionCreate(BaseModel):
11791189
status: str
11801190
planId: str
11811191

1182-
firstAdminInviteEmail: str
1192+
firstAdminInviteEmail: EmailStr
11831193
quotas: Optional[OrgQuotas] = None
11841194

11851195

backend/btrixcloud/orgs.py

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@
88
import math
99
import os
1010
import time
11-
import urllib.parse
1211

1312
from uuid import UUID, uuid4
1413
from tempfile import NamedTemporaryFile
@@ -1614,9 +1613,7 @@ async def get_pending_org_invites(
16141613
async def delete_invite(
16151614
invite: RemovePendingInvite, org: Organization = Depends(org_owner_dep)
16161615
):
1617-
# URL decode email just in case
1618-
email = urllib.parse.unquote(invite.email)
1619-
result = await user_manager.invites.remove_invite_by_email(email, org.id)
1616+
result = await user_manager.invites.remove_invite_by_email(invite.email, org.id)
16201617
if result.deleted_count > 0:
16211618
return {
16221619
"removed": True,

backend/btrixcloud/users.py

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,6 @@
88

99
from typing import Optional, List, TYPE_CHECKING, cast, Callable
1010

11-
from pydantic import EmailStr
12-
1311
from fastapi import (
1412
Request,
1513
HTTPException,
@@ -22,6 +20,7 @@
2220
from pymongo.collation import Collation
2321

2422
from .models import (
23+
EmailStr,
2524
UserCreate,
2625
UserUpdateEmailName,
2726
UserUpdatePassword,
@@ -685,7 +684,7 @@ async def get_existing_user_invite_info(
685684
return await user_manager.invites.get_invite_out(invite, user_manager, True)
686685

687686
@users_router.get("/invite/{token}", tags=["invites"], response_model=InviteOut)
688-
async def get_invite_info(token: UUID, email: str):
687+
async def get_invite_info(token: UUID, email: EmailStr):
689688
invite = await user_manager.invites.get_valid_invite(token, email)
690689

691690
return await user_manager.invites.get_invite_out(invite, user_manager, True)

backend/btrixcloud/version.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
""" current version """
22

3-
__version__ = "1.11.6"
3+
__version__ = "1.11.7"

backend/requirements.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ gunicorn
22
uvicorn[standard]
33
fastapi==0.103.2
44
motor==3.3.1
5+
pymongo==4.8.0
56
passlib
67
PyJWT==2.8.0
78
pydantic==2.8.2

backend/test/test_org.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -360,16 +360,18 @@ def test_get_pending_org_invites(
360360
361361
# URL encoded email address with comments
362362
(
363-
"user%2Bcomment-encoded-org%40example.com",
363+
"user%2Bcomment-encoded-org@example.com",
364364
365365
),
366366
# User email with diacritic characters
367367
("diacritic-té[email protected]", "diacritic-té[email protected]"),
368368
# User email with encoded diacritic characters
369369
(
370-
"diacritic-t%C3%A9st-encoded-org%40example.com",
370+
"diacritic-t%C3%A9st-encoded-org@example.com",
371371
"diacritic-té[email protected]",
372372
),
373+
# User email with upper case characters, stored as all lowercase
374+
373375
],
374376
)
375377
def test_send_and_accept_org_invite(

backend/test/test_org_subs.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212

1313
VALID_PASSWORD = "ValidPassW0rd!"
1414

15-
invite_email = "test-user@example.com"
15+
invite_email = "test-User@EXample.com"
1616

1717

1818
def test_create_sub_org_invalid_auth(crawler_auth_headers):

backend/test/test_users.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ def test_me_with_orgs(crawler_auth_headers, default_org_id):
5050
assert r.status_code == 200
5151

5252
data = r.json()
53-
assert data["email"] == CRAWLER_USERNAME
53+
assert data["email"] == CRAWLER_USERNAME_LOWERCASE
5454
assert data["id"]
5555
# assert data["is_active"]
5656
assert data["is_superuser"] is False
@@ -102,7 +102,7 @@ def test_login_user_info(admin_auth_headers, crawler_userid, default_org_id):
102102

103103
assert user_info["id"] == crawler_userid
104104
assert user_info["name"] == "new-crawler"
105-
assert user_info["email"] == CRAWLER_USERNAME
105+
assert user_info["email"] == CRAWLER_USERNAME_LOWERCASE
106106
assert user_info["is_superuser"] is False
107107
assert user_info["is_verified"]
108108

chart/Chart.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ type: application
55
icon: https://webrecorder.net/assets/icon.png
66

77
# Browsertrix and Chart Version
8-
version: v1.11.6
8+
version: v1.11.7
99

1010
dependencies:
1111
- name: btrix-admin-logging

chart/values.yaml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,7 @@ default_org: "My Organization"
9191

9292
# API Image
9393
# =========================================
94-
backend_image: "docker.io/webrecorder/browsertrix-backend:1.11.6"
94+
backend_image: "docker.io/webrecorder/browsertrix-backend:1.11.7"
9595
backend_pull_policy: "Always"
9696

9797
backend_password_secret: "PASSWORD!"
@@ -141,7 +141,7 @@ backend_avg_memory_threshold: 95
141141

142142
# Nginx Image
143143
# =========================================
144-
frontend_image: "docker.io/webrecorder/browsertrix-frontend:1.11.6"
144+
frontend_image: "docker.io/webrecorder/browsertrix-frontend:1.11.7"
145145
frontend_pull_policy: "Always"
146146

147147
frontend_cpu: "10m"

frontend/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "browsertrix-frontend",
3-
"version": "1.11.6",
3+
"version": "1.11.7",
44
"main": "index.ts",
55
"license": "AGPL-3.0-or-later",
66
"dependencies": {

version.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
1.11.6
1+
1.11.7

0 commit comments

Comments
 (0)