Безопасность: команда ротации пароля администратора из .env
В prod пароль админа задавался только при первом создании: обычный bootstrap его из .env не берёт, панель не меняет — комментарии отсылали друг к другу. Теперь `python -m app.bootstrap --reset-admin-password` применяет ADMIN_PASSWORD к существующему админу через user_service.set_password: пароль проверяется, token_version растёт, и все админские сессии (в том числе чужие) отзываются. Обычный старт по-прежнему пароль не трогает. Процедура на Pi — в deploy/pi/README.md (правка .env → docker compose up -d → exec команды), кратко — в README. #73 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LqSoRj99iwVEH5U5fnZgsd
This commit is contained in:
@@ -0,0 +1,77 @@
|
||||
"""Ротация пароля администратора из .env (#73): `python -m app.bootstrap --reset-admin-password`."""
|
||||
from __future__ import annotations
|
||||
|
||||
import pytest
|
||||
from fastapi.testclient import TestClient
|
||||
from sqlmodel import Session, select
|
||||
|
||||
from app import bootstrap
|
||||
from app.core.security import verify_password
|
||||
from app.models import User
|
||||
from tests.conftest import csrf_headers
|
||||
|
||||
OLD, NEW = "secret123", "brand-new-admin-pw"
|
||||
|
||||
|
||||
def _admin(engine) -> User:
|
||||
with Session(engine) as s:
|
||||
return s.exec(select(User).where(User.role == "admin")).one()
|
||||
|
||||
|
||||
def _login(client: TestClient, password: str):
|
||||
return client.post(
|
||||
"/api/admin/auth/login",
|
||||
json={"username": "admin", "password": password},
|
||||
headers=csrf_headers(client),
|
||||
)
|
||||
|
||||
|
||||
def test_reset_applies_env_password_and_revokes_sessions(
|
||||
client: TestClient, engine, make_admin, monkeypatch
|
||||
):
|
||||
make_admin("admin", OLD)
|
||||
assert _login(client, OLD).status_code == 200
|
||||
assert client.get("/api/admin/me").status_code == 200
|
||||
version = _admin(engine).token_version
|
||||
|
||||
monkeypatch.setattr(bootstrap.settings, "admin_password", NEW)
|
||||
with Session(engine) as s:
|
||||
bootstrap.reset_admin_password(s)
|
||||
|
||||
admin = _admin(engine)
|
||||
assert verify_password(NEW, admin.password_hash)
|
||||
assert admin.token_version == version + 1
|
||||
# Прежняя админская cookie больше не действует (в т.ч. у того, кто украл пароль).
|
||||
assert client.get("/api/admin/me").status_code == 401
|
||||
assert _login(client, OLD).status_code == 401
|
||||
assert _login(client, NEW).status_code == 200
|
||||
|
||||
|
||||
def test_regular_bootstrap_keeps_admin_password_outside_dev(engine, make_admin, monkeypatch):
|
||||
"""Обычный старт в prod по-прежнему не берёт пароль из .env — только ротация."""
|
||||
make_admin("admin", OLD)
|
||||
monkeypatch.setattr(bootstrap.settings, "app_env", "production")
|
||||
monkeypatch.setattr(bootstrap.settings, "admin_password", NEW)
|
||||
with Session(engine) as s:
|
||||
bootstrap._ensure_admin(s)
|
||||
assert verify_password(OLD, _admin(engine).password_hash)
|
||||
|
||||
|
||||
def test_reset_refuses_without_admin_or_password(engine, make_admin, monkeypatch):
|
||||
monkeypatch.setattr(bootstrap.settings, "admin_password", NEW)
|
||||
with Session(engine) as s, pytest.raises(RuntimeError, match="Администратора ещё нет"):
|
||||
bootstrap.reset_admin_password(s)
|
||||
|
||||
make_admin("admin", OLD)
|
||||
monkeypatch.setattr(bootstrap.settings, "admin_password", " ")
|
||||
with Session(engine) as s, pytest.raises(RuntimeError, match="ADMIN_PASSWORD пуст"):
|
||||
bootstrap.reset_admin_password(s)
|
||||
assert verify_password(OLD, _admin(engine).password_hash)
|
||||
|
||||
|
||||
def test_reset_refuses_when_bootstrap_disabled(engine, make_admin, monkeypatch):
|
||||
make_admin("admin", OLD)
|
||||
monkeypatch.setattr(bootstrap.settings, "admin_bootstrap_enabled", False)
|
||||
monkeypatch.setattr(bootstrap.settings, "admin_password", NEW)
|
||||
with Session(engine) as s, pytest.raises(RuntimeError, match="ADMIN_BOOTSTRAP_ENABLED"):
|
||||
bootstrap.reset_admin_password(s)
|
||||
Reference in New Issue
Block a user