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
This commit is contained in:
Ilya Kreymer 2024-08-07 12:36:06 -07:00 committed by GitHub
parent 41d43ae249
commit 5f53db75ee
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 54 additions and 14 deletions

View File

@ -17,7 +17,7 @@ from pymongo.errors import InvalidName
from .migrations import BaseMigration
CURR_DB_VERSION = "0034"
CURR_DB_VERSION = "0035"
# ============================================================================

View File

@ -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,
)

View File

@ -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

View File

@ -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()

View File

@ -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,

View File

@ -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

View File

@ -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({