B23: политика паролей исполняется + ретенция журналов по-настоящему
- backend/password_policy.py: validate_new_password (длина, сложность буквы+цифры, сверка с текущим и историей), record_password_change (хранит ПРЕДЫДУШИЙ хэш, обрезка до password_history_count), password_expired + enforce_expiry (пометка must_change_password) - users.password_changed_at + таблица password_history (010_b23_policy) - change-password и админ-сброс теперь под полной политикой (сложность и переиспользование вместо голой длины); get_current_user проверяет срок действия каждого пароля при запросе - backend/purge_logs.py: реальный DELETE просроченных audit_events (+changes) и auth_events; POST /admin/users/maintenance/purge-logs (manage_security) + CLI python -m backend.purge_logs для cron - тесты: +19 (258 passed) — валидация/история/срок, purge/идемпотентность/403
This commit is contained in:
@@ -17,6 +17,9 @@ from sqlalchemy import text
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from backend.audit import audit_log, visible_unit_ids
|
||||
from backend.logging_setup import get_logger
|
||||
from backend.password_policy import record_password_change, validate_new_password
|
||||
from backend.purge_logs import _retention_days, purge_logs
|
||||
from backend.database import get_db
|
||||
from backend.models import MchsUnit, Role, User, UserRole
|
||||
from backend.routers.auth import (
|
||||
@@ -139,6 +142,13 @@ def create_user(
|
||||
) -> dict[str, Any]:
|
||||
if len(payload.password) < 8:
|
||||
raise HTTPException(status_code=400, detail='Минимальная длина пароля: 8 символов')
|
||||
# B23: сложность проверяется и на временный пароль админа (история пуста —
|
||||
# новый пользователь, проверка переиспользования не применима)
|
||||
probe = User(username=payload.username, email=payload.email)
|
||||
try:
|
||||
validate_new_password(db, probe, payload.password)
|
||||
except ValueError as e:
|
||||
raise HTTPException(status_code=400, detail=str(e))
|
||||
if db.query(User).filter(User.username == payload.username).first():
|
||||
raise HTTPException(status_code=400, detail='Пользователь с таким именем уже существует')
|
||||
if db.query(User).filter(User.email == payload.email).first():
|
||||
@@ -234,10 +244,18 @@ def update_user(
|
||||
changes['is_active'] = (target.is_active, payload.is_active)
|
||||
target.is_active = payload.is_active
|
||||
if payload.reset_password:
|
||||
if len(payload.reset_password) < 8:
|
||||
raise HTTPException(status_code=400, detail='Минимальная длина пароля: 8 символов')
|
||||
# B23: сброс админом тоже под полной политикой
|
||||
probe = User(username=target.username, email=target.email, id=target.id)
|
||||
try:
|
||||
validate_new_password(db, probe, payload.reset_password,
|
||||
current_hash=target.hashed_password)
|
||||
except ValueError as e:
|
||||
raise HTTPException(status_code=400, detail=str(e))
|
||||
changes['password'] = ('***', '***')
|
||||
target.hashed_password = get_password_hash(payload.reset_password)
|
||||
new_hash = get_password_hash(payload.reset_password)
|
||||
record_password_change(db, target, new_hash,
|
||||
previous_hash=target.hashed_password)
|
||||
target.hashed_password = new_hash
|
||||
target.must_change_password = True
|
||||
|
||||
if not changes:
|
||||
@@ -285,4 +303,23 @@ def list_audit(
|
||||
'changes': [{'field': ch.field_name, 'old': ch.old_value, 'new': ch.new_value}
|
||||
for ch in changes],
|
||||
})
|
||||
return {'items': items, 'total': q.count(), 'retention_days': retention}
|
||||
return {'items': items, 'total': q.count(), 'retention_days': retention}
|
||||
|
||||
|
||||
@router.post('/maintenance/purge-logs')
|
||||
def run_purge_logs(
|
||||
request: Request,
|
||||
current_user: User = Depends(require_permission('manage_security')),
|
||||
db: Session = Depends(get_db),
|
||||
) -> dict[str, Any]:
|
||||
"""B23: реальное удаление журналов старше audit_retention_days.
|
||||
|
||||
Ручной запуск из админки; регулярный — cron `python -m backend.purge_logs`.
|
||||
Удаляет audit_events (+changes каскадом) и auth_events.
|
||||
"""
|
||||
result = purge_logs(db)
|
||||
audit_log(db, current_user, 'purge_logs', object_type='maintenance',
|
||||
request=request, details=result)
|
||||
log = get_logger('admin')
|
||||
log.info('purge_logs by %s: %s', getattr(current_user, 'username', '—'), result)
|
||||
return {'purged': result, 'retention_days': _retention_days(db)}
|
||||
+14
-7
@@ -17,6 +17,7 @@ from sqlalchemy.orm import Session
|
||||
from backend.database import get_db
|
||||
from backend.logging_setup import get_logger
|
||||
from backend.models import AuthEvent, SecuritySetting, User, UserSession
|
||||
from backend.password_policy import enforce_expiry, record_password_change, validate_new_password
|
||||
|
||||
log = get_logger('auth')
|
||||
|
||||
@@ -308,6 +309,9 @@ async def get_current_user(
|
||||
headers={'WWW-Authenticate': 'Bearer'},
|
||||
)
|
||||
|
||||
# B23: срок действия пароля (security_settings) — исполняется, а не декларируется
|
||||
enforce_expiry(db, user)
|
||||
|
||||
return user
|
||||
|
||||
|
||||
@@ -407,15 +411,18 @@ def change_password(
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: Session = Depends(get_db),
|
||||
) -> dict[str, str]:
|
||||
min_len = int(_get_setting(db, 'password_min_length', '8'))
|
||||
if not verify_password(payload.current_password, current_user.hashed_password):
|
||||
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail='Текущий пароль неверен')
|
||||
if len(payload.new_password) < min_len:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail=f'Минимальная длина пароля: {min_len} символов',
|
||||
)
|
||||
current_user.hashed_password = get_password_hash(payload.new_password)
|
||||
# B23: полная политика (длина, сложность, история) вместо голой длины
|
||||
try:
|
||||
validate_new_password(db, current_user, payload.new_password,
|
||||
current_hash=current_user.hashed_password)
|
||||
except ValueError as e:
|
||||
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e))
|
||||
new_hash = get_password_hash(payload.new_password)
|
||||
record_password_change(db, current_user, new_hash,
|
||||
previous_hash=current_user.hashed_password)
|
||||
current_user.hashed_password = new_hash
|
||||
current_user.must_change_password = False
|
||||
current_user.updated_at = datetime.now(timezone.utc)
|
||||
# Отзываем все сессии после смены пароля (кроме текущей — её пересоздадим)
|
||||
|
||||
Reference in New Issue
Block a user