diff --git a/ldap_sync/__init__.py b/ldap_sync/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/ldap_sync/admin.py b/ldap_sync/admin.py new file mode 100644 index 00000000..0cba55da --- /dev/null +++ b/ldap_sync/admin.py @@ -0,0 +1,64 @@ +from django.contrib import admin + +from .models import ( + LdapUser, + LdapServiceUser, + LdapServiceUserGroup, + LdapUserGroup, +) + +class LdapUserAdmin(admin.ModelAdmin): + """LdapUser Admin view. Can't change password, manage + by User General model. + + Parameters: + Django ModelAdmin: Apply on django ModelAdmin + + """ + list_display = ("name", "uidNumber", "login_shell") + exclude = ("user_password", "sambat_nt_password") + search_fields = ("name",) + + +class LdapServiceUserAdmin(admin.ModelAdmin): + """LdapServiceUser Admin view. Can't change password, manage + by User General model. + + Parameters: + Django ModelAdmin: Apply on django ModelAdmin + + """ + + list_display = ("name",) + exclude = ("user_password",) + search_fields = ("name",) + + +class LdapUserGroupAdmin(admin.ModelAdmin): + """LdapUserGroup Admin view. + + Parameters: + Django ModelAdmin: Apply on django ModelAdmin + + """ + + list_display = ("name", "members", "gid") + search_fields = ("name",) + + +class LdapServiceUserGroupAdmin(admin.ModelAdmin): + """LdapServiceUserGroup Admin view. + + Parameters: + Django ModelAdmin: Apply on django ModelAdmin + + """ + + list_display = ("name",) + search_fields = ("name",) + + +admin.site.register(LdapUser, LdapUserAdmin) +admin.site.register(LdapUserGroup, LdapUserGroupAdmin) +admin.site.register(LdapServiceUser, LdapServiceUserAdmin) +admin.site.register(LdapServiceUserGroup, LdapServiceUserGroupAdmin) diff --git a/ldap_sync/apps.py b/ldap_sync/apps.py new file mode 100644 index 00000000..b96c34d1 --- /dev/null +++ b/ldap_sync/apps.py @@ -0,0 +1,5 @@ +from django.apps import AppConfig + + +class LdapSyncConfig(AppConfig): + name = 'ldap_sync' diff --git a/users/management/commands/ldap_rebuild.py b/ldap_sync/management/commands/ldap_rebuild.py similarity index 94% rename from users/management/commands/ldap_rebuild.py rename to ldap_sync/management/commands/ldap_rebuild.py index c8b172f9..1fc3c969 100644 --- a/users/management/commands/ldap_rebuild.py +++ b/ldap_sync/management/commands/ldap_rebuild.py @@ -1,4 +1,5 @@ # Copyright © 2018 Maël Kervella +# Copyright © 2021 Hugo Levy-Falk # # 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 @@ -21,6 +22,7 @@ from django.core.management.base import BaseCommand, CommandError from django.conf import settings from users.models import User, ListRight +from ldap_sync.models import synchronise_user, synchronise_serviceuser, synchronise_usergroup def split_lines(lines): @@ -89,9 +91,9 @@ def flush_ldap(binddn, bindpass, server, usersdn, groupsdn): def sync_ldap(): """Syncrhonize the whole LDAP with the DB.""" for u in User.objects.all(): - u.ldap_sync() + synchronise_user(sender=User, instance=u) for lr in ListRight.objects.all(): - lr.ldap_sync() + synchronise_usergroup(sender=ListRight, instance=lr) class Command(BaseCommand): diff --git a/users/management/commands/ldap_sync.py b/ldap_sync/management/commands/ldap_sync.py similarity index 88% rename from users/management/commands/ldap_sync.py rename to ldap_sync/management/commands/ldap_sync.py index 73f6698e..984f3fd7 100644 --- a/users/management/commands/ldap_sync.py +++ b/ldap_sync/management/commands/ldap_sync.py @@ -1,6 +1,7 @@ # Copyright © 2017 Gabriel Détraz # Copyright © 2017 Lara Kermarec # Copyright © 2017 Augustin Lemesle +# Copyright © 2020 Hugo Levy-Falk # # 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 @@ -19,6 +20,7 @@ from django.core.management.base import BaseCommand, CommandError from users.models import User +from ldap_sync.models import synchronise_user class Command(BaseCommand): @@ -36,5 +38,5 @@ class Command(BaseCommand): ) def handle(self, *args, **options): - for usr in User.objects.all(): - usr.ldap_sync(mac_refresh=options["full"]) + for user in User.objects.all(): + synchronise_user(sender=User, instance=user) diff --git a/ldap_sync/migrations/0001_initial.py b/ldap_sync/migrations/0001_initial.py new file mode 100644 index 00000000..5a4c448e --- /dev/null +++ b/ldap_sync/migrations/0001_initial.py @@ -0,0 +1,108 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.29 on 2021-01-10 16:59 +from __future__ import unicode_literals + +from django.db import migrations +#from django.conf import settings +import ldapdb.models.fields + +#from ldap_sync.management.commands.ldap_rebuild import flush_ldap, sync_ldap + +#def rebuild_ldap(apps, schema_editor): +# usersdn = settings.LDAP["base_user_dn"] +# groupsdn = settings.LDAP["base_usergroup_dn"] +# binddn = settings.DATABASES["ldap"]["USER"] +# bindpass = settings.DATABASES["ldap"]["PASSWORD"] +# server = settings.DATABASES["ldap"]["NAME"] +# flush_ldap(binddn, bindpass, server, usersdn, groupsdn) + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ('users', '0004_auto_20210110_1811') + ] + + operations = [ + migrations.CreateModel( + name='LdapServiceUser', + fields=[ + ('dn', ldapdb.models.fields.CharField(max_length=200, serialize=False)), + ('name', ldapdb.models.fields.CharField(db_column='cn', max_length=200, primary_key=True, serialize=False)), + ('user_password', ldapdb.models.fields.CharField(blank=True, db_column='userPassword', max_length=200, null=True)), + ], + options={ + 'abstract': False, + }, + ), + migrations.CreateModel( + name='LdapServiceUserGroup', + fields=[ + ('dn', ldapdb.models.fields.CharField(max_length=200, serialize=False)), + ('name', ldapdb.models.fields.CharField(db_column='cn', max_length=200, primary_key=True, serialize=False)), + ('members', ldapdb.models.fields.ListField(blank=True, db_column='member')), + ], + options={ + 'abstract': False, + }, + ), + migrations.CreateModel( + name='LdapUser', + fields=[ + ('dn', ldapdb.models.fields.CharField(max_length=200, serialize=False)), + ('gid', ldapdb.models.fields.IntegerField(db_column='gidNumber')), + ('name', ldapdb.models.fields.CharField(db_column='cn', max_length=200, primary_key=True, serialize=False)), + ('uid', ldapdb.models.fields.CharField(db_column='uid', max_length=200)), + ('uidNumber', ldapdb.models.fields.IntegerField(db_column='uidNumber', unique=True)), + ('sn', ldapdb.models.fields.CharField(db_column='sn', max_length=200)), + ('login_shell', ldapdb.models.fields.CharField(blank=True, db_column='loginShell', max_length=200, null=True)), + ('mail', ldapdb.models.fields.CharField(db_column='mail', max_length=200)), + ('given_name', ldapdb.models.fields.CharField(db_column='givenName', max_length=200)), + ('home_directory', ldapdb.models.fields.CharField(db_column='homeDirectory', max_length=200)), + ('display_name', ldapdb.models.fields.CharField(blank=True, db_column='displayName', max_length=200, null=True)), + ('dialupAccess', ldapdb.models.fields.CharField(db_column='dialupAccess', max_length=200)), + ('sambaSID', ldapdb.models.fields.IntegerField(db_column='sambaSID', unique=True)), + ('user_password', ldapdb.models.fields.CharField(blank=True, db_column='userPassword', max_length=200, null=True)), + ('sambat_nt_password', ldapdb.models.fields.CharField(blank=True, db_column='sambaNTPassword', max_length=200, null=True)), + ('macs', ldapdb.models.fields.ListField(blank=True, db_column='radiusCallingStationId', max_length=200, null=True)), + ('shadowexpire', ldapdb.models.fields.CharField(blank=True, db_column='shadowExpire', max_length=200, null=True)), + ], + options={ + 'abstract': False, + }, + ), + migrations.CreateModel( + name='LdapUserGroup', + fields=[ + ('dn', ldapdb.models.fields.CharField(max_length=200, serialize=False)), + ('gid', ldapdb.models.fields.IntegerField(db_column='gidNumber')), + ('members', ldapdb.models.fields.ListField(blank=True, db_column='memberUid')), + ('name', ldapdb.models.fields.CharField(db_column='cn', max_length=200, primary_key=True, serialize=False)), + ], + options={ + 'abstract': False, + }, + ), + migrations.AlterField( + model_name='ldapserviceuser', + name='dn', + field=ldapdb.models.fields.CharField(max_length=200, primary_key=True, serialize=False), + ), + migrations.AlterField( + model_name='ldapserviceusergroup', + name='dn', + field=ldapdb.models.fields.CharField(max_length=200, primary_key=True, serialize=False), + ), + migrations.AlterField( + model_name='ldapuser', + name='dn', + field=ldapdb.models.fields.CharField(max_length=200, primary_key=True, serialize=False), + ), + migrations.AlterField( + model_name='ldapusergroup', + name='dn', + field=ldapdb.models.fields.CharField(max_length=200, primary_key=True, serialize=False), + ), + ] diff --git a/ldap_sync/migrations/__init__.py b/ldap_sync/migrations/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/ldap_sync/models.py b/ldap_sync/models.py new file mode 100644 index 00000000..b360e7c1 --- /dev/null +++ b/ldap_sync/models.py @@ -0,0 +1,334 @@ +import sys + +from django.db import models +from django.conf import settings +from django.dispatch import receiver + +from django.contrib.auth.models import Group + +import ldapdb.models +import ldapdb.models.fields + +import users.signals +import users.models + +import machines.models + +class LdapUser(ldapdb.models.Model): + """A class representing a LdapUser in LDAP, its LDAP conterpart. + Synced from re2o django User model, (User django models), + with a copy of its attributes/fields into LDAP, so this class is a mirror + of the classic django User model. + + The basedn userdn is specified in settings. + + Attributes: + name: The name of this User + uid: The uid (login) for the unix user + uidNumber: Linux uid number + gid: The default gid number for this user + sn: The user "str" pseudo + login_shell: Linux shell for the user + mail: Email address contact for this user + display_name: Pretty display name for this user + dialupAccess: Boolean, True for valid membership + sambaSID: Identical id as uidNumber + user_password: SSHA hashed password of user + samba_nt_password: NTLM hashed password of user + macs: Multivalued mac address + shadowexpire: Set it to 0 to block access for this user and disabled + account + """ + + # LDAP meta-data + base_dn = settings.LDAP["base_user_dn"] + object_classes = [ + "inetOrgPerson", + "top", + "posixAccount", + "sambaSamAccount", + "radiusprofile", + "shadowAccount", + ] + + # attributes + gid = ldapdb.models.fields.IntegerField(db_column="gidNumber") + name = ldapdb.models.fields.CharField( + db_column="cn", max_length=200, primary_key=True + ) + uid = ldapdb.models.fields.CharField(db_column="uid", max_length=200) + uidNumber = ldapdb.models.fields.IntegerField(db_column="uidNumber", unique=True) + sn = ldapdb.models.fields.CharField(db_column="sn", max_length=200) + login_shell = ldapdb.models.fields.CharField( + db_column="loginShell", max_length=200, blank=True, null=True + ) + mail = ldapdb.models.fields.CharField(db_column="mail", max_length=200) + given_name = ldapdb.models.fields.CharField(db_column="givenName", max_length=200) + home_directory = ldapdb.models.fields.CharField( + db_column="homeDirectory", max_length=200 + ) + display_name = ldapdb.models.fields.CharField( + db_column="displayName", max_length=200, blank=True, null=True + ) + dialupAccess = ldapdb.models.fields.CharField(db_column="dialupAccess") + sambaSID = ldapdb.models.fields.IntegerField(db_column="sambaSID", unique=True) + user_password = ldapdb.models.fields.CharField( + db_column="userPassword", max_length=200, blank=True, null=True + ) + sambat_nt_password = ldapdb.models.fields.CharField( + db_column="sambaNTPassword", max_length=200, blank=True, null=True + ) + macs = ldapdb.models.fields.ListField( + db_column="radiusCallingStationId", max_length=200, blank=True, null=True + ) + shadowexpire = ldapdb.models.fields.CharField( + db_column="shadowExpire", blank=True, null=True + ) + + def __str__(self): + return self.name + + def __unicode__(self): + return self.name + + def save(self, *args, **kwargs): + self.sn = self.name + self.uid = self.name + self.sambaSID = self.uidNumber + super(LdapUser, self).save(*args, **kwargs) + + +@receiver(users.signals.synchronise, sender=users.models.User) +def synchronise_user(sender, **kwargs): + """ + Synchronise an User to the LDAP. + Args: + * sender : The model class. + * instance : The actual instance being synchronised. + * base : Default `True`. When `True`, synchronise basic attributes. + * access_refresh : Default `True`. When `True`, synchronise the access time. + * mac_refresh : Default `True`. When True, synchronise the list of mac addresses. + * group_refresh: Default `False`. When `True` synchronise the groups of the instance. + """ + base=kwargs.get('base', True) + access_refresh=kwargs.get('access_refresh', True) + mac_refresh=kwargs.get('mac_refresh', True ) + group_refresh=kwargs.get('group_refresh', False) + + user=kwargs["instance"] + + if sys.version_info[0] >= 3 and ( + user.state == user.STATE_ACTIVE + or user.state == user.STATE_ARCHIVE + or user.state == user.STATE_DISABLED + ): + user.refresh_from_db() + try: + user_ldap = LdapUser.objects.get(uidNumber=user.uid_number) + except LdapUser.DoesNotExist: + user_ldap = LdapUser(uidNumber=user.uid_number) + base = True + access_refresh = True + mac_refresh = True + if base: + user_ldap.name = user.pseudo + user_ldap.sn = user.pseudo + user_ldap.dialupAccess = str(user.has_access()) + user_ldap.home_directory = user.home_directory + user_ldap.mail = user.get_mail + user_ldap.given_name = ( + user.surname.lower() + "_" + user.name.lower()[:3] + ) + user_ldap.gid = settings.LDAP["user_gid"] + if "{SSHA}" in user.password or "{SMD5}" in user.password: + # We remove the extra $ added at import from ldap + user_ldap.user_password = user.password[:6] + user.password[7:] + elif "{crypt}" in user.password: + # depending on the length, we need to remove or not a $ + if len(user.password) == 41: + user_ldap.user_password = user.password + else: + user_ldap.user_password = user.password[:7] + user.password[8:] + + user_ldap.sambat_nt_password = user.pwd_ntlm.upper() + if user.get_shell: + user_ldap.login_shell = str(user.get_shell) + user_ldap.shadowexpire = user.get_shadow_expire + if access_refresh: + user_ldap.dialupAccess = str(user.has_access()) + if mac_refresh: + user_ldap.macs = [ + str(mac) + for mac in machines.models.Interface.objects.filter(machine__user=user) + .values_list("mac_address", flat=True) + .distinct() + ] + if group_refresh: + # Need to refresh all groups because we don't know which groups + # were updated during edition of groups and the user may no longer + # be part of the updated group (case of group removal) + for group in Group.objects.all(): + if hasattr(group, "listright"): + synchronise_usergroup(users.models.ListRight, instance=group.listright) + user_ldap.save() + +@receiver(users.signals.remove, sender=users.models.User) +def remove_user(sender, **kwargs): + user = kwargs["instance"] + try: + user_ldap = LdapUser.objects.get(name=user.pseudo) + user_ldap.delete() + except LdapUser.DoesNotExist: + pass + +@receiver(users.signals.remove_mass, sender=users.models.User) +def remove_users(sender, **kwargs): + queryset_users = kwargs["queryset"] + LdapUser.objects.filter( + name__in=list(queryset_users.values_list("pseudo", flat=True)) + ).delete() + + +class LdapUserGroup(ldapdb.models.Model): + """A class representing a LdapUserGroup in LDAP, its LDAP conterpart. + Synced from UserGroup, (ListRight/Group django models), + with a copy of its attributes/fields into LDAP, so this class is a mirror + of the classic django ListRight model. + + The basedn usergroupdn is specified in settings. + + Attributes: + name: The name of this LdapUserGroup + gid: The gid number for this unix group + members: Users dn members of this LdapUserGroup + """ + + # LDAP meta-data + base_dn = settings.LDAP["base_usergroup_dn"] + object_classes = ["posixGroup"] + + # attributes + gid = ldapdb.models.fields.IntegerField(db_column="gidNumber") + members = ldapdb.models.fields.ListField(db_column="memberUid", blank=True) + name = ldapdb.models.fields.CharField( + db_column="cn", max_length=200, primary_key=True + ) + + def __str__(self): + return self.name + +@receiver(users.signals.synchronise, sender=users.models.ListRight) +def synchronise_usergroup(sender, **kwargs): + group = kwargs["instance"] + try: + group_ldap = LdapUserGroup.objects.get(gid=group.gid) + except LdapUserGroup.DoesNotExist: + group_ldap = LdapUserGroup(gid=group.gid) + group_ldap.name = group.unix_name + group_ldap.members = [user.pseudo for user in group.user_set.all()] + group_ldap.save() + +@receiver(users.signals.remove, sender=users.models.ListRight) +def remove_usergroup(sender, **kwargs): + group = kwargs["instance"] + try: + group_ldap = LdapUserGroup.objects.get(gid=group.gid) + group_ldap.delete() + except LdapUserGroup.DoesNotExist: + pass + + + +class LdapServiceUser(ldapdb.models.Model): + """A class representing a ServiceUser in LDAP, its LDAP conterpart. + Synced from ServiceUser, with a copy of its attributes/fields into LDAP, + so this class is a mirror of the classic django ServiceUser model. + + The basedn userservicedn is specified in settings. + + Attributes: + name: The name of this ServiceUser + user_password: The SSHA hashed password of this ServiceUser + """ + + # LDAP meta-data + base_dn = settings.LDAP["base_userservice_dn"] + object_classes = ["applicationProcess", "simpleSecurityObject"] + + # attributes + name = ldapdb.models.fields.CharField( + db_column="cn", max_length=200, primary_key=True + ) + user_password = ldapdb.models.fields.CharField( + db_column="userPassword", max_length=200, blank=True, null=True + ) + + def __str__(self): + return self.name + + +def synchronise_serviceuser_group(serviceuser): + try: + group = LdapServiceUserGroup.objects.get(name=serviceuser.access_group) + except: + group = LdapServiceUserGroup(name=serviceuser.access_group) + group.members = list( + LdapServiceUser.objects.filter( + name__in=[ + user.pseudo + for user in users.models.ServiceUser.objects.filter( + access_group=serviceuser.access_group + ) + ] + ).values_list("dn", flat=True) + ) + group.save() + + +@receiver(users.signals.synchronise, sender=users.models.ServiceUser) +def synchronise_serviceuser(sender, **kwargs): + user = kwargs["instance"] + try: + user_ldap = LdapServiceUser.objects.get(name=user.pseudo) + except LdapServiceUser.DoesNotExist: + user_ldap = LdapServiceUser(name=user.pseudo) + user_ldap.user_password = user.password[:6] + user.password[7:] + user_ldap.save() + synchronise_serviceuser_group(user) + +@receiver(users.signals.remove, sender=users.models.ServiceUser) +def remove_serviceuser(sender, **kwargs): + user = kwargs["instance"] + try: + user_ldap = LdapServiceUser.objects.get(name=user.pseudo) + user_ldap.delete() + except LdapUser.DoesNotExist: + pass + synchronise_serviceuser_group(user) + + +class LdapServiceUserGroup(ldapdb.models.Model): + """A class representing a ServiceUserGroup in LDAP, its LDAP conterpart. + Synced from ServiceUserGroup, with a copy of its attributes/fields into LDAP, + so this class is a mirror of the classic django ServiceUserGroup model. + + The basedn userservicegroupdn is specified in settings. + + Attributes: + name: The name of this ServiceUserGroup + members: ServiceUsers dn members of this ServiceUserGroup + """ + + # LDAP meta-data + base_dn = settings.LDAP["base_userservicegroup_dn"] + object_classes = ["groupOfNames"] + + # attributes + name = ldapdb.models.fields.CharField( + db_column="cn", max_length=200, primary_key=True + ) + members = ldapdb.models.fields.ListField(db_column="member", blank=True) + + def __str__(self): + return self.name + diff --git a/ldap_sync/tests.py b/ldap_sync/tests.py new file mode 100644 index 00000000..7ce503c2 --- /dev/null +++ b/ldap_sync/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/ldap_sync/urls.py b/ldap_sync/urls.py new file mode 100644 index 00000000..24f25ce8 --- /dev/null +++ b/ldap_sync/urls.py @@ -0,0 +1,4 @@ +from django.conf.urls import url +from .import views + +urlpatterns = [] diff --git a/ldap_sync/views.py b/ldap_sync/views.py new file mode 100644 index 00000000..91ea44a2 --- /dev/null +++ b/ldap_sync/views.py @@ -0,0 +1,3 @@ +from django.shortcuts import render + +# Create your views here. diff --git a/re2o/context_processors.py b/re2o/context_processors.py index c32c263f..07ee1c68 100644 --- a/re2o/context_processors.py +++ b/re2o/context_processors.py @@ -79,12 +79,12 @@ def context_optionnal_apps(request): optionnal_templates_navbar_user_list = [ app.views.navbar_user() for app in optionnal_apps - if hasattr(app.views, "navbar_user") + if hasattr(app, "views") and hasattr(app.views, "navbar_user") ] optionnal_templates_navbar_logout_list = [ app.views.navbar_logout() for app in optionnal_apps - if hasattr(app.views, "navbar_logout") + if hasattr(app, "views") and hasattr(app.views, "navbar_logout") ] return { "optionnal_templates_navbar_user_list": optionnal_templates_navbar_user_list, diff --git a/users/admin.py b/users/admin.py index a18dae90..d083a951 100644 --- a/users/admin.py +++ b/users/admin.py @@ -46,10 +46,6 @@ from .models import ( Ban, Whitelist, Request, - LdapUser, - LdapServiceUser, - LdapServiceUserGroup, - LdapUserGroup, ) from .forms import ( UserAdminForm, @@ -57,57 +53,6 @@ from .forms import ( ) -class LdapUserAdmin(admin.ModelAdmin): - """LdapUser Admin view. Can't change password, manage - by User General model. - - Parameters: - Django ModelAdmin: Apply on django ModelAdmin - - """ - list_display = ("name", "uidNumber", "login_shell") - exclude = ("user_password", "sambat_nt_password") - search_fields = ("name",) - - -class LdapServiceUserAdmin(admin.ModelAdmin): - """LdapServiceUser Admin view. Can't change password, manage - by User General model. - - Parameters: - Django ModelAdmin: Apply on django ModelAdmin - - """ - - list_display = ("name",) - exclude = ("user_password",) - search_fields = ("name",) - - -class LdapUserGroupAdmin(admin.ModelAdmin): - """LdapUserGroup Admin view. - - Parameters: - Django ModelAdmin: Apply on django ModelAdmin - - """ - - list_display = ("name", "members", "gid") - search_fields = ("name",) - - -class LdapServiceUserGroupAdmin(admin.ModelAdmin): - """LdapServiceUserGroup Admin view. - - Parameters: - Django ModelAdmin: Apply on django ModelAdmin - - """ - - list_display = ("name",) - search_fields = ("name",) - - class SchoolAdmin(VersionAdmin): """School Admin view and management. @@ -338,10 +283,6 @@ class ServiceUserAdmin(VersionAdmin, BaseUserAdmin): admin.site.register(Adherent, AdherentAdmin) admin.site.register(Club, ClubAdmin) admin.site.register(ServiceUser, ServiceUserAdmin) -admin.site.register(LdapUser, LdapUserAdmin) -admin.site.register(LdapUserGroup, LdapUserGroupAdmin) -admin.site.register(LdapServiceUser, LdapServiceUserAdmin) -admin.site.register(LdapServiceUserGroup, LdapServiceUserGroupAdmin) admin.site.register(School, SchoolAdmin) admin.site.register(ListRight, ListRightAdmin) admin.site.register(ListShell, ListShellAdmin) diff --git a/users/migrations/0004_auto_20210110_1811.py b/users/migrations/0004_auto_20210110_1811.py new file mode 100644 index 00000000..f6179f01 --- /dev/null +++ b/users/migrations/0004_auto_20210110_1811.py @@ -0,0 +1,27 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.29 on 2021-01-10 17:11 +from __future__ import unicode_literals + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('users', '0003_ldapserviceuser_ldapserviceusergroup_ldapuser_ldapusergroup'), + ] + + operations = [ + migrations.DeleteModel( + name='LdapServiceUser', + ), + migrations.DeleteModel( + name='LdapServiceUserGroup', + ), + migrations.DeleteModel( + name='LdapUser', + ), + migrations.DeleteModel( + name='LdapUserGroup', + ), + ] diff --git a/users/models.py b/users/models.py index 2b6eaacf..8313bfd7 100755 --- a/users/models.py +++ b/users/models.py @@ -39,14 +39,6 @@ Here are defined the following django models : * Schools (teaching structures) * Rights (Groups and ListRight) * ServiceUser (for ldap connexions) - -Also define django-ldapdb models : - * LdapUser - * LdapGroup - * LdapServiceUser - -These objects are sync from django regular models as auxiliary models from -sql data into ldap. """ @@ -82,8 +74,6 @@ from django.core.files.uploadedfile import InMemoryUploadedFile from reversion import revisions as reversion -import ldapdb.models -import ldapdb.models.fields from re2o.settings import LDAP, GID_RANGES, UID_RANGES from re2o.field_permissions import FieldPermissionModelMixin @@ -96,6 +86,8 @@ from machines.models import Domain, Interface, Machine, regen from preferences.models import GeneralOption, AssoOption, OptionalUser from preferences.models import OptionalMachine, MailMessageOption +from users import signals + from PIL import Image from io import BytesIO import sys @@ -1042,7 +1034,7 @@ class User( """ cls.mass_disable_email(queryset_users) Machine.mass_delete(Machine.objects.filter(user__in=queryset_users)) - cls.ldap_delete_users(queryset_users) + signals.remove_mass.send(sender=cls, queryset=queryset_users) def archive(self): """Method, archive user by unassigning ips. @@ -1072,7 +1064,7 @@ class User( def full_archive(self): """Method, full archive an user by unassigning ips, deleting data - and ldap deletion. + and authentication deletion. Parameters: self (user instance): user to full archive. @@ -1080,7 +1072,7 @@ class User( """ self.archive() self.delete_data() - self.ldap_del() + signals.remove.send(sender=User, instance=self) @classmethod def mass_full_archive(cls, users_list): @@ -1102,14 +1094,14 @@ class User( def unarchive(self): """Method, unarchive an user by assigning ips, and recreating - ldap user associated. + authentication user associated. Parameters: self (user instance): user to unarchive. """ self.assign_ips() - self.ldap_sync() + signals.synchronise.send(sender=self.__class__, instance=self) def state_sync(self): """Master Method, call unarchive, full_archive or archive method @@ -1135,109 +1127,6 @@ class User( ): self.full_archive() - def ldap_sync( - self, base=True, access_refresh=True, mac_refresh=True, group_refresh=False - ): - """Method ldap_sync, sync in ldap with self user attributes. - Each User instance is copy into ldap, via a LdapUser virtual objects. - This method performs a copy of several attributes (name, surname, mail, - hashed SSHA password, ntlm password, shell, homedirectory). - - Update, or create if needed a ldap entry related with the User instance. - - Parameters: - self (user instance): user to sync in ldap. - base (boolean): Default true, if base is true, perform a basic - sync of basic attributes. - access_refresh (boolean): Default true, if access_refresh is true, - update the dialup_access attributes based on has_access (is this user - has a valid internet access). - mac_refresh (boolean): Default true, if mac_refresh, update the mac_address - list of the user. - group_refresh (boolean): Default False, if true, update the groups membership - of this user. Onerous option, call ldap_sync() on every groups of the user. - - """ - if sys.version_info[0] >= 3 and ( - self.state == self.STATE_ACTIVE - or self.state == self.STATE_ARCHIVE - or self.state == self.STATE_DISABLED - ): - self.refresh_from_db() - try: - user_ldap = LdapUser.objects.get(uidNumber=self.uid_number) - except LdapUser.DoesNotExist: - user_ldap = LdapUser(uidNumber=self.uid_number) - base = True - access_refresh = True - mac_refresh = True - if base: - user_ldap.name = self.pseudo - user_ldap.sn = self.pseudo - user_ldap.dialupAccess = str(self.has_access()) - user_ldap.home_directory = self.home_directory - user_ldap.mail = self.get_mail - user_ldap.given_name = ( - self.surname.lower() + "_" + self.name.lower()[:3] - ) - user_ldap.gid = LDAP["user_gid"] - if "{SSHA}" in self.password or "{SMD5}" in self.password: - # We remove the extra $ added at import from ldap - user_ldap.user_password = self.password[:6] + self.password[7:] - elif "{crypt}" in self.password: - # depending on the length, we need to remove or not a $ - if len(self.password) == 41: - user_ldap.user_password = self.password - else: - user_ldap.user_password = self.password[:7] + self.password[8:] - - user_ldap.sambat_nt_password = self.pwd_ntlm.upper() - if self.get_shell: - user_ldap.login_shell = str(self.get_shell) - user_ldap.shadowexpire = self.get_shadow_expire - if access_refresh: - user_ldap.dialupAccess = str(self.has_access()) - if mac_refresh: - user_ldap.macs = [ - str(mac) - for mac in Interface.objects.filter(machine__user=self) - .values_list("mac_address", flat=True) - .distinct() - ] - if group_refresh: - # Need to refresh all groups because we don't know which groups - # were updated during edition of groups and the user may no longer - # be part of the updated group (case of group removal) - for group in Group.objects.all(): - if hasattr(group, "listright"): - group.listright.ldap_sync() - user_ldap.save() - - def ldap_del(self): - """Method, delete an user in ldap. - - Parameters: - self (user instance): user to delete in Ldap. - - """ - try: - user_ldap = LdapUser.objects.get(name=self.pseudo) - user_ldap.delete() - except LdapUser.DoesNotExist: - pass - - @classmethod - def ldap_delete_users(cls, queryset_users): - """Class method, delete several users in ldap (queryset). - - Parameters: - queryset_users (list of users queryset): users to delete - in ldap. - """ - LdapUser.objects.filter( - name__in=list(queryset_users.values_list("pseudo", flat=True)) - ) - ###### Send mail functions ###### def notif_inscription(self, request=None): @@ -2195,7 +2084,7 @@ class Club(User): @receiver(post_save, sender=User) def user_post_save(**kwargs): """Django signal, post save operations on Adherent, Club and User. - Sync pseudo, sync ldap, create mailalias and send welcome email if needed + Sync pseudo, sync authentication, create mailalias and send welcome email if needed (new user) """ @@ -2207,8 +2096,7 @@ def user_post_save(**kwargs): user.notif_inscription(user.request) user.set_active() user.state_sync() - user.ldap_sync( - base=True, access_refresh=True, mac_refresh=False, group_refresh=True + signals.synchronise.send(sender=User, instance=user, base=True, access_refresh=True, mac_refresh=False, group_refresh=True ) regen("mailing") @@ -2216,14 +2104,13 @@ def user_post_save(**kwargs): @receiver(m2m_changed, sender=User.groups.through) def user_group_relation_changed(**kwargs): """Django signal, used for User Groups change (related models). - Sync ldap, with calling group_refresh. + Sync authentication, with calling group_refresh. """ action = kwargs["action"] if action in ("post_add", "post_remove", "post_clear"): user = kwargs["instance"] - user.ldap_sync( - base=False, access_refresh=False, mac_refresh=False, group_refresh=True + signals.synchronise.send(sender=User, instance=user, base=False, access_refresh=False, mac_refresh=False, group_refresh=True ) @@ -2232,20 +2119,20 @@ def user_group_relation_changed(**kwargs): @receiver(post_delete, sender=User) def user_post_delete(**kwargs): """Django signal, post delete operations on Adherent, Club and User. - Delete user in ldap. + Delete user in authentication. """ user = kwargs["instance"] - user.ldap_del() + signals.remove.send(sender=User, instance=user) regen("mailing") class ServiceUser(RevMixin, AclMixin, AbstractBaseUser): """A class representing a serviceuser (it is considered as a user with special informations). - The serviceuser is a special user used with special access to ldap tree. It is + The serviceuser is a special user used with special access to authentication tree. It is its only usefullness, and service user can't connect to re2o. - Each service connected to ldap for auth (ex dokuwiki, owncloud, etc) should + Each service connected to authentication for auth (ex dokuwiki, owncloud, etc) should have a different service user with special acl (readonly, auth) and password. Attributes: @@ -2293,65 +2180,6 @@ class ServiceUser(RevMixin, AclMixin, AbstractBaseUser): """ return self.pseudo - def ldap_sync(self): - """Method ldap_sync, sync the serviceuser in ldap with its attributes. - Each ServiceUser instance is copy into ldap, via a LdapServiceUser virtual object. - This method performs a copy of several attributes (pseudo, access). - - Update, or create if needed a mirror ldap entry related with the ServiceUserinstance. - - Parameters: - self (serviceuser instance): ServiceUser to sync in ldap. - - """ - try: - user_ldap = LdapServiceUser.objects.get(name=self.pseudo) - except LdapServiceUser.DoesNotExist: - user_ldap = LdapServiceUser(name=self.pseudo) - user_ldap.user_password = self.password[:6] + self.password[7:] - user_ldap.save() - self.serviceuser_group_sync() - - def ldap_del(self): - """Method, delete an ServiceUser in ldap. - - Parameters: - self (ServiceUser instance): serviceuser to delete in Ldap. - - """ - try: - user_ldap = LdapServiceUser.objects.get(name=self.pseudo) - user_ldap.delete() - except LdapUser.DoesNotExist: - pass - self.serviceuser_group_sync() - - def serviceuser_group_sync(self): - """Method, update serviceuser group sync in ldap. - In LDAP, Acl depends on the ldapgroup (readonly, auth, or usermgt), - so the ldap group need to be synced with the accessgroup field on ServiceUser. - Called by ldap_sync and ldap_del. - - Parameters: - self (ServiceUser instance): serviceuser to update groups in LDAP. - - """ - try: - group = LdapServiceUserGroup.objects.get(name=self.access_group) - except: - group = LdapServiceUserGroup(name=self.access_group) - group.members = list( - LdapServiceUser.objects.filter( - name__in=[ - user.pseudo - for user in ServiceUser.objects.filter( - access_group=self.access_group - ) - ] - ).values_list("dn", flat=True) - ) - group.save() - def __str__(self): return self.pseudo @@ -2359,21 +2187,21 @@ class ServiceUser(RevMixin, AclMixin, AbstractBaseUser): @receiver(post_save, sender=ServiceUser) def service_user_post_save(**kwargs): """Django signal, post save operations on ServiceUser. - Sync or create serviceuser in ldap. + Sync or create serviceuser in authentication. """ service_user = kwargs["instance"] - service_user.ldap_sync() + signals.synchronise.send(sender=ServiceUser, instance=service_user) @receiver(post_delete, sender=ServiceUser) def service_user_post_delete(**kwargs): """Django signal, post delete operations on ServiceUser. - Delete service user in ldap. + Delete service user in authentication. """ service_user = kwargs["instance"] - service_user.ldap_del() + signals.remove.send(sender=ServiceUser, instance=service_user) class School(RevMixin, AclMixin, models.Model): @@ -2448,58 +2276,25 @@ class ListRight(RevMixin, AclMixin, Group): def __str__(self): return self.name - def ldap_sync(self): - """Method ldap_sync, sync the listright/group in ldap with its listright attributes. - Each ListRight/Group instance is copy into ldap, via a LdapUserGroup virtual objects. - This method performs a copy of several attributes (name, members, gid, etc). - The primary key is the gid, and should never change. - - Update, or create if needed a ldap entry related with the ListRight/Group instance. - - Parameters: - self (listright instance): ListRight/Group to sync in ldap. - - """ - try: - group_ldap = LdapUserGroup.objects.get(gid=self.gid) - except LdapUserGroup.DoesNotExist: - group_ldap = LdapUserGroup(gid=self.gid) - group_ldap.name = self.unix_name - group_ldap.members = [user.pseudo for user in self.user_set.all()] - group_ldap.save() - - def ldap_del(self): - """Method, delete an ListRight/Group in ldap. - - Parameters: - self (listright/Group instance): group to delete in Ldap. - - """ - try: - group_ldap = LdapUserGroup.objects.get(gid=self.gid) - group_ldap.delete() - except LdapUserGroup.DoesNotExist: - pass - @receiver(post_save, sender=ListRight) def listright_post_save(**kwargs): """Django signal, post save operations on ListRight/Group objects. - Sync or create group in ldap. + Sync or create group in authentication. """ right = kwargs["instance"] - right.ldap_sync() + signals.synchronise.send(sender=ListRight, instance=right) @receiver(post_delete, sender=ListRight) def listright_post_delete(**kwargs): """Django signal, post delete operations on ListRight/Group objects. - Delete group in ldap. + Delete group in authentication. """ right = kwargs["instance"] - right.ldap_del() + signals.remove.send(sender=ListRight, instance=right) class ListShell(RevMixin, AclMixin, models.Model): @@ -2649,13 +2444,13 @@ class Ban(RevMixin, AclMixin, models.Model): @receiver(post_save, sender=Ban) def ban_post_save(**kwargs): """Django signal, post save operations on Ban objects. - Sync user's access state in ldap, call email notification if needed. + Sync user's access state in authentication, call email notification if needed. """ ban = kwargs["instance"] is_created = kwargs["created"] user = ban.user - user.ldap_sync(base=False, access_refresh=True, mac_refresh=False) + signals.synchronise.send(sender=User, instance=user, base=False, access_refresh=True, mac_refresh=False) regen("mailing") if is_created: ban.notif_ban(ban.request) @@ -2669,11 +2464,11 @@ def ban_post_save(**kwargs): @receiver(post_delete, sender=Ban) def ban_post_delete(**kwargs): """Django signal, post delete operations on Ban objects. - Sync user's access state in ldap. + Sync user's access state in authentication. """ user = kwargs["instance"].user - user.ldap_sync(base=False, access_refresh=True, mac_refresh=False) + signals.synchronise.send(sender=User, instance=user, base=False, access_refresh=True, mac_refresh=False) regen("mailing") regen("dhcp") regen("mac_ip_list") @@ -2740,12 +2535,12 @@ class Whitelist(RevMixin, AclMixin, models.Model): @receiver(post_save, sender=Whitelist) def whitelist_post_save(**kwargs): """Django signal, post save operations on Whitelist objects. - Sync user's access state in ldap. + Sync user's access state in authentication. """ whitelist = kwargs["instance"] user = whitelist.user - user.ldap_sync(base=False, access_refresh=True, mac_refresh=False) + signals.synchronise.send(sender=User, instance=user, base=False, access_refresh=True, mac_refresh=False) is_created = kwargs["created"] regen("mailing") if is_created: @@ -2759,11 +2554,11 @@ def whitelist_post_save(**kwargs): @receiver(post_delete, sender=Whitelist) def whitelist_post_delete(**kwargs): """Django signal, post delete operations on Whitelist objects. - Sync user's access state in ldap. + Sync user's access state in authentication. """ user = kwargs["instance"].user - user.ldap_sync(base=False, access_refresh=True, mac_refresh=False) + signals.synchronise.send(sender=User, instance=user, base=False, access_refresh=True, mac_refresh=False) regen("mailing") regen("dhcp") regen("mac_ip_list") @@ -2996,171 +2791,3 @@ class EMailAddress(RevMixin, AclMixin, models.Model): raise ValidationError(reason) super(EMailAddress, self).clean(*args, **kwargs) - -class LdapUser(ldapdb.models.Model): - """A class representing a LdapUser in LDAP, its LDAP conterpart. - Synced from re2o django User model, (User django models), - with a copy of its attributes/fields into LDAP, so this class is a mirror - of the classic django User model. - - The basedn userdn is specified in settings. - - Attributes: - name: The name of this User - uid: The uid (login) for the unix user - uidNumber: Linux uid number - gid: The default gid number for this user - sn: The user "str" pseudo - login_shell: Linux shell for the user - mail: Email address contact for this user - display_name: Pretty display name for this user - dialupAccess: Boolean, True for valid membership - sambaSID: Identical id as uidNumber - user_password: SSHA hashed password of user - samba_nt_password: NTLM hashed password of user - macs: Multivalued mac address - shadowexpire: Set it to 0 to block access for this user and disabled - account - """ - - # LDAP meta-data - base_dn = LDAP["base_user_dn"] - object_classes = [ - "inetOrgPerson", - "top", - "posixAccount", - "sambaSamAccount", - "radiusprofile", - "shadowAccount", - ] - - # attributes - gid = ldapdb.models.fields.IntegerField(db_column="gidNumber") - name = ldapdb.models.fields.CharField( - db_column="cn", max_length=200, primary_key=True - ) - uid = ldapdb.models.fields.CharField(db_column="uid", max_length=200) - uidNumber = ldapdb.models.fields.IntegerField(db_column="uidNumber", unique=True) - sn = ldapdb.models.fields.CharField(db_column="sn", max_length=200) - login_shell = ldapdb.models.fields.CharField( - db_column="loginShell", max_length=200, blank=True, null=True - ) - mail = ldapdb.models.fields.CharField(db_column="mail", max_length=200) - given_name = ldapdb.models.fields.CharField(db_column="givenName", max_length=200) - home_directory = ldapdb.models.fields.CharField( - db_column="homeDirectory", max_length=200 - ) - display_name = ldapdb.models.fields.CharField( - db_column="displayName", max_length=200, blank=True, null=True - ) - dialupAccess = ldapdb.models.fields.CharField(db_column="dialupAccess") - sambaSID = ldapdb.models.fields.IntegerField(db_column="sambaSID", unique=True) - user_password = ldapdb.models.fields.CharField( - db_column="userPassword", max_length=200, blank=True, null=True - ) - sambat_nt_password = ldapdb.models.fields.CharField( - db_column="sambaNTPassword", max_length=200, blank=True, null=True - ) - macs = ldapdb.models.fields.ListField( - db_column="radiusCallingStationId", max_length=200, blank=True, null=True - ) - shadowexpire = ldapdb.models.fields.CharField( - db_column="shadowExpire", blank=True, null=True - ) - - def __str__(self): - return self.name - - def __unicode__(self): - return self.name - - def save(self, *args, **kwargs): - self.sn = self.name - self.uid = self.name - self.sambaSID = self.uidNumber - super(LdapUser, self).save(*args, **kwargs) - - -class LdapUserGroup(ldapdb.models.Model): - """A class representing a LdapUserGroup in LDAP, its LDAP conterpart. - Synced from UserGroup, (ListRight/Group django models), - with a copy of its attributes/fields into LDAP, so this class is a mirror - of the classic django ListRight model. - - The basedn usergroupdn is specified in settings. - - Attributes: - name: The name of this LdapUserGroup - gid: The gid number for this unix group - members: Users dn members of this LdapUserGroup - """ - - # LDAP meta-data - base_dn = LDAP["base_usergroup_dn"] - object_classes = ["posixGroup"] - - # attributes - gid = ldapdb.models.fields.IntegerField(db_column="gidNumber") - members = ldapdb.models.fields.ListField(db_column="memberUid", blank=True) - name = ldapdb.models.fields.CharField( - db_column="cn", max_length=200, primary_key=True - ) - - def __str__(self): - return self.name - - -class LdapServiceUser(ldapdb.models.Model): - """A class representing a ServiceUser in LDAP, its LDAP conterpart. - Synced from ServiceUser, with a copy of its attributes/fields into LDAP, - so this class is a mirror of the classic django ServiceUser model. - - The basedn userservicedn is specified in settings. - - Attributes: - name: The name of this ServiceUser - user_password: The SSHA hashed password of this ServiceUser - """ - - # LDAP meta-data - base_dn = LDAP["base_userservice_dn"] - object_classes = ["applicationProcess", "simpleSecurityObject"] - - # attributes - name = ldapdb.models.fields.CharField( - db_column="cn", max_length=200, primary_key=True - ) - user_password = ldapdb.models.fields.CharField( - db_column="userPassword", max_length=200, blank=True, null=True - ) - - def __str__(self): - return self.name - - -class LdapServiceUserGroup(ldapdb.models.Model): - """A class representing a ServiceUserGroup in LDAP, its LDAP conterpart. - Synced from ServiceUserGroup, with a copy of its attributes/fields into LDAP, - so this class is a mirror of the classic django ServiceUserGroup model. - - The basedn userservicegroupdn is specified in settings. - - Attributes: - name: The name of this ServiceUserGroup - members: ServiceUsers dn members of this ServiceUserGroup - """ - - # LDAP meta-data - base_dn = LDAP["base_userservicegroup_dn"] - object_classes = ["groupOfNames"] - - # attributes - name = ldapdb.models.fields.CharField( - db_column="cn", max_length=200, primary_key=True - ) - members = ldapdb.models.fields.ListField(db_column="member", blank=True) - - def __str__(self): - return self.name - - diff --git a/users/signals.py b/users/signals.py new file mode 100644 index 00000000..7336e5fc --- /dev/null +++ b/users/signals.py @@ -0,0 +1,33 @@ +""" +A set of signals used by users. Various classes in users emit these signals to signal the need to sync or +remove an object from optionnal authentication backends, e.g. LDAP. + +* `users.signals.synchronise`: + Expresses the need for an instance of a users class to be synchronised. `sender` and `instance` are + always set. It is up to the receiver to ensure the others are set correctly if they make sense. + Arguments: + * `sender` : The model class. + * `instance` : The actual instance being synchronised. + * `base` : Default `True`. When `True`, synchronise basic attributes. + * `access_refresh` : Default `True`. When `True`, synchronise the access time. + * `mac_refresh` : Default `True`. When True, synchronise the list of mac addresses. + * `group_refresh`: Default `False`. When `True` synchronise the groups of the instance. +* `users.signals.remove`: + Expresses the need for an instance of a users class to be removed. + Arguments: + * `sender` : The model class. + * `instance` : The actual instance being removed. +* `users.signals.remove_mass`: + Same as `users.signals.remove` except it removes a queryset. For now it is only used by `users.models.User`. + Arguments: + * `sender` : The model class. + * `queryset` : The actual instances being removed. + +""" + +import django.dispatch + +synchronise = django.dispatch.Signal(providing_args=["sender", "instance", "base", "access_refresh", "mac_refresh", "group_refresh"]) +remove = django.dispatch.Signal(providing_args=["sender", "instance"]) +remove_mass = django.dispatch.Signal(providing_args=["sender", "queryset"]) +