# -*- mode: python; coding: utf-8 -*- # Re2o est un logiciel d'administration développé initiallement au rezometz. Il # se veut agnostique au réseau considéré, de manière à être installable en # quelques clics. # # Copyright © 2017-2020 Gabriel Détraz # Copyright © 2017-2020 Lara Kermarec # Copyright © 2017-2020 Augustin Lemesle # Copyright © 2017-2020 Hugo Levy--Falk # Copyright © 2017-2020 Jean-Romain Garnier # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License along # with this program; if not, write to the Free Software Foundation, Inc., # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. """ Definition des forms pour l'application users. Modification, creation de : - un user (informations personnelles) - un bannissement - le mot de passe d'un user - une whiteliste - un user de service """ from __future__ import unicode_literals from django import forms from django.forms import ModelForm, Form from django.contrib.auth.forms import ReadOnlyPasswordHashField from django.contrib.auth.password_validation import validate_password, password_validators_help_text_html from django.core.validators import MinLengthValidator from django.utils import timezone from django.utils.functional import lazy from django.contrib.auth.models import Group, Permission from django.utils.translation import ugettext_lazy as _ from django.utils.safestring import mark_safe from machines.models import Interface, Machine, Nas from topologie.models import Port from preferences.models import OptionalUser from re2o.utils import remove_user_room from re2o.base import get_input_formats_help_text from re2o.mixins import FormRevMixin from re2o.field_permissions import FieldPermissionFormMixin from preferences.models import GeneralOption from .widgets import DateTimePicker from .models import ( User, ServiceUser, School, ListRight, Whitelist, EMailAddress, ListShell, Ban, Adherent, Club, ) class PassForm(FormRevMixin, FieldPermissionFormMixin, forms.ModelForm): """Formulaire de changement de mot de passe. Verifie que les 2 nouveaux mots de passe renseignés sont identiques et respectent une norme""" selfpasswd = forms.CharField( label=_("Current password"), max_length=255, widget=forms.PasswordInput ) passwd1 = forms.CharField( label=_("New password"), max_length=255, widget=forms.PasswordInput, help_text=password_validators_help_text_html() ) passwd2 = forms.CharField( label=_("New password confirmation"), max_length=255, widget=forms.PasswordInput, ) class Meta: model = User fields = [] def clean_passwd2(self): """Verifie que passwd1 et 2 sont identiques""" # Check that the two password entries match password1 = self.cleaned_data.get("passwd1") password2 = self.cleaned_data.get("passwd2") if password1 and password2 and password1 != password2: raise forms.ValidationError(_("The new passwords don't match.")) validate_password(password1, user=self.instance) return password2 def clean_selfpasswd(self): """Verifie si il y a lieu que le mdp self est correct""" if not self.instance.check_password(self.cleaned_data.get("selfpasswd")): raise forms.ValidationError(_("The current password is incorrect.")) return def save(self, commit=True): """Changement du mot de passe""" user = super(PassForm, self).save(commit=False) user.set_password(self.cleaned_data.get("passwd1")) user.state = User.STATE_NOT_YET_ACTIVE user.set_active() user.save() class UserAdminForm(FormRevMixin, forms.ModelForm): """A form for creating new and editing users. Includes all the required fields, plus a repeated password. Formulaire pour la création d'un user. N'est utilisé que pour l'admin, lors de la creation d'un user par admin. Inclu tous les champs obligatoires""" password1 = forms.CharField( label=_("Password"), widget=forms.PasswordInput, max_length=255, help_text=password_validators_help_text_html(), required=False, ) password2 = forms.CharField( label=_("Password confirmation"), widget=forms.PasswordInput, max_length=255, required=False, ) def __init__(self, *args, **kwargs): prefix = kwargs.pop("prefix", self.Meta.model.__name__) super(UserAdminForm, self).__init__(*args, prefix=prefix, **kwargs) self.fields["email"].required = True class Meta: fields = ("pseudo", "surname", "name", "email", "is_superuser") def clean_password2(self): """Verifie que password1 et 2 sont identiques""" # Check that the two password entries match password1 = self.cleaned_data.get("password1") password2 = self.cleaned_data.get("password2") if password1 and password2: if password1 and password2 and password1 != password2: raise forms.ValidationError(_("The passwords don't match.")) validate_password(password1) return password2 def save(self, commit=True): # Save the provided password in hashed format user = super(UserAdminForm, self).save(commit=False) if self.cleaned_data["password1"]: user.set_password(self.cleaned_data["password1"]) user.save() return user class ServiceUserAdminForm(FormRevMixin, forms.ModelForm): """A form for creating new users. Includes all the required fields, plus a repeated password. Formulaire pour la creation de nouveaux serviceusers. Requiert seulement un mot de passe; et un pseudo""" password1 = forms.CharField( label=_("Password"), widget=forms.PasswordInput, max_length=255, ) password2 = forms.CharField( label=_("Password confirmation"), widget=forms.PasswordInput, max_length=255, ) def __init__(self, *args, **kwargs): prefix = kwargs.pop("prefix", self.Meta.model.__name__) super(ServiceUserAdminForm, self).__init__(*args, prefix=prefix, **kwargs) class Meta: model = ServiceUser fields = ("pseudo",) def clean_password2(self): """Verifie que password1 et 2 sont identiques""" # Check that the two password entries match password1 = self.cleaned_data.get("password1") password2 = self.cleaned_data.get("password2") if password1 and password2 and password1 != password2: raise forms.ValidationError(_("The passwords don't match.")) return password2 def save(self, commit=True): # Save the provided password in hashed format user = super(ServiceUserAdminForm, self).save(commit=False) user.set_password(self.cleaned_data["password1"]) user.save() return user class ResetPasswordForm(forms.Form): """Formulaire de demande de reinitialisation de mot de passe, mdp oublié""" pseudo = forms.CharField(label=_("Username"), max_length=255) email = forms.EmailField(max_length=255) class MassArchiveForm(forms.Form): """Formulaire d'archivage des users inactif. Prend en argument du formulaire la date de depart avant laquelle archiver les users""" date = forms.DateTimeField(help_text="%d/%m/%y") full_archive = forms.BooleanField( label=_( "Fully archive users? WARNING: CRITICAL OPERATION IF TRUE" ), initial=False, required=False, ) def clean(self): cleaned_data = super(MassArchiveForm, self).clean() date = cleaned_data.get("date") if date: if date > timezone.now(): raise forms.ValidationError( _( "Impossible to archive users" " whose end access date is in" " the future." ) ) class AdherentForm(FormRevMixin, FieldPermissionFormMixin, ModelForm): """Formulaire de base d'edition d'un user. Formulaire de base, utilisé pour l'edition de self par self ou un cableur. On formate les champs avec des label plus jolis""" def __init__(self, *args, **kwargs): prefix = kwargs.pop("prefix", self.Meta.model.__name__) super(AdherentForm, self).__init__(*args, prefix=prefix, **kwargs) self.fields["name"].label = _("First name") self.fields["surname"].label = _("Surname") self.fields["email"].label = _("Email address") self.fields["school"].label = _("School") self.fields["comment"].label = _("Comment") self.fields["profile_image"].label = _("Profile Image") if "room" in self.fields: self.fields["room"].label = _("Room") self.fields["room"].empty_label = _("No room") self.fields["school"].empty_label = _("Select a school") class Meta: model = Adherent fields = [ "name", "surname", "pseudo", "email", "school", "comment", "telephone", "profile_image", "room", ] force = forms.BooleanField( label=_("Force the move?"), initial=False, required=False ) def clean_telephone(self): """Verifie que le tel est présent si 'option est validée dans preferences""" telephone = self.cleaned_data["telephone"] if not telephone and OptionalUser.get_cached_value("is_tel_mandatory"): raise forms.ValidationError(_("A valid telephone number is required.")) return telephone def clean_force(self): """On supprime l'ancien user de la chambre si et seulement si la case est cochée""" room = self.cleaned_data.get("room") if self.cleaned_data.get("force", False) and room: remove_user_room(room) return def clean_room(self): """On supprime l'ancien user de la chambre si l'option est activée, et que l'ancien user a une connexion désactivée""" # Handle case where regular users can force move room = self.cleaned_data.get("room") room_policy = OptionalUser.get_cached_value("self_room_policy") if room_policy == OptionalUser.DISABLED or not room: return room # Remove the previous user's room, if allowed and necessary remove_user_room(room, force=bool(room_policy == OptionalUser.ALL_ROOM)) # Run standard clean process return room class AdherentCreationForm(AdherentForm): """Formulaire de création d'un user. AdherentForm auquel on ajoute une checkbox afin d'éviter les doublons d'utilisateurs et, optionnellement, un champ mot de passe""" # Champ pour choisir si un lien est envoyé par mail pour le mot de passe init_password_by_mail_info = _( "If this options is set, you will receive a link to set" " your initial password by email. If you do not have" " any means of accessing your emails, you can disable" " this option to set your password immediatly." " You will still receive an email to confirm your address." " Failure to confirm your address will result in an" " automatic suspension of your account until you do." ) init_password_by_mail = forms.BooleanField( help_text=init_password_by_mail_info, required=False, initial=True ) init_password_by_mail.label = _("Send password reset link by email.") # Champs pour initialiser le mot de passe # Validators are handled manually since theses fields aren't always required password1 = forms.CharField( required=False, label=_("Password"), widget=forms.PasswordInput, max_length=255, help_text=password_validators_help_text_html() ) password2 = forms.CharField( required=False, label=_("Password confirmation"), widget=forms.PasswordInput, max_length=255, ) # Champ permettant d'éviter au maxium les doublons d'utilisateurs former_user_check_info = _( "If you already have an account, please use it. If your lost access to" " it, please consider using the forgotten password button on the" " login page or contacting support." ) former_user_check = forms.BooleanField( required=True, help_text=former_user_check_info ) former_user_check.label = _("I certify that I have not had an account before.") # Checkbox for GTU gtu_check = forms.BooleanField(required=True) class Meta: model = Adherent fields = [ "name", "surname", "pseudo", "email", "school", "comment", "telephone", "profile_image", "room", "state", ] def __init__(self, *args, **kwargs): super(AdherentCreationForm, self).__init__(*args, **kwargs) gtu_file = GeneralOption.get_cached_value("GTU") self.fields["email"].required = True self.fields["gtu_check"].label = mark_safe( "%s %s." % ( _("I commit to accept the"), gtu_file.url if gtu_file else "#", _("General Terms of Use"), ) ) # Remove password fields if option is disabled if not OptionalUser.get_cached_value("allow_set_password_during_user_creation"): self.fields.pop("init_password_by_mail") self.fields.pop("password1") self.fields.pop("password2") def clean_password2(self): """Verifie que password1 et 2 sont identiques (si nécessaire)""" send_email = self.cleaned_data.get("init_password_by_mail") if send_email: return None # Check that the two password entries match password1 = self.cleaned_data.get("password1") password2 = self.cleaned_data.get("password2") if password1 and password2 and password1 != password2: raise forms.ValidationError(_("The passwords don't match.")) validate_password(password1) return password2 def save(self, commit=True): """Set the user's password, if entered Returns the user and a bool indicating whether an email to init the password should be sent""" # Save the provided password in hashed format user = super(AdherentForm, self).save(commit=False) is_set_password_allowed = OptionalUser.get_cached_value("allow_set_password_during_user_creation") set_passwd = is_set_password_allowed and not self.cleaned_data.get("init_password_by_mail") if set_passwd: user.set_password(self.cleaned_data["password1"]) user.save() return user class AdherentEditForm(AdherentForm): """Formulaire d'édition d'un user. AdherentForm incluant la modification des champs gpg et shell""" def __init__(self, *args, **kwargs): super(AdherentEditForm, self).__init__(*args, **kwargs) self.fields["gpg_fingerprint"].widget.attrs["placeholder"] = _( "Leave empty if you don't have any GPG key." ) self.user = kwargs["instance"] self.fields["email"].required = bool(self.user.email) if "shell" in self.fields: self.fields["shell"].empty_label = _("Default shell") class Meta: model = Adherent fields = [ "name", "surname", "pseudo", "email", "school", "comment", "telephone", "profile_image", "room", "shell", "gpg_fingerprint", "shortcuts_enabled", ] class ClubForm(FormRevMixin, FieldPermissionFormMixin, ModelForm): """Formulaire de base d'edition d'un user. Formulaire de base, utilisé pour l'edition de self par self ou un cableur. On formate les champs avec des label plus jolis""" def __init__(self, *args, **kwargs): prefix = kwargs.pop("prefix", self.Meta.model.__name__) super(ClubForm, self).__init__(*args, prefix=prefix, **kwargs) self.fields["surname"].label = _("Name") self.fields["school"].label = _("School") self.fields["comment"].label = _("Comment") self.fields["email"].label = _("Email address") if "room" in self.fields: self.fields["room"].label = _("Room") self.fields["room"].empty_label = _("No room") self.fields["school"].empty_label = _("Select a school") self.fields["mailing"].label = _("Use a mailing list") class Meta: model = Club fields = [ "surname", "pseudo", "school", "comment", "room", "email", "telephone", "profile_image", "email", "shell", "mailing", ] def clean_telephone(self): """Verifie que le tel est présent si 'option est validée dans preferences""" telephone = self.cleaned_data["telephone"] if not telephone and OptionalUser.get_cached_value("is_tel_mandatory"): raise forms.ValidationError(_("A valid telephone number is required.")) return telephone class ClubAdminandMembersForm(FormRevMixin, ModelForm): """Permet d'éditer la liste des membres et des administrateurs d'un club""" class Meta: model = Club fields = ["administrators", "members"] def __init__(self, *args, **kwargs): prefix = kwargs.pop("prefix", self.Meta.model.__name__) super(ClubAdminandMembersForm, self).__init__(*args, prefix=prefix, **kwargs) class PasswordForm(FormRevMixin, ModelForm): """ Formulaire de changement brut de mot de passe. Ne pas utiliser sans traitement""" class Meta: model = User fields = ["password", "pwd_ntlm"] def __init__(self, *args, **kwargs): prefix = kwargs.pop("prefix", self.Meta.model.__name__) super(PasswordForm, self).__init__(*args, prefix=prefix, **kwargs) class ServiceUserForm(FormRevMixin, ModelForm): """Service user creation force initial password set""" password = forms.CharField( label=_("New password"), max_length=255, validators=[MinLengthValidator(8)], widget=forms.PasswordInput, required=True, ) class Meta: model = ServiceUser fields = ("pseudo", "access_group", "comment") def __init__(self, *args, **kwargs): prefix = kwargs.pop("prefix", self.Meta.model.__name__) super(ServiceUserForm, self).__init__(*args, prefix=prefix, **kwargs) def save(self, commit=True): """Password change""" user = super(ServiceUserForm, self).save(commit=False) if self.cleaned_data["password"]: user.set_password(self.cleaned_data.get("password")) user.save() class EditServiceUserForm(ServiceUserForm): """Formulaire d'edition de base d'un service user. Ne permet d'editer que son group d'acl et son commentaire""" password = forms.CharField( label=_("New password"), max_length=255, validators=[MinLengthValidator(8)], widget=forms.PasswordInput, required=False, ) class Meta(ServiceUserForm.Meta): fields = ["access_group", "comment"] class StateForm(FormRevMixin, ModelForm): """Change state of an user, and if its main email is verified or not""" class Meta: model = User fields = ["state", "email_state"] def __init__(self, *args, **kwargs): prefix = kwargs.pop("prefix", self.Meta.model.__name__) super(StateForm, self).__init__(*args, prefix=prefix, **kwargs) self.fields["state"].label = _("State") self.fields["email_state"].label = _("Email state") class GroupForm(FieldPermissionFormMixin, FormRevMixin, ModelForm): """ Gestion des groupes d'un user""" groups = forms.ModelMultipleChoiceField( Group.objects.all(), widget=forms.CheckboxSelectMultiple, required=False ) class Meta: model = User fields = ["is_superuser", "groups"] def __init__(self, *args, **kwargs): prefix = kwargs.pop("prefix", self.Meta.model.__name__) super(GroupForm, self).__init__(*args, prefix=prefix, **kwargs) if "is_superuser" in self.fields: self.fields["is_superuser"].label = _("Superuser") class SchoolForm(FormRevMixin, ModelForm): """Edition, creation d'un école""" class Meta: model = School fields = ["name"] def __init__(self, *args, **kwargs): prefix = kwargs.pop("prefix", self.Meta.model.__name__) super(SchoolForm, self).__init__(*args, prefix=prefix, **kwargs) self.fields["name"].label = _("School") class ShellForm(FormRevMixin, ModelForm): """Edition, creation d'un école""" class Meta: model = ListShell fields = ["shell"] def __init__(self, *args, **kwargs): prefix = kwargs.pop("prefix", self.Meta.model.__name__) super(ShellForm, self).__init__(*args, prefix=prefix, **kwargs) self.fields["shell"].label = _("Shell name") class ListRightForm(FormRevMixin, ModelForm): """Edition, d'un groupe , équivalent à un droit Ne permet pas d'editer le gid, car il sert de primary key""" permissions = forms.ModelMultipleChoiceField( Permission.objects.all().select_related("content_type"), widget=forms.CheckboxSelectMultiple, required=False, ) class Meta: model = ListRight fields = ("name", "unix_name", "critical", "permissions", "details") def __init__(self, *args, **kwargs): prefix = kwargs.pop("prefix", self.Meta.model.__name__) super(ListRightForm, self).__init__(*args, prefix=prefix, **kwargs) self.fields["unix_name"].label = _("Name of the group of rights") class NewListRightForm(ListRightForm): """Ajout d'un groupe/list de droit """ class Meta(ListRightForm.Meta): fields = ("name", "unix_name", "gid", "critical", "permissions", "details") def __init__(self, *args, **kwargs): super(NewListRightForm, self).__init__(*args, **kwargs) self.fields["gid"].label = _( "GID. Warning: this field must not be edited after creation." ) class DelListRightForm(Form): """Suppression d'un ou plusieurs groupes""" listrights = forms.ModelMultipleChoiceField( queryset=ListRight.objects.none(), label=_("Current groups of rights"), widget=forms.CheckboxSelectMultiple, ) def __init__(self, *args, **kwargs): instances = kwargs.pop("instances", None) super(DelListRightForm, self).__init__(*args, **kwargs) if instances: self.fields["listrights"].queryset = instances else: self.fields["listrights"].queryset = ListRight.objects.all() class DelSchoolForm(Form): """Suppression d'une ou plusieurs écoles""" schools = forms.ModelMultipleChoiceField( queryset=School.objects.none(), label=_("Current schools"), widget=forms.CheckboxSelectMultiple, ) def __init__(self, *args, **kwargs): instances = kwargs.pop("instances", None) super(DelSchoolForm, self).__init__(*args, **kwargs) if instances: self.fields["schools"].queryset = instances else: self.fields["schools"].queryset = School.objects.all() class BanForm(FormRevMixin, ModelForm): """Creation, edition d'un objet bannissement""" def __init__(self, *args, **kwargs): prefix = kwargs.pop("prefix", self.Meta.model.__name__) super(BanForm, self).__init__(*args, prefix=prefix, **kwargs) self.fields["date_end"].label = _("End date") self.fields["date_end"].localize = False class Meta: model = Ban exclude = ["user"] widgets = {"date_end": DateTimePicker} class WhitelistForm(FormRevMixin, ModelForm): """Creation, edition d'un objet whitelist""" def __init__(self, *args, **kwargs): prefix = kwargs.pop("prefix", self.Meta.model.__name__) super(WhitelistForm, self).__init__(*args, prefix=prefix, **kwargs) self.fields["date_end"].label = _("End date") self.fields["date_end"].localize = False class Meta: model = Whitelist exclude = ["user"] widgets = {"date_end": DateTimePicker} class EMailAddressForm(FormRevMixin, ModelForm): """Create and edit a local email address""" def __init__(self, *args, **kwargs): prefix = kwargs.pop("prefix", self.Meta.model.__name__) super(EMailAddressForm, self).__init__(*args, prefix=prefix, **kwargs) self.fields["local_part"].label = _("Local part of the email address") self.fields["local_part"].help_text = _("Can't contain @.") def clean_local_part(self): return self.cleaned_data.get("local_part").lower() class Meta: model = EMailAddress exclude = ["user"] class EmailSettingsForm(FormRevMixin, FieldPermissionFormMixin, ModelForm): """Edit email-related settings""" def __init__(self, *args, **kwargs): prefix = kwargs.pop("prefix", self.Meta.model.__name__) super(EmailSettingsForm, self).__init__(*args, prefix=prefix, **kwargs) self.user = kwargs["instance"] self.fields["email"].label = _("Main email address") self.fields["email"].required = bool(self.user.email) if "local_email_redirect" in self.fields: self.fields["local_email_redirect"].label = _("Redirect local emails") if "local_email_enabled" in self.fields: self.fields["local_email_enabled"].label = _("Use local emails") class Meta: model = User fields = ["email", "local_email_enabled", "local_email_redirect"] class InitialRegisterForm(forms.Form): register_room = forms.BooleanField(required=False) register_machine = forms.BooleanField(required=False) def __init__(self, *args, **kwargs): switch_ip = kwargs.pop("switch_ip") switch_port = kwargs.pop("switch_port") client_mac = kwargs.pop("client_mac") self.user = kwargs.pop("user") if switch_ip and switch_port: # Looking for a port port = Port.objects.filter( switch__interface__ipv4__ipv4=switch_ip, port=switch_port ).first() # If a port exists, checking there is a room AND radius if port: if ( port.get_port_profile.radius_type != "NO" and port.get_port_profile.radius_mode == "STRICT" and hasattr(port, "room") ): # Requesting user is not in this room ? if self.user.room != port.room: self.new_room = port.room if client_mac and switch_ip: # If this interface doesn't already exists if not Interface.objects.filter(mac_address=client_mac): self.mac_address = client_mac self.nas_type = Nas.objects.filter( nas_type__interface__ipv4__ipv4=switch_ip ).first() super(InitialRegisterForm, self).__init__(*args, **kwargs) if hasattr(self, "new_room"): self.fields["register_room"].label = _("This room is my room") else: self.fields.pop("register_room") if hasattr(self, "mac_address"): self.fields["register_machine"].label = _( "This new connected device is mine" ) else: self.fields.pop("register_machine") def clean_register_room(self): if self.cleaned_data["register_room"]: if self.user.is_class_adherent: remove_user_room(self.new_room) user = self.user.adherent user.room = self.new_room user.save() if self.user.is_class_club: user = self.user.club user.room = self.new_room user.save() def clean_register_machine(self): if self.cleaned_data["register_machine"]: if self.mac_address and self.nas_type: self.user.autoregister_machine(self.mac_address, self.nas_type)