mirror of
https://github.com/retspen/webvirtcloud
synced 2026-11-11 04:36:48 +00:00
feat(logs): changes to users, groups, owners, flavors and settings are logged
The Logs page now records the panel's own records too: users (add, change with the changed fields, delete, block, unblock, an admin's password set, a user's own password change including the first-login one, own profile edits), groups with their members and permissions, VM owners from the accounts pages and from the VM page, OTP QR code mails, flavors from web and API, app settings and the theme, removed-VM records and SSH keys. No row carries a password or a key. - A user is created in one transaction: when a VM named in NEW_USER_DEFAULT_INSTANCES is missing, no half-made user stays. - The OTP mail checks the email address before it creates a device; without one it writes a failed row and makes no device. - The VM page's owner rows read "VM web-1 owner bob: add" and "...: remove" instead of "Add owner: bob" and "Delete owner: 17"; a second owner refused by the single-owner setting is a failed row. Owner rows keep the VM's host and name, so the VM's log tab shows them. - A setting or theme posted with its current value is no change and writes no row; the old free-text setting rows are gone. Host-side objects (networks, storage, interfaces, secrets, filters) follow in the next layer.
This commit is contained in:
parent
8c0fbff8f7
commit
e04adec87b
10 changed files with 468 additions and 46 deletions
44
appsettings/tests/test_audit_rows.py
Normal file
44
appsettings/tests/test_audit_rows.py
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
"""App setting and theme changes write rows on the Logs page."""
|
||||
|
||||
from unittest.mock import patch
|
||||
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.db import DatabaseError
|
||||
from django.test import TestCase
|
||||
from django.urls import reverse
|
||||
|
||||
from appsettings.models import AppSettings
|
||||
from logs.models import Logs
|
||||
|
||||
|
||||
def rows():
|
||||
return list(Logs.objects.order_by("id").values_list("message", "success", "host"))
|
||||
|
||||
|
||||
class SettingRowTests(TestCase):
|
||||
def setUp(self):
|
||||
self.client.force_login(get_user_model().objects.create_superuser("audit-admin", "audit@example.com", "pw"))
|
||||
Logs.objects.all().delete() # the login's row
|
||||
self.url = reverse("appsettings")
|
||||
|
||||
def test_setting_and_theme_rows(self):
|
||||
name = AppSettings.objects.get(key="LOGS_PER_PAGE").name
|
||||
self.client.post(self.url, {"LOGS_PER_PAGE": "50"})
|
||||
self.client.post(self.url, {"LOGS_PER_PAGE": "50"}) # no change
|
||||
before = AppSettings.objects.get(key="WORKER_CONCURRENCY").value
|
||||
self.client.post(self.url, {"WORKER_CONCURRENCY": "0"}) # out of bounds
|
||||
self.assertEqual(AppSettings.objects.get(key="WORKER_CONCURRENCY").value, before)
|
||||
theme = "cerulean" if AppSettings.objects.get(key="BOOTSTRAP_THEME").value != "cerulean" else "brite"
|
||||
self.client.post(self.url, {"BOOTSTRAP_THEME": theme})
|
||||
self.client.post(self.url, {"BOOTSTRAP_THEME": theme}) # no change
|
||||
self.assertEqual(rows(), [
|
||||
(f"Setting {name}: change to 50", True, "-"),
|
||||
(f"Setting Theme: change to {theme}", True, "-"),
|
||||
])
|
||||
|
||||
def test_a_failing_save_is_a_failed_row(self):
|
||||
name = AppSettings.objects.get(key="LOGS_PER_PAGE").name
|
||||
with patch.object(AppSettings, "save", side_effect=DatabaseError("disk full")):
|
||||
response = self.client.post(self.url, {"LOGS_PER_PAGE": "50"}, follow=True)
|
||||
self.assertIn("error", [m.level_tag for m in response.context["messages"]])
|
||||
self.assertEqual(rows(), [(f"Setting {name}: change to 50: DatabaseError", False, "-")])
|
||||
|
|
@ -6,7 +6,7 @@ from django.contrib import messages
|
|||
from django.http import HttpResponseRedirect
|
||||
from django.shortcuts import render
|
||||
from django.utils.translation import gettext_noop as _
|
||||
from logs.views import addlogmsg
|
||||
from logs.audit import audit
|
||||
from operations.utils import BOUNDS
|
||||
|
||||
from appsettings.models import AppSettings
|
||||
|
|
@ -36,11 +36,12 @@ def appsettings(request):
|
|||
if theme not in themes_list:
|
||||
messages.error(request, _("Unknown theme: %(theme)s") % {"theme": theme})
|
||||
return HttpResponseRedirect(request.get_full_path())
|
||||
bootstrap_theme.value = theme
|
||||
bootstrap_theme.save()
|
||||
if theme != bootstrap_theme.value:
|
||||
with audit(request, None, "Setting Theme", f"change to {theme}"):
|
||||
bootstrap_theme.value = theme
|
||||
bootstrap_theme.save()
|
||||
msg = _("Theme is changed. Now: %(theme)s") % {"theme": theme}
|
||||
messages.success(request, msg)
|
||||
addlogmsg(request.user.username, "-", "", msg)
|
||||
return HttpResponseRedirect(request.get_full_path())
|
||||
|
||||
for setting in appsettings:
|
||||
|
|
@ -52,9 +53,12 @@ def appsettings(request):
|
|||
messages.error(request, _("%(setting)s must be a whole number between %(low)s and %(high)s")
|
||||
% {"setting": setting.name, "low": low, "high": high})
|
||||
return HttpResponseRedirect(request.get_full_path())
|
||||
value = request.POST.get(setting.key, "")
|
||||
try:
|
||||
setting.value = request.POST.get(setting.key, "")
|
||||
setting.save()
|
||||
if value != setting.value:
|
||||
with audit(request, None, f"Setting {setting.name}", f"change to {value}"):
|
||||
setting.value = value
|
||||
setting.save()
|
||||
|
||||
msg = _("%(setting)s is changed. Now: %(value)s") % {
|
||||
"setting": setting.name,
|
||||
|
|
@ -64,8 +68,6 @@ def appsettings(request):
|
|||
except Exception as err:
|
||||
msg = err
|
||||
messages.error(request, msg)
|
||||
|
||||
addlogmsg(request.user.username, "-", "", msg)
|
||||
return HttpResponseRedirect(request.get_full_path())
|
||||
|
||||
return render(request, "appsettings.html", locals())
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue