feat: login IP whitelist, blocks UI and fail2ban sync (0.4.1)
Admin settings for web login whitelist, blocked IP list with unblock, and optional fail2ban ignoreip sync for SSH jail. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,88 @@
|
||||
"""Login security whitelist and unblock."""
|
||||
|
||||
from datetime import datetime, timezone
|
||||
|
||||
from sqlalchemy import delete
|
||||
|
||||
from app.models.login_attempt import LoginAttempt
|
||||
from app.services.login_security_settings import (
|
||||
clear_web_login_block,
|
||||
get_effective_login_security_config,
|
||||
is_ip_login_whitelisted,
|
||||
list_web_login_blocks,
|
||||
parse_ip_whitelist_text,
|
||||
upsert_login_security_settings,
|
||||
)
|
||||
|
||||
|
||||
def test_parse_ip_whitelist_text():
|
||||
assert parse_ip_whitelist_text("192.168.160.3\n192.168.160.4") == [
|
||||
"192.168.160.3",
|
||||
"192.168.160.4",
|
||||
]
|
||||
assert parse_ip_whitelist_text("192.168.160.3, 10.0.0.1") == [
|
||||
"192.168.160.3",
|
||||
"10.0.0.1",
|
||||
]
|
||||
assert parse_ip_whitelist_text("not-an-ip\n192.168.1.1") == ["192.168.1.1"]
|
||||
|
||||
|
||||
def test_whitelisted_ip_not_blocked(client, db_session, monkeypatch):
|
||||
monkeypatch.setenv("SAC_LOGIN_MAX_FAILURES", "2")
|
||||
monkeypatch.setenv("SAC_LOGIN_ALERT_TELEGRAM", "false")
|
||||
monkeypatch.setattr(
|
||||
"app.services.login_rate_limit.client_ip_from_request",
|
||||
lambda _request: "192.168.160.3",
|
||||
)
|
||||
from app.config import get_settings
|
||||
|
||||
get_settings.cache_clear()
|
||||
db_session.execute(delete(LoginAttempt))
|
||||
db_session.commit()
|
||||
|
||||
upsert_login_security_settings(
|
||||
db_session,
|
||||
ip_whitelist_text="192.168.160.3",
|
||||
sync_fail2ban=False,
|
||||
)
|
||||
|
||||
for _ in range(5):
|
||||
r = client.post(
|
||||
"/api/v1/auth/login",
|
||||
json={"username": "test-admin", "password": "wrong"},
|
||||
)
|
||||
assert r.status_code == 401
|
||||
|
||||
get_settings.cache_clear()
|
||||
|
||||
|
||||
def test_list_web_blocks_and_clear(db_session, monkeypatch):
|
||||
monkeypatch.setenv("SAC_LOGIN_MAX_FAILURES", "2")
|
||||
monkeypatch.setenv("SAC_LOGIN_FAILURE_WINDOW_MINUTES", "15")
|
||||
from app.config import get_settings
|
||||
|
||||
get_settings.cache_clear()
|
||||
|
||||
now = datetime.now(timezone.utc)
|
||||
db_session.add(
|
||||
LoginAttempt(ip_address="203.0.113.50", username="bad", success=False, created_at=now)
|
||||
)
|
||||
db_session.add(
|
||||
LoginAttempt(ip_address="203.0.113.50", username="bad", success=False, created_at=now)
|
||||
)
|
||||
db_session.commit()
|
||||
|
||||
blocks = list_web_login_blocks(db_session)
|
||||
assert any(b.ip_address == "203.0.113.50" for b in blocks)
|
||||
|
||||
deleted = clear_web_login_block(db_session, "203.0.113.50")
|
||||
assert deleted >= 2
|
||||
assert not list_web_login_blocks(db_session)
|
||||
|
||||
get_settings.cache_clear()
|
||||
|
||||
|
||||
def test_is_ip_login_whitelisted():
|
||||
wl = ("192.168.160.3",)
|
||||
assert is_ip_login_whitelisted("192.168.160.3", wl)
|
||||
assert not is_ip_login_whitelisted("192.168.160.4", wl)
|
||||
Reference in New Issue
Block a user