From 5f53db75eeed3c43beffc1be9e73c2adb2c800ea Mon Sep 17 00:00:00 2001 From: Ilya Kreymer Date: Wed, 7 Aug 2024 12:36:06 -0700 Subject: [PATCH] fix resetting of invalid logins: (#2002) * Fixes issue in FailedLogin model: - fix data-model to remove nested 'attempted.attempted' - migrate existing data to remove nested field * Also, avoid setting dt_now() in model as that results in fixed date for all objects: - update FailedLogin to update 'attempted' date on every attempt - also update PageNote object to set date in constructor * Update text for too many logins to make it clear it is set only if its a valid email * fixes #2001 --- backend/btrixcloud/db.py | 2 +- .../migration_0035_fix_failed_logins.py | 37 +++++++++++++++++++ backend/btrixcloud/models.py | 5 +-- backend/btrixcloud/pages.py | 10 ++++- backend/btrixcloud/users.py | 6 +-- backend/test/test_users.py | 6 +-- frontend/src/pages/log-in.ts | 2 +- 7 files changed, 54 insertions(+), 14 deletions(-) create mode 100644 backend/btrixcloud/migrations/migration_0035_fix_failed_logins.py diff --git a/backend/btrixcloud/db.py b/backend/btrixcloud/db.py index 76462563..50c1ead4 100644 --- a/backend/btrixcloud/db.py +++ b/backend/btrixcloud/db.py @@ -17,7 +17,7 @@ from pymongo.errors import InvalidName from .migrations import BaseMigration -CURR_DB_VERSION = "0034" +CURR_DB_VERSION = "0035" # ============================================================================ diff --git a/backend/btrixcloud/migrations/migration_0035_fix_failed_logins.py b/backend/btrixcloud/migrations/migration_0035_fix_failed_logins.py new file mode 100644 index 00000000..a3402cb5 --- /dev/null +++ b/backend/btrixcloud/migrations/migration_0035_fix_failed_logins.py @@ -0,0 +1,37 @@ +""" +Migration 0035 -- fix model for failed logins +""" + +from btrixcloud.migrations import BaseMigration + + +MIGRATION_VERSION = "0035" + + +class Migration(BaseMigration): + """Migration class.""" + + # pylint: disable=unused-argument + def __init__(self, mdb, **kwargs): + super().__init__(mdb, migration_version=MIGRATION_VERSION) + + async def migrate_up(self): + """Perform migration up. + + Set created from attempted.attempted + """ + failed_logins = self.mdb["logins"] + + try: + res = await failed_logins.update_many( + {"attempted.attempted": {"$exists": 1}}, + [{"$set": {"attempted": "$attempted.attempted"}}], + ) + updated = res.modified_count + print(f"{updated} failed logins fixed", flush=True) + # pylint: disable=broad-exception-caught + except Exception as err: + print( + f"Error fixing failed logins: {err}", + flush=True, + ) diff --git a/backend/btrixcloud/models.py b/backend/btrixcloud/models.py index 4f839651..e594038f 100644 --- a/backend/btrixcloud/models.py +++ b/backend/btrixcloud/models.py @@ -24,7 +24,6 @@ from pydantic import ( # from fastapi_users import models as fastapi_users_models from .db import BaseMongoModel -from .utils import dt_now # crawl scale for constraint MAX_CRAWL_SCALE = int(os.environ.get("MAX_CRAWL_SCALE", 3)) @@ -162,7 +161,7 @@ class FailedLogin(BaseMongoModel): Failed login model """ - attempted: datetime = dt_now() + attempted: datetime email: str # Consecutive failed logins, reset to 0 on successful login or after @@ -1996,7 +1995,7 @@ class PageNote(BaseModel): id: UUID text: str - created: datetime = dt_now() + created: datetime userid: UUID userName: str diff --git a/backend/btrixcloud/pages.py b/backend/btrixcloud/pages.py index 0904d2bd..3d842bd6 100644 --- a/backend/btrixcloud/pages.py +++ b/backend/btrixcloud/pages.py @@ -329,7 +329,9 @@ class PageOps: crawl_id: str, ) -> Dict[str, Union[bool, PageNote]]: """Add note to page""" - note = PageNote(id=uuid4(), text=text, userid=user.id, userName=user.name) + note = PageNote( + id=uuid4(), text=text, userid=user.id, userName=user.name, created=dt_now() + ) modified = dt_now() @@ -371,7 +373,11 @@ class PageOps: raise HTTPException(status_code=404, detail="page_note_not_found") new_note = PageNote( - id=note_in.id, text=note_in.text, userid=user.id, userName=user.name + id=note_in.id, + text=note_in.text, + userid=user.id, + userName=user.name, + created=dt_now(), ) page_notes[matching_index] = new_note.dict() diff --git a/backend/btrixcloud/users.py b/backend/btrixcloud/users.py index 8d0895fb..e0d6896b 100644 --- a/backend/btrixcloud/users.py +++ b/backend/btrixcloud/users.py @@ -37,7 +37,7 @@ from .models import ( SuccessResponse, ) from .pagination import DEFAULT_PAGE_SIZE, paginated_format -from .utils import is_bool +from .utils import is_bool, dt_now from .auth import ( init_jwt_auth, @@ -525,13 +525,13 @@ class UserManager: If a FailedLogin object doesn't already exist, create it """ - failed_login = FailedLogin(id=uuid4(), email=email) + failed_login = FailedLogin(id=uuid4(), email=email, attempted=dt_now()) await self.failed_logins.find_one_and_update( {"email": email}, { "$setOnInsert": failed_login.to_dict(exclude={"count", "attempted"}), - "$set": {"attempted": failed_login.dict(include={"attempted"})}, + "$set": {"attempted": failed_login.attempted}, "$inc": {"count": 1}, }, upsert=True, diff --git a/backend/test/test_users.py b/backend/test/test_users.py index 6fda12f4..c9996469 100644 --- a/backend/test/test_users.py +++ b/backend/test/test_users.py @@ -424,16 +424,14 @@ def test_user_change_role(admin_auth_headers, default_org_id): def test_forgot_password(): r = requests.post( - f"{API_PREFIX}/auth/forgot-password", - json={"email": "no-such-user@example.com"} + f"{API_PREFIX}/auth/forgot-password", json={"email": "no-such-user@example.com"} ) # always return success for security reasons even if user doesn't exist assert r.status_code == 202 detail = r.json()["success"] == True r = requests.post( - f"{API_PREFIX}/auth/forgot-password", - json={"email": VALID_USER_EMAIL} + f"{API_PREFIX}/auth/forgot-password", json={"email": VALID_USER_EMAIL} ) assert r.status_code == 202 detail = r.json()["success"] == True diff --git a/frontend/src/pages/log-in.ts b/frontend/src/pages/log-in.ts index 0f0db523..1e8011e0 100644 --- a/frontend/src/pages/log-in.ts +++ b/frontend/src/pages/log-in.ts @@ -375,7 +375,7 @@ export class LogInPage extends LiteElement { let message = msg("Sorry, invalid username or password"); if (e.statusCode === 429) { message = msg( - "Sorry, too many failed login attempts. A reset password link has been sent to your email.", + "Sorry, too many failed login attempts. If this is a valid email, a reset password link has been sent to your email.", ); } this.formStateService.send({