1
0
Fork 0
mirror of https://github.com/retspen/webvirtcloud synced 2026-11-08 19:26:48 +00:00
webvirtcloud/accounts/tests.py
catborise c2fea2aa39 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.
2026-10-06 11:05:03 +03:00

287 lines
10 KiB
Python

from appsettings.settings import app_settings
from computes.models import Compute
from django.conf import settings
from django.contrib.auth import get_user_model
from django.contrib.auth.models import Permission
from django.shortcuts import reverse
from django.test import Client, RequestFactory, TestCase
from instances.models import Instance
from accounts.forms import UserInstanceForm, UserSSHKeyForm
from accounts.models import UserInstance, UserSSHKey
from accounts.utils import validate_ssh_key
class AccountsTestCase(TestCase):
@classmethod
def setUpClass(cls):
super().setUpClass()
# Add users for testing purposes
User = get_user_model()
cls.admin_user = User.objects.get(pk=1)
cls.test_user = User.objects.create_user(username="test", password="test")
# Add compute
cls.compute = Compute.objects.create(
name="accounts-test-compute",
hostname="localhost",
login="",
password="",
details="local",
type=4,
)
cls.instance = Instance.objects.create(
compute=cls.compute,
name="test-vm",
)
@classmethod
def tearDownClass(cls):
if getattr(cls, "instance", None):
cls.instance.delete()
if getattr(cls, "compute", None):
cls.compute.delete()
super().tearDownClass()
def setUp(self):
self.client.login(request=RequestFactory().get("/"), username="admin", password="admin")
permission = Permission.objects.get(codename="change_password")
self.test_user.user_permissions.add(permission)
self.rsa_key = "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAAAgQC6OOdbfv27QVnSC6sKxGaHb6YFc+3gxCkyVR3cTSXE/n5BEGf8aOgBpepULWa1RZfxYHY14PlKULDygdXSdrrR2kNSwoKz/Oo4d+3EE92L7ocl1+djZbptzgWgtw1OseLwbFik+iKlIdqPsH+IUQvX7yV545ZQtAP8Qj1R+uCqkw== test@test"
self.ecdsa_key = "ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBJc5xpT3R0iFJYNZbmWgAiDlHquX/BcV1kVTsnBfiMsZgU3lGaqz2eb7IBcir/dxGnsVENTTmPQ6sNcxLxT9kkQ= realgecko@archlinux"
def test_profile(self):
response = self.client.get(reverse("accounts:profile"))
self.assertEqual(response.status_code, 200)
response = self.client.get(
reverse("accounts:account", args=[self.test_user.id])
)
self.assertEqual(response.status_code, 200)
def test_account_with_otp(self):
settings.OTP_ENABLED = True
response = self.client.get(
reverse("accounts:account", args=[self.test_user.id])
)
self.assertEqual(response.status_code, 200)
def test_login_logout(self):
client = Client()
response = client.post(
reverse("accounts:login"), {"username": "test", "password": "test"}
)
self.assertRedirects(response, settings.LOGIN_REDIRECT_URL)
response = client.post(reverse("accounts:logout"))
self.assertRedirects(response, reverse("accounts:login"))
def test_change_password(self):
self.client.force_login(self.test_user)
response = self.client.get(reverse("accounts:change_password"))
self.assertEqual(response.status_code, 200)
response = self.client.post(
reverse("accounts:change_password"),
{
"old_password": "wrongpass",
"new_password1": "newpw",
"new_password2": "newpw",
},
)
self.assertEqual(response.status_code, 200)
response = self.client.post(
reverse("accounts:change_password"),
{
"old_password": "test",
"new_password1": "newpw",
"new_password2": "newpw",
},
)
self.assertRedirects(response, reverse("accounts:profile"))
self.client.logout()
logged_in = self.client.login(request=RequestFactory().get("/"), username="test", password="newpw")
self.assertTrue(logged_in)
def test_user_instance_create_update_delete(self):
# create
response = self.client.get(
reverse("accounts:user_instance_create", args=[self.test_user.id])
)
self.assertEqual(response.status_code, 200)
response = self.client.post(
reverse("accounts:user_instance_create", args=[self.test_user.id]),
{
"user": self.test_user.id,
"instance": self.instance.id,
"is_change": False,
"is_delete": False,
"is_vnc": False,
},
)
self.assertRedirects(
response, reverse("accounts:account", args=[self.test_user.id])
)
user_instance: UserInstance = UserInstance.objects.get(
user=self.test_user, instance=self.instance
)
self.assertEqual(user_instance.user, self.test_user)
self.assertEqual(user_instance.instance, self.instance)
self.assertEqual(user_instance.is_change, False)
self.assertEqual(user_instance.is_delete, False)
self.assertEqual(user_instance.is_vnc, False)
# update
response = self.client.get(
reverse("accounts:user_instance_update", args=[user_instance.id])
)
self.assertEqual(response.status_code, 200)
response = self.client.post(
reverse("accounts:user_instance_update", args=[user_instance.id]),
{
"user": self.test_user.id,
"instance": self.instance.id,
"is_change": True,
"is_delete": True,
"is_vnc": True,
},
)
self.assertRedirects(
response, reverse("accounts:account", args=[self.test_user.id])
)
user_instance = UserInstance.objects.get(
user=self.test_user, instance=self.instance
)
self.assertEqual(user_instance.user, self.test_user)
self.assertEqual(user_instance.instance, self.instance)
self.assertEqual(user_instance.is_change, True)
self.assertEqual(user_instance.is_delete, True)
self.assertEqual(user_instance.is_vnc, True)
# delete
response = self.client.get(
reverse("accounts:user_instance_delete", args=[user_instance.id])
)
self.assertEqual(response.status_code, 200)
response = self.client.post(
reverse("accounts:user_instance_delete", args=[user_instance.id])
)
self.assertRedirects(
response, reverse("accounts:account", args=[self.test_user.id])
)
# test 'next' redirect during deletion
user_instance = UserInstance.objects.create(
user=self.test_user, instance=self.instance
)
response = self.client.post(
reverse("accounts:user_instance_delete", args=[user_instance.id])
+ "?next="
+ reverse("index")
)
self.assertRedirects(response, reverse("index"))
def test_update_user_profile(self):
self.client.force_login(self.test_user)
user = get_user_model().objects.get(username="test")
self.assertEqual(user.first_name, "")
self.assertEqual(user.last_name, "")
self.assertEqual(user.email, "")
response = self.client.post(
reverse("accounts:profile"),
{
"first_name": "first name",
"last_name": "last name",
"email": "email@mail.mail",
},
)
self.assertRedirects(response, reverse("accounts:profile"))
user = get_user_model().objects.get(username="test")
self.assertEqual(user.first_name, "first name")
self.assertEqual(user.last_name, "last name")
self.assertEqual(user.email, "email@mail.mail")
def test_create_delete_ssh_key(self):
response = self.client.get(reverse("accounts:ssh_key_create"))
self.assertEqual(response.status_code, 200)
response = self.client.post(
reverse("accounts:ssh_key_create"),
{
"keyname": "keyname",
"keypublic": self.rsa_key,
},
)
self.assertRedirects(response, reverse("accounts:profile"))
key = UserSSHKey.objects.get(keyname="keyname")
self.assertEqual(key.keyname, "keyname")
self.assertEqual(key.keypublic, self.rsa_key)
response = self.client.get(reverse("accounts:ssh_key_delete", args=[key.id]))
self.assertEqual(response.status_code, 200)
response = self.client.post(reverse("accounts:ssh_key_delete", args=[key.id]))
self.assertRedirects(response, reverse("accounts:profile"))
def test_validate_ssh_key(self):
self.assertFalse(validate_ssh_key(""))
self.assertFalse(validate_ssh_key("ssh-rsa ABBA test@test"))
self.assertFalse(validate_ssh_key("ssh-rsa AAAABwdzZGY= test@test"))
self.assertFalse(validate_ssh_key("ssh-rsa AAA test@test"))
# validate ecdsa key
self.assertTrue(validate_ssh_key(self.ecdsa_key))
def test_forms(self):
# raise available validation errors for maximum coverage
form = UserSSHKeyForm(
{"keyname": "keyname", "keypublic": self.rsa_key}, user=self.test_user
)
form.save()
form = UserSSHKeyForm(
{"keyname": "keyname", "keypublic": self.rsa_key}, user=self.test_user
)
self.assertFalse(form.is_valid())
form = UserSSHKeyForm(
{"keyname": "keyname", "keypublic": "invalid key"}, user=self.test_user
)
self.assertFalse(form.is_valid())
app_settings.ALLOW_INSTANCE_MULTIPLE_OWNER = "False"
form = UserInstanceForm(
{
"user": self.admin_user.id,
"instance": self.instance.id,
"is_change": False,
"is_delete": False,
"is_vnc": False,
}
)
form.save()
form = UserInstanceForm(
{
"user": self.test_user.id,
"instance": self.instance.id,
"is_change": False,
"is_delete": False,
"is_vnc": False,
}
)
self.assertFalse(form.is_valid())