diff --git a/freeradius_utils/auth.py b/freeradius_utils/auth.py index 64ddbc9d..5ea4e48c 100644 --- a/freeradius_utils/auth.py +++ b/freeradius_utils/auth.py @@ -257,12 +257,15 @@ def check_user_machine_and_register(nas_type, username, mac_address): return (True, u'Access Ok, Capture de la mac...', user.pwd_ntlm) else: return (False, u'Erreur dans le register mac %s' % reason, '') + else: + return (False, u'Machine inconnue', '') else: return (False, u"Machine inconnue", '') def decide_vlan_and_register_switch(nas, nas_type, port_number, mac_address): # Get port from switch and port number + extra_log = "" if not nas: return ('?', u'Nas inconnu', VLAN_OK) @@ -273,9 +276,15 @@ def decide_vlan_and_register_switch(nas, nas_type, port_number, mac_address): return (sw_name, u'Port inconnu', VLAN_OK) port = port.first() + # Si un vlan a été précisé, on l'utilise pour VLAN_OK + if port.vlan_force: + DECISION_VLAN = int(port.vlan_force.vlan_id) + extra_log = u"Force sur vlan " + str(DECISION_VLAN) + else: + DECISION_VLAN = VLAN_OK if port.radius == 'NO': - return (sw_name, u"Pas d'authentification sur ce port", VLAN_OK) + return (sw_name, u"Pas d'authentification sur ce port" + extra_log, DECISION_VLAN) if port.radius == 'BLOQ': return (sw_name, u'Port desactive', VLAN_NOK) @@ -309,16 +318,12 @@ def decide_vlan_and_register_switch(nas, nas_type, port_number, mac_address): else: result, reason = room_user.first().autoregister_machine(mac_address, nas_type) if result: - return (sw_name, u'Access Ok, Capture de la mac...', VLAN_OK) + return (sw_name, u'Access Ok, Capture de la mac...' + extra_log, DECISION_VLAN) else: return (sw_name, u'Erreur dans le register mac %s' % reason + unicode(mac_address), VLAN_NOK) elif not interface.first().is_active: return (sw_name, u'Machine non active / adherent non cotisant', VLAN_NOK) else: - return (sw_name, u'Machine OK', VLAN_OK) - - # On gere bien tous les autres états possibles, il ne reste que le VLAN en dur - return (sw_name, u'VLAN impose', int(port.radius)) - + return (sw_name, u'Machine OK' + extra_log, DECISION_VLAN) diff --git a/machines/templates/machines/aff_machines.html b/machines/templates/machines/aff_machines.html index db3cdd37..3e26f64b 100644 --- a/machines/templates/machines/aff_machines.html +++ b/machines/templates/machines/aff_machines.html @@ -96,13 +96,11 @@ with this program; if not, write to the Free Software Foundation, Inc., Gerer les alias - {% if interface.may_have_port_open %}
  • Gerer la configuration des ports
  • - {% endif %}
  • Historique diff --git a/machines/views.py b/machines/views.py index b286db75..580ad4d8 100644 --- a/machines/views.py +++ b/machines/views.py @@ -1002,8 +1002,7 @@ def configure_ports(request, pk): messages.error(request, u"Interface inexistante" ) return redirect("/machines") if not interface_instance.may_have_port_open(): - messages.error(request, "L'ip de cette interface n'est pas publique ou non assignée") - return redirect("/machines") + messages.error(request, "Attention, l'ipv4 n'est pas publique, l'ouverture n'aura pas d'effet en v4") interface = EditOuverturePortConfigForm(request.POST or None, instance=interface_instance) if interface.is_valid(): interface.save() diff --git a/topologie/admin.py b/topologie/admin.py index d1657330..8dcce849 100644 --- a/topologie/admin.py +++ b/topologie/admin.py @@ -29,16 +29,16 @@ from reversion.admin import VersionAdmin from .models import Port, Room, Switch, Stack class StackAdmin(VersionAdmin): - list_display = ('name', 'stack_id', 'details') + pass class SwitchAdmin(VersionAdmin): - list_display = ('switch_interface','location','number','details') + pass class PortAdmin(VersionAdmin): - list_display = ('switch', 'port','room','machine_interface','radius','details') + pass class RoomAdmin(VersionAdmin): - list_display = ('name','details') + pass admin.site.register(Port, PortAdmin) admin.site.register(Room, RoomAdmin) diff --git a/topologie/forms.py b/topologie/forms.py index 1f826809..87a3917d 100644 --- a/topologie/forms.py +++ b/topologie/forms.py @@ -33,7 +33,7 @@ class PortForm(ModelForm): class EditPortForm(ModelForm): class Meta(PortForm.Meta): - fields = ['room', 'related', 'machine_interface', 'radius', 'details'] + fields = ['room', 'related', 'machine_interface', 'radius', 'vlan_force', 'details'] def __init__(self, *args, **kwargs): super(EditPortForm, self).__init__(*args, **kwargs) @@ -42,7 +42,7 @@ class EditPortForm(ModelForm): class AddPortForm(ModelForm): class Meta(PortForm.Meta): - fields = ['port', 'room', 'machine_interface', 'related', 'radius', 'details'] + fields = ['port', 'room', 'machine_interface', 'related', 'radius', 'vlan_force', 'details'] class StackForm(ModelForm): class Meta: diff --git a/topologie/migrations/0029_auto_20171002_0334.py b/topologie/migrations/0029_auto_20171002_0334.py new file mode 100644 index 00000000..73ddff1b --- /dev/null +++ b/topologie/migrations/0029_auto_20171002_0334.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.7 on 2017-10-02 01:34 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('topologie', '0028_auto_20170913_1503'), + ] + + operations = [ + migrations.AlterField( + model_name='port', + name='radius', + field=models.CharField(choices=[('NO', 'NO'), ('STRICT', 'STRICT'), ('BLOQ', 'BLOQ'), ('COMMON', 'COMMON'), ('3', '3'), ('7', '7'), ('8', '8'), ('13', '13'), ('20', '20'), ('42', '42'), ('69', '69')], default='NO', max_length=32), + ), + ] diff --git a/topologie/migrations/0030_auto_20171004_0235.py b/topologie/migrations/0030_auto_20171004_0235.py new file mode 100644 index 00000000..83f3b022 --- /dev/null +++ b/topologie/migrations/0030_auto_20171004_0235.py @@ -0,0 +1,26 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.7 on 2017-10-04 00:35 +from __future__ import unicode_literals + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('topologie', '0029_auto_20171002_0334'), + ] + + operations = [ + migrations.AddField( + model_name='port', + name='vlan_force', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='machines.Vlan'), + ), + migrations.AlterField( + model_name='port', + name='radius', + field=models.CharField(choices=[('NO', 'NO'), ('STRICT', 'STRICT'), ('BLOQ', 'BLOQ'), ('COMMON', 'COMMON')], default='NO', max_length=32), + ), + ] diff --git a/topologie/models.py b/topologie/models.py index 0aafa24e..7ac10243 100644 --- a/topologie/models.py +++ b/topologie/models.py @@ -91,23 +91,20 @@ class Switch(models.Model): class Port(models.Model): PRETTY_NAME = "Port de switch" - STATES_BASE = ( + STATES = ( ('NO', 'NO'), ('STRICT', 'STRICT'), ('BLOQ', 'BLOQ'), ('COMMON', 'COMMON'), ) - try: - STATES = STATES_BASE + tuple([(str(id), str(id)) for id in list(Vlan.objects.values_list('vlan_id', flat=True).order_by('vlan_id'))]) - except: - STATES = STATES_BASE - + switch = models.ForeignKey('Switch', related_name="ports") port = models.IntegerField() room = models.ForeignKey('Room', on_delete=models.PROTECT, blank=True, null=True) machine_interface = models.ForeignKey('machines.Interface', on_delete=models.SET_NULL, blank=True, null=True) related = models.OneToOneField('self', null=True, blank=True, related_name='related_port') radius = models.CharField(max_length=32, choices=STATES, default='NO') + vlan_force = models.ForeignKey('machines.Vlan', on_delete=models.SET_NULL, blank=True, null=True) details = models.CharField(max_length=255, blank=True) class Meta: diff --git a/topologie/templates/topologie/aff_port.html b/topologie/templates/topologie/aff_port.html index 6bd84abc..609d4349 100644 --- a/topologie/templates/topologie/aff_port.html +++ b/topologie/templates/topologie/aff_port.html @@ -30,6 +30,7 @@ with this program; if not, write to the Free Software Foundation, Inc., Interface machine Related Radius + Vlan forcé Détails @@ -53,6 +54,7 @@ with this program; if not, write to the Free Software Foundation, Inc., {% endif %} {{ port.radius }} + {% if not port.vlan_force %} Aucun{%else %}{{ port.vlan_force }}{% endif %} {{ port.details }} diff --git a/users/forms.py b/users/forms.py index 8eaffd10..0099176f 100644 --- a/users/forms.py +++ b/users/forms.py @@ -207,6 +207,7 @@ class EditInfoForm(BaseInfoForm): ] class InfoForm(EditInfoForm): + """ Utile pour forcer un déménagement quand il y a déjà un user en place""" force = forms.BooleanField(label="Forcer le déménagement ?", initial=False, required=False) def clean_force(self): @@ -215,15 +216,18 @@ class InfoForm(EditInfoForm): return class UserForm(InfoForm): + """ Model form general""" class Meta(InfoForm.Meta): fields = '__all__' class PasswordForm(ModelForm): + """ Formulaire de changement brut de mot de passe. Ne pas utiliser sans traitement""" class Meta: model = User fields = ['password', 'pwd_ntlm'] class ServiceUserForm(ModelForm): + """ Modification d'un service user""" password = forms.CharField(label=u'Nouveau mot de passe', max_length=255, validators=[MinLengthValidator(8)], widget=forms.PasswordInput, required=False) class Meta: @@ -235,6 +239,7 @@ class EditServiceUserForm(ServiceUserForm): fields = ['access_group','comment'] class StateForm(ModelForm): + """ Changement de l'état d'un user""" class Meta: model = User fields = ['state'] diff --git a/users/models.py b/users/models.py index 22364a49..3a0af0f0 100644 --- a/users/models.py +++ b/users/models.py @@ -56,6 +56,9 @@ from preferences.models import GeneralOption, AssoOption, OptionalUser, Optional now = timezone.now() + +#### Utilitaires généraux + def remove_user_room(room): """ Déménage de force l'ancien locataire de la chambre """ try: @@ -73,6 +76,8 @@ def linux_user_check(login): def linux_user_validator(login): + """ Retourne une erreur de validation si le login ne respecte + pas les contraintes unix (maj, min, chiffres ou tiret)""" if not linux_user_check(login): raise forms.ValidationError( ", ce pseudo ('%(label)s') contient des carractères interdits", @@ -80,6 +85,7 @@ def linux_user_validator(login): ) def get_fresh_user_uid(): + """ Renvoie le plus petit uid non pris. Fonction très paresseuse """ uids = list(range(int(min(UID_RANGES['users'])),int(max(UID_RANGES['users'])))) try: used_uids = list(User.objects.values_list('uid_number', flat=True)) @@ -89,12 +95,15 @@ def get_fresh_user_uid(): return min(free_uids) def get_fresh_gid(): + """ Renvoie le plus petit gid libre """ gids = list(range(int(min(GID_RANGES['posix'])),int(max(GID_RANGES['posix'])))) used_gids = list(ListRight.objects.values_list('gid', flat=True)) free_gids = [ id for id in gids if id not in used_gids] return min(free_gids) def get_admin_right(): + """ Renvoie l'instance droit admin. La crée si elle n'existe pas + Lui attribue un gid libre""" try: admin_right = ListRight.objects.get(listright="admin") except ListRight.DoesNotExist: @@ -104,15 +113,20 @@ def get_admin_right(): return admin_right def all_adherent(search_time=now): + """ Fonction renvoyant tous les users adherents. Optimisee pour n'est qu'une seule requete sql + Inspecte les factures de l'user et ses cotisation, regarde si elles sont posterieur à now (end_time)""" return User.objects.filter(facture__in=Facture.objects.filter(vente__in=Vente.objects.filter(cotisation__in=Cotisation.objects.filter(vente__in=Vente.objects.filter(facture__in=Facture.objects.all().exclude(valid=False))).filter(date_end__gt=search_time)))).distinct() def all_baned(search_time=now): + """ Fonction renvoyant tous les users bannis """ return User.objects.filter(ban__in=Ban.objects.filter(date_end__gt=search_time)).distinct() def all_whitelisted(search_time=now): + """ Fonction renvoyant tous les users whitelistes """ return User.objects.filter(whitelist__in=Whitelist.objects.filter(date_end__gt=search_time)).distinct() def all_has_access(search_time=now): + """ Renvoie tous les users beneficiant d'une connexion : user adherent ou whiteliste et non banni """ return User.objects.filter(Q(state=User.STATE_ACTIVE) & ~Q(ban__in=Ban.objects.filter(date_end__gt=timezone.now())) & (Q(whitelist__in=Whitelist.objects.filter(date_end__gt=timezone.now())) | Q(facture__in=Facture.objects.filter(vente__in=Vente.objects.filter(cotisation__in=Cotisation.objects.filter(vente__in=Vente.objects.filter(facture__in=Facture.objects.all().exclude(valid=False))).filter(date_end__gt=search_time)))))).distinct() class UserManager(BaseUserManager): @@ -152,6 +166,9 @@ class UserManager(BaseUserManager): class User(AbstractBaseUser): + """ Definition de l'utilisateur de base. + Champs principaux : name, surnname, pseudo, email, room, password + Herite du django BaseUser et du système d'auth django""" PRETTY_NAME = "Utilisateurs" STATE_ACTIVE = 0 STATE_DISABLED = 1 @@ -187,14 +204,17 @@ class User(AbstractBaseUser): @property def is_active(self): + """ Renvoie si l'user est à l'état actif""" return self.state == self.STATE_ACTIVE @property def is_staff(self): + """ Fonction de base django, renvoie si l'user est admin""" return self.is_admin @property def is_admin(self): + """ Renvoie si l'user est admin""" try: Right.objects.get(user=self, right__listright='admin') except Right.DoesNotExist: @@ -203,18 +223,24 @@ class User(AbstractBaseUser): @is_admin.setter def is_admin(self, value): + """ Change la valeur de admin à true ou false suivant la valeur de value""" if value and not self.is_admin: self.make_admin() elif not value and self.is_admin: self.un_admin() def get_full_name(self): + """ Renvoie le nom complet de l'user formaté nom/prénom""" return '%s %s' % (self.name, self.surname) def get_short_name(self): + """ Renvoie seulement le nom""" return self.name def has_perms(self, perms, obj=None): + """ Renvoie true si l'user dispose de la permission. + Prend en argument une liste de permissions. + TODO : Arranger cette fonction""" for perm in perms: if perm in RIGHTS_LINK: query = Q() @@ -233,6 +259,7 @@ class User(AbstractBaseUser): def has_right(self, right): + """ Renvoie si un user a un right donné. Crée le right si il n'existe pas""" try: list_right = ListRight.objects.get(listright=right) except: @@ -242,29 +269,38 @@ class User(AbstractBaseUser): @cached_property def is_bureau(self): + """ True si user a les droits bureau """ return self.has_right('bureau') @cached_property def is_bofh(self): + """ True si l'user a les droits bofh""" return self.has_right('bofh') @cached_property def is_cableur(self): + """ True si l'user a les droits cableur + (également true si bureau, infra ou bofh)""" return self.has_right('cableur') or self.has_right('bureau') or self.has_right('infra') or self.has_right('bofh') @cached_property def is_trez(self): + """ Renvoie true si droits trésorier pour l'user""" return self.has_right('tresorier') @cached_property def is_infra(self): + """ True si a les droits infra""" return self.has_right('infra') def end_adhesion(self): + """ Renvoie la date de fin d'adhésion d'un user. Examine les objets + cotisation""" date_max = Cotisation.objects.filter(vente__in=Vente.objects.filter(facture__in=Facture.objects.filter(user=self).exclude(valid=False))).aggregate(models.Max('date_end'))['date_end__max'] return date_max def is_adherent(self): + """ Renvoie True si l'user est adhérent : si self.end_adhesion()>now""" end = self.end_adhesion() if not end: return False @@ -327,6 +363,8 @@ class User(AbstractBaseUser): @cached_property def solde(self): + """ Renvoie le solde d'un user. Vérifie que l'option solde est activé, retourne 0 sinon. + Somme les crédits de solde et retire les débit payés par solde""" options, created = OptionalUser.objects.get_or_create() user_solde = options.user_solde if user_solde: @@ -337,8 +375,10 @@ class User(AbstractBaseUser): else: return 0 - def user_interfaces(self): - return Interface.objects.filter(machine__in=Machine.objects.filter(user=self, active=True)) + def user_interfaces(self, active=True): + """ Renvoie toutes les interfaces dont les machines appartiennent à self + Par defaut ne prend que les interfaces actives""" + return Interface.objects.filter(machine__in=Machine.objects.filter(user=self, active=active)) def assign_ips(self): """ Assign une ipv4 aux machines d'un user """ @@ -351,6 +391,7 @@ class User(AbstractBaseUser): interface.save() def unassign_ips(self): + """ Désassigne les ipv4 aux machines de l'user""" interfaces = self.user_interfaces() for interface in interfaces: with transaction.atomic(), reversion.create_revision(): @@ -359,10 +400,12 @@ class User(AbstractBaseUser): interface.save() def archive(self): + """ Archive l'user : appelle unassign_ips() puis passe state à ARCHIVE""" self.unassign_ips() self.state = User.STATE_ARCHIVE def unarchive(self): + """ Désarchive l'user : réassigne ses ip et le passe en state ACTIVE""" self.assign_ips() self.state = User.STATE_ACTIVE @@ -383,6 +426,10 @@ class User(AbstractBaseUser): user_right.delete() def ldap_sync(self, base=True, access_refresh=True, mac_refresh=True): + """ Synchronisation du ldap. Synchronise dans le ldap les attributs de self + Options : base : synchronise tous les attributs de base - nom, prenom, mail, password, shell, home + access_refresh : synchronise le dialup_access notant si l'user a accès aux services + mac_refresh : synchronise les machines de l'user""" self.refresh_from_db() try: user_ldap = LdapUser.objects.get(uidNumber=self.uid_number) @@ -411,6 +458,7 @@ class User(AbstractBaseUser): user_ldap.save() def ldap_del(self): + """ Supprime la version ldap de l'user""" try: user_ldap = LdapUser.objects.get(name=self.pseudo) user_ldap.delete() @@ -458,9 +506,11 @@ class User(AbstractBaseUser): return def autoregister_machine(self, mac_address, nas_type): - all_machines = self.all_machines() + """ Fonction appellée par freeradius. Enregistre la mac pour une machine inconnue + sur le compte de l'user""" + all_interfaces = self.user_interfaces(active=False) options, created = OptionalMachine.objects.get_or_create() - if all_machines.count() > options.max_lambdauser_interfaces: + if all_interfaces.count() > options.max_lambdauser_interfaces: return False, "Maximum de machines enregistrees atteinte" if not nas_type: return False, "Re2o ne sait pas à quel machinetype affecter cette machine" @@ -487,10 +537,9 @@ class User(AbstractBaseUser): return False, e return True, "Ok" - def all_machines(self): - return Interface.objects.filter(machine__in=Machine.objects.filter(user=self)) - def set_user_password(self, password): + """ A utiliser de préférence, set le password en hash courrant et + dans la version ntlm""" self.set_password(password) self.pwd_ntlm = hashNT(password) return @@ -517,6 +566,8 @@ class User(AbstractBaseUser): @receiver(post_save, sender=User) def user_post_save(sender, **kwargs): + """ Synchronisation post_save : envoie le mail de bienvenue si creation + Synchronise le ldap""" is_created = kwargs['created'] user = kwargs['instance'] if is_created: @@ -531,6 +582,7 @@ def user_post_delete(sender, **kwargs): regen('mailing') class ServiceUser(AbstractBaseUser): + """ Classe des users daemons, règle leurs accès au ldap""" readonly = 'readonly' ACCESS = ( ('auth', 'auth'), @@ -549,6 +601,7 @@ class ServiceUser(AbstractBaseUser): objects = UserManager() def ldap_sync(self): + """ Synchronisation du ServiceUser dans sa version ldap""" try: user_ldap = LdapServiceUser.objects.get(name=self.pseudo) except LdapServiceUser.DoesNotExist: @@ -578,15 +631,19 @@ class ServiceUser(AbstractBaseUser): @receiver(post_save, sender=ServiceUser) def service_user_post_save(sender, **kwargs): + """ Synchronise un service user ldap après modification django""" service_user = kwargs['instance'] service_user.ldap_sync() @receiver(post_delete, sender=ServiceUser) def service_user_post_delete(sender, **kwargs): + """ Supprime un service user ldap après suppression django""" service_user = kwargs['instance'] service_user.ldap_del() class Right(models.Model): + """ Couple droit/user. Peut-être aurait-on mieux fait ici d'utiliser un manytomany + Ceci dit le résultat aurait été le même avec une table intermediaire""" PRETTY_NAME = "Droits affectés à des users" user = models.ForeignKey('User', on_delete=models.PROTECT) @@ -600,15 +657,18 @@ class Right(models.Model): @receiver(post_save, sender=Right) def right_post_save(sender, **kwargs): + """ Synchronise les users ldap groups avec les groupes de droits""" right = kwargs['instance'].right right.ldap_sync() @receiver(post_delete, sender=Right) def right_post_delete(sender, **kwargs): + """ Supprime l'user du groupe""" right = kwargs['instance'].right right.ldap_sync() class School(models.Model): + """ Etablissement d'enseignement""" PRETTY_NAME = "Etablissements enregistrés" name = models.CharField(max_length=255) @@ -618,6 +678,9 @@ class School(models.Model): class ListRight(models.Model): + """ Ensemble des droits existants. Chaque droit crée un groupe ldap synchronisé, avec gid. + Permet de gérer facilement les accès serveurs et autres + La clef de recherche est le gid, pour cette raison là il n'est plus modifiable après creation""" PRETTY_NAME = "Liste des droits existants" listright = models.CharField(max_length=255, unique=True, validators=[RegexValidator('^[a-z]+$', message="Les groupes unix ne peuvent contenir que des lettres minuscules")]) @@ -645,6 +708,7 @@ class ListRight(models.Model): @receiver(post_save, sender=ListRight) def listright_post_save(sender, **kwargs): + """ Synchronise le droit ldap quand il est modifié""" right = kwargs['instance'] right.ldap_sync() @@ -662,6 +726,8 @@ class ListShell(models.Model): return self.shell class Ban(models.Model): + """ Bannissement. Actuellement a un effet tout ou rien. + Gagnerait à être granulaire""" PRETTY_NAME = "Liste des bannissements" STATE_HARD = 0 @@ -702,6 +768,7 @@ class Ban(models.Model): @receiver(post_save, sender=Ban) def ban_post_save(sender, **kwargs): + """ Regeneration de tous les services après modification d'un ban""" ban = kwargs['instance'] is_created = kwargs['created'] user = ban.user @@ -717,6 +784,7 @@ def ban_post_save(sender, **kwargs): @receiver(post_delete, sender=Ban) def ban_post_delete(sender, **kwargs): + """ Regen de tous les services après suppression d'un ban""" user = kwargs['instance'].user user.ldap_sync(base=False, access_refresh=True, mac_refresh=False) regen('mailing') @@ -760,6 +828,9 @@ def whitelist_post_delete(sender, **kwargs): regen('mac_ip_list') class Request(models.Model): + """ Objet request, générant une url unique de validation. + Utilisé par exemple pour la generation du mot de passe et + sa réinitialisation""" PASSWD = 'PW' EMAIL = 'EM' TYPE_CHOICES = ( diff --git a/users/views.py b/users/views.py index 2c6406bf..66a5f7ad 100644 --- a/users/views.py +++ b/users/views.py @@ -716,6 +716,8 @@ class JSONResponse(HttpResponse): @login_required @permission_required('serveur') def mailing(request): + """ Fonction de serialisation des addresses mail de tous les users + Pour generation de ml all users""" mails = all_has_access().values('email').distinct() seria = MailSerializer(mails, many=True) return JSONResponse(seria.data)