mirror of
https://github.com/retspen/webvirtcloud
synced 2026-11-08 11:16:47 +00:00
Each was checked with git grep across every tracked file (templates, JS, shell, unit files and the extensionless console daemons) and for dynamic use (URL names, template names, registries): - vrtManager: host_is_up, is_kvm_supported, get_emulators, get_file_extensions, both get_network_forward, get_snapshots, get_storages_images, get_os_type, get_host_arch, the get_pretty_* trio and, in util, is_kvm_available, randomMAC, get_max_vcpus, compareMAC, pretty_mem, pretty_bytes and validate_uuid; with them the port constants only host_is_up read. Some were reached only by their tests, which go too. - The flavor edit page: nothing linked to it; flavors are created and deleted on the Flavors page and edited through the API. - computes.validate_name, UserForm.identity_has_errors, the has_perm template tag, the ALLOW_EMPTY_PASSWORD setting nothing reads, hidePleaseWaitDialog, default_app_config (ignored since Django 4.1) and the Python < 3.6 lines of secret_generator.py.
239 lines
9 KiB
Python
239 lines
9 KiB
Python
from decimal import ROUND_HALF_UP, Decimal
|
|
|
|
from django import forms
|
|
from django.conf import settings
|
|
from django.contrib.auth import password_validation
|
|
from django.contrib.auth.forms import AdminPasswordChangeForm
|
|
from django.contrib.auth.models import Group, User
|
|
from django.core.exceptions import ValidationError
|
|
from django.utils.translation import gettext_lazy as _
|
|
|
|
from accounts.forms import UniqueEmailMixin
|
|
from accounts.models import UserAttributes
|
|
from instances.models import Instance
|
|
|
|
from .models import Permission
|
|
|
|
|
|
# what each permission lets a user do, shown beside its switch
|
|
PERMISSION_HELP = {
|
|
"change_password": _("Change their own password on their profile page"),
|
|
"clone_instances": _("Clone the VMs they may change and the templates they may view, within their quota"),
|
|
"passwordless_console": _("Open a console without the VM's console password"),
|
|
"snapshot_instances": _("Take, revert and delete snapshots of the VMs they may change"),
|
|
"view_instances": _("See every VM and host in the lists, read-only"),
|
|
}
|
|
|
|
|
|
def permission_rows(field):
|
|
"""Each permission's checkbox of a bound field with what it lets a user do."""
|
|
return [(box, PERMISSION_HELP.get(box.data["value"].instance.codename, "")) for box in field]
|
|
|
|
|
|
class GroupForm(forms.ModelForm):
|
|
permissions = forms.ModelMultipleChoiceField(
|
|
widget=forms.CheckboxSelectMultiple,
|
|
queryset=Permission.objects.filter(content_type__model="permissionset"),
|
|
required=False,
|
|
)
|
|
|
|
users = forms.ModelMultipleChoiceField(
|
|
widget=forms.CheckboxSelectMultiple,
|
|
queryset=User.objects.order_by("username"),
|
|
required=False,
|
|
)
|
|
|
|
def __init__(self, *args, **kwargs):
|
|
super(GroupForm, self).__init__(*args, **kwargs)
|
|
instance = getattr(self, "instance", None)
|
|
if instance and instance.id:
|
|
self.fields["users"].initial = self.instance.user_set.all()
|
|
|
|
def permission_rows(self):
|
|
return permission_rows(self["permissions"])
|
|
|
|
def save_m2m(self):
|
|
self.instance.user_set.set(self.cleaned_data["users"])
|
|
|
|
def save(self, *args, **kwargs):
|
|
instance = super(GroupForm, self).save()
|
|
self.save_m2m()
|
|
return instance
|
|
|
|
class Meta:
|
|
model = Group
|
|
fields = "__all__"
|
|
|
|
|
|
class UserForm(UniqueEmailMixin, forms.ModelForm):
|
|
# one choice for is_staff and is_superuser: a superuser keeps the
|
|
# is_staff it had (it adds nothing to every permission)
|
|
role = forms.ChoiceField(
|
|
widget=forms.RadioSelect,
|
|
choices=[("user", _("User")), ("template_editor", _("Template editor")), ("superuser", _("Superuser"))],
|
|
initial="user",
|
|
)
|
|
|
|
user_permissions = forms.ModelMultipleChoiceField(
|
|
widget=forms.CheckboxSelectMultiple,
|
|
queryset=Permission.objects.filter(content_type__model="permissionset"),
|
|
label=_("Permissions"),
|
|
required=False,
|
|
)
|
|
|
|
groups = forms.ModelMultipleChoiceField(
|
|
widget=forms.CheckboxSelectMultiple,
|
|
queryset=Group.objects.all(),
|
|
label=_("Groups"),
|
|
required=False,
|
|
)
|
|
|
|
class Meta:
|
|
model = User
|
|
fields = [
|
|
"username",
|
|
"groups",
|
|
"first_name",
|
|
"last_name",
|
|
"email",
|
|
"user_permissions",
|
|
"is_active",
|
|
]
|
|
|
|
def __init__(self, *args, **kwargs):
|
|
super().__init__(*args, **kwargs)
|
|
if self.instance.is_superuser:
|
|
self.initial["role"] = "superuser"
|
|
elif self.instance.is_staff:
|
|
self.initial["role"] = "template_editor"
|
|
|
|
def clean(self):
|
|
cleaned = super().clean()
|
|
role = cleaned.get("role")
|
|
if role:
|
|
cleaned["is_superuser"] = self.instance.is_superuser = role == "superuser"
|
|
if role != "superuser":
|
|
self.instance.is_staff = role == "template_editor"
|
|
return cleaned
|
|
|
|
def permission_rows(self):
|
|
return permission_rows(self["user_permissions"])
|
|
|
|
class UserCreateForm(UserForm):
|
|
password = forms.CharField(widget=forms.PasswordInput)
|
|
|
|
def __init__(self, *args, **kwargs):
|
|
super().__init__(*args, **kwargs)
|
|
# New users may change their own password unless unticked here
|
|
self.fields["user_permissions"].initial = Permission.objects.filter(
|
|
content_type__app_label="accounts", codename="change_password"
|
|
)
|
|
|
|
def clean(self):
|
|
# the VMs a new user owns, by name: a name may repeat across hosts
|
|
cleaned = super().clean()
|
|
self.default_instances = []
|
|
for name in settings.NEW_USER_DEFAULT_INSTANCES:
|
|
found = list(Instance.objects.filter(name=name)[:2])
|
|
if len(found) != 1:
|
|
raise ValidationError(_("NEW_USER_DEFAULT_INSTANCES: %(name)s is not exactly one VM") % {"name": name})
|
|
self.default_instances += found
|
|
return cleaned
|
|
|
|
def _post_clean(self):
|
|
# The validators in AUTH_PASSWORD_VALIDATORS, as on the password change
|
|
# forms; after the instance has the form's values (username, email).
|
|
super()._post_clean()
|
|
password = self.cleaned_data.get("password")
|
|
if password:
|
|
try:
|
|
password_validation.validate_password(password, self.instance)
|
|
except ValidationError as error:
|
|
self.add_error("password", error)
|
|
|
|
def save(self, commit=True):
|
|
# hashed before the user is first written: the raw password never
|
|
# reaches the database
|
|
user = super().save(commit=False)
|
|
user.set_password(self.cleaned_data["password"])
|
|
if commit:
|
|
user.save()
|
|
self.save_m2m()
|
|
return user
|
|
|
|
class Meta(UserForm.Meta):
|
|
fields = [
|
|
"username",
|
|
"password",
|
|
"groups",
|
|
"first_name",
|
|
"last_name",
|
|
"email",
|
|
"user_permissions",
|
|
"is_active",
|
|
]
|
|
|
|
|
|
# the quota's limits, with the unit each is entered in (memory in GiB, kept in MiB)
|
|
QUOTA_UNITS = {"max_instances": "", "max_cpus": "", "max_memory": "GiB", "max_disk_size": "GiB"}
|
|
|
|
|
|
class UserAttributesForm(forms.ModelForm):
|
|
"""The quota: each limit a number from 0 (none allowed) or No limit,
|
|
which is kept as -1; memory is entered in GiB and kept in MiB."""
|
|
|
|
max_instances = forms.IntegerField(label=_("VMs"), min_value=0, required=False)
|
|
max_cpus = forms.IntegerField(label=_("vCPUs"), min_value=0, required=False)
|
|
max_memory = forms.DecimalField(label=_("Memory"), min_value=0, required=False)
|
|
max_disk_size = forms.IntegerField(label=_("Disk"), min_value=0, required=False)
|
|
|
|
class Meta:
|
|
model = UserAttributes
|
|
fields = list(QUOTA_UNITS)
|
|
|
|
def __init__(self, *args, **kwargs):
|
|
super().__init__(*args, **kwargs)
|
|
for name in QUOTA_UNITS:
|
|
limit = getattr(self.instance, name) # a new user's: the model's default
|
|
self.fields[f"{name}_unlimited"] = forms.BooleanField(label=_("No limit"), required=False, initial=limit == -1)
|
|
if limit == -1:
|
|
self.initial[name] = None
|
|
elif name == "max_memory":
|
|
# exact: a MiB count over 1024 always ends, so saving it again changes nothing
|
|
self.initial[name] = format(Decimal(limit) / 1024, "f")
|
|
else:
|
|
self.initial[name] = limit
|
|
|
|
def clean(self):
|
|
cleaned = super().clean()
|
|
for name in QUOTA_UNITS:
|
|
if cleaned.get(f"{name}_unlimited"):
|
|
cleaned[name] = -1
|
|
elif cleaned.get(name) is None:
|
|
if name not in self.errors:
|
|
self.add_error(name, _("Enter a limit or choose No limit."))
|
|
elif name == "max_memory":
|
|
cleaned[name] = int((cleaned[name] * 1024).to_integral_value(ROUND_HALF_UP))
|
|
return cleaned
|
|
|
|
def quota_rows(self):
|
|
"""Each limit's field, its No limit box and its unit, for the form page."""
|
|
return [(self[name], self[f"{name}_unlimited"], unit) for name, unit in QUOTA_UNITS.items()]
|
|
|
|
|
|
class AdminPasswordForm(AdminPasswordChangeForm):
|
|
"""A password an administrator sets; the user may have to change it at their next login."""
|
|
must_change_password = forms.BooleanField(label=_("The user must change it at their next login"), required=False)
|
|
|
|
def __init__(self, *args, **kwargs):
|
|
super().__init__(*args, **kwargs)
|
|
self.fields["must_change_password"] = self.fields.pop("must_change_password") # last, after Django's own
|
|
# Django's "Password-based authentication: Enabled/Disabled", said as how the user signs in
|
|
self.fields["usable_password"].choices = [("true", _("With a password")), ("false", _("Without a password"))]
|
|
|
|
def clean(self):
|
|
cleaned = super().clean()
|
|
# the change form asks for the old password: a user without one could never pass it
|
|
if cleaned.get("must_change_password") and not cleaned.get("set_usable_password", True):
|
|
raise ValidationError(_("A user without a password cannot be made to change it"))
|
|
return cleaned
|