mirror of
https://github.com/retspen/webvirtcloud
synced 2026-11-11 12:46:49 +00:00
feat(accounts): failed logins lock a user for the address they come from
Nothing limited failed logins: a password could be guessed at the speed the server answers. django-axes (8.3.1, Django 5.2, Python 3.10-3.13) now counts them. - A user is locked for the address the failed logins come from (username and IP together), so an attacker cannot lock someone out from elsewhere and users behind one NAT address do not share a lock. A successful login resets the count. axes sits in front of every authentication: the login page with or without OTP, LDAP and the API. - The number of failed logins (default 5) and the lock time in minutes (default 15) are on the Settings page (LOGIN_FAILURE_LIMIT, LOGIN_LOCKOUT_MINUTES, added by migration appsettings.0014) and apply at once; an invalid value falls back to the default. Unlock with "manage.py axes_reset_username <username>". - Behind the bundled nginx every request comes from 127.0.0.1; the client address is read from X-Real-IP, which nginx sets, but only for requests from a loopback address or from LOGIN_TRUSTED_PROXIES, so a forged header does not escape a lock. settings.py.template gets the app, middleware, backend and AXES_* settings; README tells existing installations what to copy. Tests that call client.login now pass a request, which the axes backend needs. The new tests fail with axes disabled.
This commit is contained in:
parent
62ffaab808
commit
c2fea2aa39
17 changed files with 223 additions and 27 deletions
30
appsettings/migrations/0014_login_lockout_settings.py
Normal file
30
appsettings/migrations/0014_login_lockout_settings.py
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
from django.db import migrations
|
||||
|
||||
|
||||
SETTINGS = [
|
||||
("Login Failure Limit", "LOGIN_FAILURE_LIMIT", "5",
|
||||
"Failed logins of a user from one address before it is locked"),
|
||||
("Login Lockout Minutes", "LOGIN_LOCKOUT_MINUTES", "15",
|
||||
"How long a locked login stays locked, in minutes"),
|
||||
]
|
||||
|
||||
|
||||
def add_settings(apps, schema_editor):
|
||||
setting = apps.get_model("appsettings", "AppSettings")
|
||||
db_alias = schema_editor.connection.alias
|
||||
for name, key, value, description in SETTINGS:
|
||||
setting.objects.using(db_alias).get_or_create(
|
||||
key=key, defaults={"name": name, "value": value, "choices": "", "description": description}
|
||||
)
|
||||
|
||||
|
||||
def del_settings(apps, schema_editor):
|
||||
setting = apps.get_model("appsettings", "AppSettings")
|
||||
db_alias = schema_editor.connection.alias
|
||||
setting.objects.using(db_alias).filter(key__in=[key for _, key, _, _ in SETTINGS]).delete()
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [("appsettings", "0013_remove_qemu_console_default_type")]
|
||||
|
||||
operations = [migrations.RunPython(add_settings, del_settings)]
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
from django.test import TestCase
|
||||
from django.test import RequestFactory, TestCase
|
||||
from django.urls import reverse
|
||||
|
||||
from appsettings.models import AppSettings
|
||||
|
|
@ -7,7 +7,7 @@ from appsettings.settings import app_settings, get_settings
|
|||
|
||||
class AppSettingsTestCase(TestCase):
|
||||
def setUp(self):
|
||||
self.client.login(username="admin", password="admin")
|
||||
self.client.login(request=RequestFactory().get("/"), username="admin", password="admin")
|
||||
|
||||
def test_get_settings_populates_singleton(self):
|
||||
AppSettings.objects.update_or_create(
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue