security: implement user lockout (#1552)

* add data-types required for login security

* implement user lockout checking at login

* cleanup legacy patterns

* expose passwords in test_user

* test user lockout after bad attempts

* test user service

* bump alembic version

* save increment to database

* add locked_at to datetime transformer on import

* do proper test cleanup

* implement scheduled task

* spelling

* document env variables

* implement context manager for session

* use context manager

* implement reset script

* cleanup generator

* run generator

* implement API endpoint for resetting locked users

* add button to reset all locked users

* add info when account is locked

* use ignore instead of expect-error
This commit is contained in:
Hayden
2022-08-13 13:18:12 -08:00
committed by GitHub
parent ca64584fd1
commit b3c41a4bd0
35 changed files with 450 additions and 46 deletions

View File

@@ -8,7 +8,9 @@ from mealie.routes._base import BaseAdminController, controller
from mealie.routes._base.mixins import HttpRepo
from mealie.schema.response.pagination import PaginationQuery
from mealie.schema.response.responses import ErrorResponse
from mealie.schema.user.auth import UnlockResults
from mealie.schema.user.user import UserIn, UserOut, UserPagination
from mealie.services.user_services.user_service import UserService
router = APIRouter(prefix="/users", tags=["Admin: Users"])
@@ -17,9 +19,6 @@ router = APIRouter(prefix="/users", tags=["Admin: Users"])
class AdminUserManagementRoutes(BaseAdminController):
@cached_property
def repo(self):
if not self.user:
raise Exception("No user is logged in.")
return self.repos.users
# =======================================================================
@@ -44,6 +43,13 @@ class AdminUserManagementRoutes(BaseAdminController):
data.password = security.hash_password(data.password)
return self.mixins.create_one(data)
@router.post("/unlock", response_model=UnlockResults)
def unlock_users(self, force: bool = False) -> UnlockResults:
user_service = UserService(self.repos)
unlocked = user_service.reset_locked_users(force=force)
return UnlockResults(unlocked=unlocked)
@router.get("/{item_id}", response_model=UserOut)
def get_one(self, item_id: UUID4):
return self.mixins.get_one(item_id)

View File

@@ -10,6 +10,7 @@ from sqlalchemy.orm.session import Session
from mealie.core import security
from mealie.core.dependencies import get_current_user
from mealie.core.security import authenticate_user
from mealie.core.security.security import UserLockedOut
from mealie.db.db_setup import generate_session
from mealie.routes._base.routers import UserAPIRouter
from mealie.schema.user import PrivateUser
@@ -53,7 +54,10 @@ def get_token(data: CustomOAuth2Form = Depends(), session: Session = Depends(gen
email = data.username
password = data.password
user = authenticate_user(session, email, password) # type: ignore
try:
user = authenticate_user(session, email, password) # type: ignore
except UserLockedOut as e:
raise HTTPException(status_code=status.HTTP_423_LOCKED, detail="User is locked out") from e
if not user:
raise HTTPException(