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:
parent
41d43ae249
commit
5f53db75ee
@ -17,7 +17,7 @@ from pymongo.errors import InvalidName
|
|||||||
from .migrations import BaseMigration
|
from .migrations import BaseMigration
|
||||||
|
|
||||||
|
|
||||||
CURR_DB_VERSION = "0034"
|
CURR_DB_VERSION = "0035"
|
||||||
|
|
||||||
|
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
|
@ -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,
|
||||||
|
)
|
@ -24,7 +24,6 @@ from pydantic import (
|
|||||||
# from fastapi_users import models as fastapi_users_models
|
# from fastapi_users import models as fastapi_users_models
|
||||||
|
|
||||||
from .db import BaseMongoModel
|
from .db import BaseMongoModel
|
||||||
from .utils import dt_now
|
|
||||||
|
|
||||||
# crawl scale for constraint
|
# crawl scale for constraint
|
||||||
MAX_CRAWL_SCALE = int(os.environ.get("MAX_CRAWL_SCALE", 3))
|
MAX_CRAWL_SCALE = int(os.environ.get("MAX_CRAWL_SCALE", 3))
|
||||||
@ -162,7 +161,7 @@ class FailedLogin(BaseMongoModel):
|
|||||||
Failed login model
|
Failed login model
|
||||||
"""
|
"""
|
||||||
|
|
||||||
attempted: datetime = dt_now()
|
attempted: datetime
|
||||||
email: str
|
email: str
|
||||||
|
|
||||||
# Consecutive failed logins, reset to 0 on successful login or after
|
# Consecutive failed logins, reset to 0 on successful login or after
|
||||||
@ -1996,7 +1995,7 @@ class PageNote(BaseModel):
|
|||||||
|
|
||||||
id: UUID
|
id: UUID
|
||||||
text: str
|
text: str
|
||||||
created: datetime = dt_now()
|
created: datetime
|
||||||
userid: UUID
|
userid: UUID
|
||||||
userName: str
|
userName: str
|
||||||
|
|
||||||
|
@ -329,7 +329,9 @@ class PageOps:
|
|||||||
crawl_id: str,
|
crawl_id: str,
|
||||||
) -> Dict[str, Union[bool, PageNote]]:
|
) -> Dict[str, Union[bool, PageNote]]:
|
||||||
"""Add note to page"""
|
"""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()
|
modified = dt_now()
|
||||||
|
|
||||||
@ -371,7 +373,11 @@ class PageOps:
|
|||||||
raise HTTPException(status_code=404, detail="page_note_not_found")
|
raise HTTPException(status_code=404, detail="page_note_not_found")
|
||||||
|
|
||||||
new_note = PageNote(
|
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()
|
page_notes[matching_index] = new_note.dict()
|
||||||
|
|
||||||
|
@ -37,7 +37,7 @@ from .models import (
|
|||||||
SuccessResponse,
|
SuccessResponse,
|
||||||
)
|
)
|
||||||
from .pagination import DEFAULT_PAGE_SIZE, paginated_format
|
from .pagination import DEFAULT_PAGE_SIZE, paginated_format
|
||||||
from .utils import is_bool
|
from .utils import is_bool, dt_now
|
||||||
|
|
||||||
from .auth import (
|
from .auth import (
|
||||||
init_jwt_auth,
|
init_jwt_auth,
|
||||||
@ -525,13 +525,13 @@ class UserManager:
|
|||||||
|
|
||||||
If a FailedLogin object doesn't already exist, create it
|
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(
|
await self.failed_logins.find_one_and_update(
|
||||||
{"email": email},
|
{"email": email},
|
||||||
{
|
{
|
||||||
"$setOnInsert": failed_login.to_dict(exclude={"count", "attempted"}),
|
"$setOnInsert": failed_login.to_dict(exclude={"count", "attempted"}),
|
||||||
"$set": {"attempted": failed_login.dict(include={"attempted"})},
|
"$set": {"attempted": failed_login.attempted},
|
||||||
"$inc": {"count": 1},
|
"$inc": {"count": 1},
|
||||||
},
|
},
|
||||||
upsert=True,
|
upsert=True,
|
||||||
|
@ -424,16 +424,14 @@ def test_user_change_role(admin_auth_headers, default_org_id):
|
|||||||
|
|
||||||
def test_forgot_password():
|
def test_forgot_password():
|
||||||
r = requests.post(
|
r = requests.post(
|
||||||
f"{API_PREFIX}/auth/forgot-password",
|
f"{API_PREFIX}/auth/forgot-password", json={"email": "no-such-user@example.com"}
|
||||||
json={"email": "no-such-user@example.com"}
|
|
||||||
)
|
)
|
||||||
# always return success for security reasons even if user doesn't exist
|
# always return success for security reasons even if user doesn't exist
|
||||||
assert r.status_code == 202
|
assert r.status_code == 202
|
||||||
detail = r.json()["success"] == True
|
detail = r.json()["success"] == True
|
||||||
|
|
||||||
r = requests.post(
|
r = requests.post(
|
||||||
f"{API_PREFIX}/auth/forgot-password",
|
f"{API_PREFIX}/auth/forgot-password", json={"email": VALID_USER_EMAIL}
|
||||||
json={"email": VALID_USER_EMAIL}
|
|
||||||
)
|
)
|
||||||
assert r.status_code == 202
|
assert r.status_code == 202
|
||||||
detail = r.json()["success"] == True
|
detail = r.json()["success"] == True
|
||||||
|
@ -375,7 +375,7 @@ export class LogInPage extends LiteElement {
|
|||||||
let message = msg("Sorry, invalid username or password");
|
let message = msg("Sorry, invalid username or password");
|
||||||
if (e.statusCode === 429) {
|
if (e.statusCode === 429) {
|
||||||
message = msg(
|
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({
|
this.formStateService.send({
|
||||||
|
Loading…
Reference in New Issue
Block a user