mirror of
https://github.com/retspen/webvirtcloud
synced 2026-11-08 11:16:47 +00:00
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.
280 lines
9 KiB
Python
280 lines
9 KiB
Python
from datetime import timedelta
|
|
|
|
from accounts.models import Instance, UserAttributes, UserInstance
|
|
from appsettings.settings import app_settings
|
|
from django.conf import settings
|
|
from django.contrib import messages
|
|
from django.contrib.auth import update_session_auth_hash
|
|
from django.contrib.auth.forms import AdminPasswordChangeForm
|
|
from django.contrib.auth.models import Group, User
|
|
from django.core.paginator import Paginator
|
|
from django.db import transaction
|
|
from django.shortcuts import get_object_or_404, redirect, render
|
|
from django.utils.http import url_has_allowed_host_and_scheme
|
|
from django.utils.translation import gettext_lazy as _
|
|
from django.views.decorators.http import require_POST
|
|
from instances.models import InstanceTombstone
|
|
from logs.audit import audit, changed_fields, snapshot
|
|
from logs.models import Logs
|
|
|
|
from . import forms
|
|
from .decorators import superuser_only
|
|
|
|
|
|
def group_snapshot(group):
|
|
"""snapshot() of a group with its members, which are a reverse relation snapshot() does not see."""
|
|
return snapshot(group) | {"users": frozenset(group.user_set.values_list("pk", flat=True))}
|
|
|
|
|
|
@superuser_only
|
|
def group_list(request):
|
|
groups = Group.objects.all()
|
|
return render(
|
|
request,
|
|
"admin/group_list.html",
|
|
{
|
|
"groups": groups,
|
|
},
|
|
)
|
|
|
|
|
|
@superuser_only
|
|
def group_create(request):
|
|
form = forms.GroupForm(request.POST or None)
|
|
if form.is_valid():
|
|
with audit(request, None, f"Group {form.cleaned_data['name']}", "add"):
|
|
form.save()
|
|
return redirect("admin:group_list")
|
|
|
|
return render(
|
|
request,
|
|
"common/form.html",
|
|
{
|
|
"form": form,
|
|
"title": _("Create Group"),
|
|
},
|
|
)
|
|
|
|
|
|
@superuser_only
|
|
def group_update(request, pk):
|
|
group = get_object_or_404(Group, pk=pk)
|
|
subject, before = f"Group {group.name}", group_snapshot(group)
|
|
form = forms.GroupForm(request.POST or None, instance=group)
|
|
if form.is_valid():
|
|
with audit(request, None, subject, "change") as entry:
|
|
form.save()
|
|
fields = changed_fields(before, group_snapshot(group))
|
|
entry.action = "change " + ", ".join(fields)
|
|
entry.skip = not fields
|
|
return redirect("admin:group_list")
|
|
|
|
return render(
|
|
request,
|
|
"common/form.html",
|
|
{
|
|
"form": form,
|
|
"title": _("Update Group"),
|
|
},
|
|
)
|
|
|
|
|
|
@superuser_only
|
|
def group_delete(request, pk):
|
|
group = get_object_or_404(Group, pk=pk)
|
|
if request.method == "POST":
|
|
with audit(request, None, f"Group {group.name}", "delete"):
|
|
group.delete()
|
|
return redirect("admin:group_list")
|
|
|
|
return render(
|
|
request,
|
|
"common/confirm_delete.html",
|
|
{"object": group, "consequence": _("Its members stay but lose the permissions it gave them.")},
|
|
)
|
|
|
|
|
|
@superuser_only
|
|
def user_list(request):
|
|
users = User.objects.all()
|
|
return render(
|
|
request,
|
|
"admin/user_list.html",
|
|
{
|
|
"users": users,
|
|
"title": _("Users"),
|
|
},
|
|
)
|
|
|
|
|
|
@superuser_only
|
|
def user_create(request):
|
|
user_form = forms.UserCreateForm(request.POST or None)
|
|
attributes_form = forms.UserAttributesForm(request.POST or None)
|
|
if user_form.is_valid() and attributes_form.is_valid():
|
|
with audit(request, None, f"User {user_form.cleaned_data['username']}", "add"), transaction.atomic():
|
|
user = user_form.save()
|
|
attributes = attributes_form.save(commit=False)
|
|
attributes.user = user
|
|
attributes.save()
|
|
add_default_instances(user)
|
|
return redirect("admin:user_list")
|
|
|
|
return render(
|
|
request,
|
|
"admin/user_form.html",
|
|
{
|
|
"user_form": user_form,
|
|
"attributes_form": attributes_form,
|
|
"title": _("Create User"),
|
|
},
|
|
)
|
|
|
|
|
|
@superuser_only
|
|
def user_update(request, pk):
|
|
user = get_object_or_404(User, pk=pk)
|
|
attributes, attributes_created = UserAttributes.objects.get_or_create(user=user)
|
|
subject = f"User {user.username}"
|
|
before, before_attributes = snapshot(user), snapshot(attributes)
|
|
user_form = forms.UserForm(request.POST or None, instance=user)
|
|
attributes_form = forms.UserAttributesForm(
|
|
request.POST or None, instance=attributes
|
|
)
|
|
if user_form.is_valid() and attributes_form.is_valid():
|
|
with audit(request, None, subject, "change") as entry:
|
|
user_form.save()
|
|
attributes_form.save()
|
|
fields = set(changed_fields(before, snapshot(user))) | set(changed_fields(before_attributes, snapshot(attributes)))
|
|
fields = sorted(fields - {"password", "last_login"})
|
|
entry.action = "change " + ", ".join(fields)
|
|
entry.skip = not fields
|
|
next_url = request.GET.get("next")
|
|
if next_url and url_has_allowed_host_and_scheme(
|
|
url=next_url, allowed_hosts={request.get_host()}, require_https=request.is_secure()
|
|
):
|
|
return redirect(next_url)
|
|
return redirect("admin:user_list")
|
|
|
|
return render(
|
|
request,
|
|
"admin/user_form.html",
|
|
{
|
|
"user_form": user_form,
|
|
"attributes_form": attributes_form,
|
|
"title": _("Update User"),
|
|
},
|
|
)
|
|
|
|
|
|
@superuser_only
|
|
def user_update_password(request, pk):
|
|
user = get_object_or_404(User, pk=pk)
|
|
if request.method == "POST":
|
|
form = AdminPasswordChangeForm(user, request.POST)
|
|
if form.is_valid():
|
|
with audit(request, None, f"User {user.username}", "set password"):
|
|
user = form.save()
|
|
update_session_auth_hash(request, user) # Important!
|
|
messages.success(
|
|
request, _("Password changed for %(user)s") % {"user": user.username}
|
|
)
|
|
return redirect("admin:user_list")
|
|
else:
|
|
messages.error(request, _("Wrong Data Provided"))
|
|
else:
|
|
form = AdminPasswordChangeForm(user)
|
|
|
|
return render(
|
|
request,
|
|
"accounts/change_password_form.html",
|
|
{
|
|
"form": form,
|
|
"user": user.username,
|
|
},
|
|
)
|
|
|
|
|
|
@superuser_only
|
|
def user_delete(request, pk):
|
|
user = get_object_or_404(User, pk=pk)
|
|
if request.method == "POST":
|
|
with audit(request, None, f"User {user.username}", "delete"):
|
|
user.delete()
|
|
return redirect("admin:user_list")
|
|
|
|
return render(
|
|
request,
|
|
"common/confirm_delete.html",
|
|
{"object": user,
|
|
"consequence": _("Their access to VMs, SSH keys and settings go; the VMs themselves stay.")},
|
|
)
|
|
|
|
|
|
@require_POST
|
|
@superuser_only
|
|
def user_block(request, pk):
|
|
user: User = get_object_or_404(User, pk=pk)
|
|
with audit(request, None, f"User {user.username}", "block"):
|
|
user.is_active = False
|
|
user.save()
|
|
return redirect("admin:user_list")
|
|
|
|
|
|
@require_POST
|
|
@superuser_only
|
|
def user_unblock(request, pk):
|
|
user: User = get_object_or_404(User, pk=pk)
|
|
with audit(request, None, f"User {user.username}", "unblock"):
|
|
user.is_active = True
|
|
user.save()
|
|
return redirect("admin:user_list")
|
|
|
|
|
|
@superuser_only
|
|
def logs(request):
|
|
paginator = Paginator(Logs.objects.order_by("-date", "-id"), int(app_settings.LOGS_PER_PAGE))
|
|
logs = paginator.get_page(request.GET.get("page"))
|
|
return render(request, "admin/logs.html", {"logs": logs})
|
|
|
|
|
|
@superuser_only
|
|
def tombstone_list(request):
|
|
"""The owners kept for VMs that vanished from their compute, in case the
|
|
same UUID comes back."""
|
|
tombstones = list(InstanceTombstone.objects.order_by("-removed"))
|
|
ids = {owner["user"] for tombstone in tombstones for owner in tombstone.owners}
|
|
names = dict(User.objects.filter(id__in=ids).values_list("id", "username"))
|
|
retention = timedelta(days=getattr(settings, "INSTANCE_OWNERSHIP_RETENTION_DAYS", 30))
|
|
for tombstone in tombstones:
|
|
tombstone.owner_names = [names.get(owner["user"], _("deleted user")) for owner in tombstone.owners]
|
|
tombstone.expires = tombstone.removed + retention
|
|
return render(request, "admin/tombstone_list.html", {"tombstones": tombstones})
|
|
|
|
|
|
@superuser_only
|
|
def tombstone_delete(request, pk):
|
|
tombstone = get_object_or_404(InstanceTombstone, pk=pk)
|
|
if request.method == "POST":
|
|
with audit(request, None, f"Removed VM {tombstone.name} ({tombstone.uuid})", "delete"):
|
|
tombstone.delete()
|
|
return redirect("admin:tombstone_list")
|
|
|
|
return render(
|
|
request,
|
|
"common/confirm_delete.html",
|
|
{"object": tombstone,
|
|
"consequence": _("Its owners no longer get the VM back if its UUID shows up again.")},
|
|
)
|
|
|
|
|
|
def add_default_instances(user):
|
|
"""
|
|
Adds instances listed in NEW_USER_DEFAULT_INSTANCES to user
|
|
"""
|
|
existing_instances = UserInstance.objects.filter(user=user)
|
|
if not existing_instances:
|
|
for instance_name in settings.NEW_USER_DEFAULT_INSTANCES:
|
|
instance = Instance.objects.get(name=instance_name)
|
|
user_instance = UserInstance(user=user, instance=instance)
|
|
user_instance.save()
|