mirror of
https://github.com/retspen/webvirtcloud
synced 2026-11-08 11:16:47 +00:00
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.
253 lines
7.3 KiB
Python
253 lines
7.3 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.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.models import Logs
|
|
|
|
from . import forms
|
|
from .decorators import superuser_only
|
|
|
|
|
|
@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():
|
|
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)
|
|
form = forms.GroupForm(request.POST or None, instance=group)
|
|
if form.is_valid():
|
|
form.save()
|
|
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":
|
|
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():
|
|
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)
|
|
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():
|
|
user_form.save()
|
|
attributes_form.save()
|
|
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():
|
|
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":
|
|
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)
|
|
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)
|
|
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":
|
|
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()
|