1
0
Fork 0
mirror of https://github.com/retspen/webvirtcloud synced 2026-11-08 11:16:47 +00:00
webvirtcloud/accounts/forms.py
catborise 1c60696ea1 feat(accounts): API tokens for scripts and tools
The REST API took only the login session, so a script could not use it
without a browser. A user now makes API tokens on the profile page (API
Tokens tab): a name, and an expiry of 30, 90 or 365 days or none. The
token is shown once, on a page that is not cached, and then kept only as
a SHA-512 digest; the list shows its name, first characters, creation
and expiry, and a token is deleted there. Tools send it as
"Authorization: Token wvc_...", and act as its user, with that user's
permissions. Making and deleting a token is logged, without the token.

A password alone (HTTP Basic) still does not reach the API, as it would
skip the OTP second factor: a token is made in a session that passed the
login and its OTP. The token class comes first, so a refused request gets
401 with its challenge instead of 403.

django-rest-knox 5.1.0 provides the tokens (accounts.ApiToken adds the
name); settings.py takes "knox", KNOX_TOKEN_MODEL and REST_KNOX from the
template.
2026-10-24 23:14:27 +03:00

101 lines
3.5 KiB
Python

from appsettings.settings import app_settings
from django.contrib.auth import get_user_model
from django.contrib.auth.forms import AuthenticationForm
from django.core.validators import MaxLengthValidator
from django.forms import CharField, EmailField, Form, ModelForm, TypedChoiceField, ValidationError
from django.utils.translation import gettext_lazy as _
from .models import UserInstance, UserSSHKey
from .utils import validate_ssh_key
class UserInstanceForm(ModelForm):
def __init__(self, *args, **kwargs):
super(UserInstanceForm, self).__init__(*args, **kwargs)
# Make user and instance fields not editable after creation
instance = getattr(self, "instance", None)
if instance and instance.id is not None:
self.fields["user"].disabled = True
self.fields["instance"].disabled = True
def clean_instance(self):
instance = self.cleaned_data["instance"]
if app_settings.ALLOW_INSTANCE_MULTIPLE_OWNER == "False":
# the ownership being edited is not another owner
exists = UserInstance.objects.filter(instance=instance).exclude(pk=self.instance.pk)
if exists:
raise ValidationError(_("Instance owned by another user"))
return instance
class Meta:
model = UserInstance
fields = "__all__"
class ProfileForm(ModelForm):
class Meta:
model = get_user_model()
fields = ("first_name", "last_name", "email")
class UserSSHKeyForm(ModelForm):
def __init__(self, *args, **kwargs):
self.user = kwargs.pop("user", None)
self.publickeys = UserSSHKey.objects.filter(user=self.user)
super().__init__(*args, **kwargs)
def clean_keyname(self):
for key in self.publickeys:
if self.cleaned_data["keyname"] == key.keyname:
raise ValidationError(_("Key name already exist"))
return self.cleaned_data["keyname"]
def clean_keypublic(self):
for key in self.publickeys:
if self.cleaned_data["keypublic"] == key.keypublic:
raise ValidationError(_("Public key already exist"))
if not validate_ssh_key(self.cleaned_data["keypublic"]):
raise ValidationError(_("Invalid key"))
return self.cleaned_data["keypublic"]
def save(self, commit=True):
ssh_key = super().save(commit=False)
ssh_key.user = self.user
if commit:
ssh_key.save()
return ssh_key
class Meta:
model = UserSSHKey
fields = ("keyname", "keypublic")
class ApiTokenForm(Form):
name = CharField(label=_("Name"), max_length=64, help_text=_("What uses it, such as a script or a host"))
days = TypedChoiceField(
label=_("Expires"), coerce=int, empty_value=None, required=False, initial=90,
choices=[(30, _("in 30 days")), (90, _("in 90 days")), (365, _("in a year")), ("", _("never"))],
)
class EmailOTPForm(Form):
email = EmailField(label=_("Email"))
class BoundedUsernameMixin:
"""Django's login form gives the username its max_length after the field
is built, so a longer one is not refused and reaches authentication;
django-axes then cannot store it (PostgreSQL refuses it: 500)."""
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
username = self.fields["username"]
username.validators.append(MaxLengthValidator(username.max_length))
class LoginForm(BoundedUsernameMixin, AuthenticationForm):
pass