1
0
Fork 0
mirror of https://github.com/retspen/webvirtcloud synced 2026-11-09 11:46:49 +00:00
webvirtcloud/accounts/otp.py
catborise 8c0fbff8f7 feat(logs): admin changes and logins are recorded on the Logs page
The Logs page shows who added, changed or deleted a compute, from web
or API, and every login, failed login, lockout and logout, with the
client address and a Result column (Done, Failed, Unknown for older
rows). A failed change is recorded with its reason and the error still
reaches the user; a refused one is recorded too.

- logs.audit.write saves each row in its own savepoint and never
  raises, so a log write cannot break or undo the action it records.
  The audit() context manager writes one row when the change ends.
- Logs gains success and ip; user, host and instance are wider and
  date is indexed. The page orders by date then id.
- A compute edit names the changed fields; an edit that changes
  nothing writes no row, and an empty password field is no change.
- A compute's VMs leaving the panel get a system row each: owners
  kept on the Removed VMs page, moved to another compute, restored,
  left without owners, or the removed record expired.
- Job rows carry the result. A job's log entry that cannot be
  computed is logged and skipped instead of rolling back the finish
  (test_service.py changes this on purpose).
- Logins are recorded by signal receivers; CustomLoginView is removed
  and the plain LoginView is used. A wrong OTP code is a failed login,
  counted by django-axes toward the lockout.

Panel records and host-side objects follow in the next layers.
2026-10-23 10:30:48 +03:00

43 lines
2 KiB
Python

"""The login form used when OTP_ENABLED.
django-otp's ``OTPAuthenticationForm`` requires every user to present a token
and, since 1.6, to name the device explicitly, so the single-field login page
cannot satisfy it and a user who never enrolled is locked out. This form makes
OTP per user: a user with a confirmed TOTP device must enter a valid code, one
without a device logs in with the password alone. The device is chosen by the
server, so the page needs only the token field and a posted device id (which
django-otp does not check for confirmed status or ownership-by-type) is
ignored.
"""
from django.contrib.auth.signals import user_login_failed
from django.core.exceptions import ValidationError
from django_otp.forms import OTPAuthenticationForm
from django_otp.plugins.otp_totp.models import TOTPDevice
class SingleDeviceOTPForm(OTPAuthenticationForm):
def _confirmed_totp(self, user, for_verify=False):
"""The user's oldest confirmed TOTP device, or None. With for_verify
the row is locked (select_for_update) so the token's replay and
throttling counters cannot race; call it only inside clean_otp's
transaction."""
devices = TOTPDevice.objects.filter(user=user, confirmed=True)
if for_verify:
devices = devices.select_for_update()
return devices.order_by("id").first()
def _chosen_device(self, user):
# Ignore a posted otp_device: it is verified within clean_otp's
# transaction, where the lock belongs.
return self._confirmed_totp(user, for_verify=True)
def clean_otp(self, user):
if user is not None and self._confirmed_totp(user) is None:
return # no confirmed device: the password is the only factor
try:
return super().clean_otp(user)
except ValidationError:
# a wrong code is a failed login: counted by django-axes and logged
user_login_failed.send(sender=__name__, credentials={"username": user.get_username()}, request=self.request)
raise