diff --git a/users/admin.py b/users/admin.py index df868017..da0293cc 100644 --- a/users/admin.py +++ b/users/admin.py @@ -3,8 +3,8 @@ from django.contrib.auth.models import Group from django.contrib.auth.admin import UserAdmin as BaseUserAdmin from reversion.admin import VersionAdmin -from .models import User, School, Right, ListRight, ListShell, Ban, Whitelist, Request, LdapUser, LdapUserGroup -from .forms import UserChangeForm, UserCreationForm +from .models import User, ServiceUser, School, Right, ListRight, ListShell, Ban, Whitelist, Request, LdapUser, LdapServiceUser, LdapUserGroup +from .forms import UserChangeForm, UserCreationForm, ServiceUserChangeForm, ServiceUserCreationForm class UserAdmin(admin.ModelAdmin): @@ -22,10 +22,15 @@ class UserAdmin(admin.ModelAdmin): class LdapUserAdmin(admin.ModelAdmin): - list_display = ('name','uidNumber','loginShell') + list_display = ('name','uidNumber','login_shell') exclude = ('user_password','sambat_nt_password') search_fields = ('name',) +class LdapServiceUserAdmin(admin.ModelAdmin): + list_display = ('name',) + exclude = ('user_password',) + search_fields = ('name',) + class LdapUserGroupAdmin(admin.ModelAdmin): list_display = ('name','members','gid') search_fields = ('name',) @@ -62,10 +67,11 @@ class UserAdmin(VersionAdmin, BaseUserAdmin): # These override the definitions on the base UserAdmin # that reference specific fields on auth.User. list_display = ('pseudo', 'name', 'surname', 'email', 'school', 'is_admin', 'shell') + list_display = ('pseudo',) list_filter = () fieldsets = ( (None, {'fields': ('pseudo', 'password')}), - ('Personal info', {'fields': ('name', 'surname', 'email', 'school','shell')}), + ('Personal info', {'fields': ('name', 'surname', 'email', 'school','shell', 'uid_number')}), ('Permissions', {'fields': ('is_admin', )}), ) # add_fieldsets is not a standard ModelAdmin attribute. UserAdmin @@ -80,9 +86,36 @@ class UserAdmin(VersionAdmin, BaseUserAdmin): ordering = ('pseudo',) filter_horizontal = () +class ServiceUserAdmin(VersionAdmin, BaseUserAdmin): + # The forms to add and change user instances + form = ServiceUserChangeForm + add_form = ServiceUserCreationForm + + # The fields to be used in displaying the User model. + # These override the definitions on the base UserAdmin + # that reference specific fields on auth.User. + list_display = ('pseudo',) + list_filter = () + fieldsets = ( + (None, {'fields': ('pseudo', 'password')}), + ) + # add_fieldsets is not a standard ModelAdmin attribute. UserAdmin + # overrides get_fieldsets to use this attribute when creating a user. + add_fieldsets = ( + (None, { + 'classes': ('wide',), + 'fields': ('pseudo', 'password1', 'password2')} + ), + ) + search_fields = ('pseudo',) + ordering = ('pseudo',) + filter_horizontal = () + admin.site.register(User, UserAdmin) +admin.site.register(ServiceUser, ServiceUserAdmin) admin.site.register(LdapUser, LdapUserAdmin) admin.site.register(LdapUserGroup, LdapUserGroupAdmin) +admin.site.register(LdapServiceUser, LdapServiceUserAdmin) admin.site.register(School, SchoolAdmin) admin.site.register(Right, RightAdmin) admin.site.register(ListRight, ListRightAdmin) @@ -92,7 +125,9 @@ admin.site.register(Whitelist, WhitelistAdmin) admin.site.register(Request, RequestAdmin) # Now register the new UserAdmin... admin.site.unregister(User) +admin.site.unregister(ServiceUser) admin.site.register(User, UserAdmin) +admin.site.register(ServiceUser, ServiceUserAdmin) # ... and, since we're not using Django's built-in permissions, # unregister the Group model from admin. admin.site.unregister(Group) diff --git a/users/forms.py b/users/forms.py index 88cd567f..5b6482d7 100644 --- a/users/forms.py +++ b/users/forms.py @@ -4,7 +4,7 @@ from django import forms from django.contrib.auth.forms import ReadOnlyPasswordHashField -from .models import User, get_admin_right +from .models import User, ServiceUser, get_admin_right class PassForm(forms.Form): @@ -39,6 +39,30 @@ class UserCreationForm(forms.ModelForm): user.is_admin = self.cleaned_data.get("is_admin") return user +class ServiceUserCreationForm(forms.ModelForm): + """A form for creating new users. Includes all the required + fields, plus a repeated password.""" + password1 = forms.CharField(label='Password', widget=forms.PasswordInput, min_length=8, max_length=255) + password2 = forms.CharField(label='Password confirmation', widget=forms.PasswordInput, min_length=8, max_length=255) + + class Meta: + model = ServiceUser + fields = ('pseudo',) + + def clean_password2(self): + # 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("Passwords don't match") + return password2 + + def save(self, commit=True): + # Save the provided password in hashed format + user = super(ServiceUserCreationForm, self).save(commit=False) + user.set_password(self.cleaned_data["password1"]) + user.save() + return user class UserChangeForm(forms.ModelForm): """A form for updating users. Includes all the fields on @@ -71,6 +95,23 @@ class UserChangeForm(forms.ModelForm): user.save() return user +class ServiceUserChangeForm(forms.ModelForm): + """A form for updating users. Includes all the fields on + the user, but replaces the password field with admin's + password hash display field. + """ + password = ReadOnlyPasswordHashField() + + class Meta: + model = ServiceUser + fields = ('pseudo',) + + def clean_password(self): + # Regardless of what the user provides, return the initial value. + # This is done here, rather than on the field, because the + # field does not have access to the initial value + return self.initial["password"] + class ResetPasswordForm(forms.Form): pseudo = forms.CharField(label=u'Pseudo', max_length=255) email = forms.EmailField(max_length=255) diff --git a/users/migrations/0030_auto_20160726_0357.py b/users/migrations/0030_auto_20160726_0357.py new file mode 100644 index 00000000..bd1d0e91 --- /dev/null +++ b/users/migrations/0030_auto_20160726_0357.py @@ -0,0 +1,21 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import migrations, models +import django.db.models.deletion +import ldapdb.models.fields + + +class Migration(migrations.Migration): + + dependencies = [ + ('users', '0029_auto_20160726_0229'), + ] + + operations = [ + migrations.AlterField( + model_name='ldapuser', + name='display_name', + field=ldapdb.models.fields.CharField(null=True, max_length=200, db_column='displayName', blank=True), + ), + ] diff --git a/users/migrations/0031_auto_20160726_0359.py b/users/migrations/0031_auto_20160726_0359.py new file mode 100644 index 00000000..c3f5f98b --- /dev/null +++ b/users/migrations/0031_auto_20160726_0359.py @@ -0,0 +1,21 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import migrations, models +import ldapdb.models.fields +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('users', '0030_auto_20160726_0357'), + ] + + operations = [ + migrations.AlterField( + model_name='user', + name='shell', + field=models.ForeignKey(to='users.ListShell', on_delete=django.db.models.deletion.PROTECT, null=True, blank=True), + ), + ] diff --git a/users/migrations/0032_auto_20160727_2122.py b/users/migrations/0032_auto_20160727_2122.py new file mode 100644 index 00000000..beeeaca9 --- /dev/null +++ b/users/migrations/0032_auto_20160727_2122.py @@ -0,0 +1,39 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import migrations, models +import users.models +import ldapdb.models.fields + + +class Migration(migrations.Migration): + + dependencies = [ + ('users', '0031_auto_20160726_0359'), + ] + + operations = [ + migrations.CreateModel( + name='LdapServiceUser', + fields=[ + ('dn', models.CharField(max_length=200)), + ('name', ldapdb.models.fields.CharField(db_column='cn', max_length=200, serialize=False, primary_key=True)), + ('user_password', ldapdb.models.fields.CharField(db_column='userPassword', blank=True, max_length=200, null=True)), + ], + options={ + 'abstract': False, + }, + ), + migrations.CreateModel( + name='ServiceUser', + fields=[ + ('id', models.AutoField(verbose_name='ID', auto_created=True, primary_key=True, serialize=False)), + ('password', models.CharField(max_length=128, verbose_name='password')), + ('last_login', models.DateTimeField(blank=True, verbose_name='last login', null=True)), + ('pseudo', models.CharField(max_length=32, help_text='Doit contenir uniquement des lettres, chiffres, ou tirets', unique=True, validators=[users.models.linux_user_validator])), + ], + options={ + 'abstract': False, + }, + ), + ] diff --git a/users/migrations/0033_remove_ldapuser_loginshell.py b/users/migrations/0033_remove_ldapuser_loginshell.py new file mode 100644 index 00000000..afe71627 --- /dev/null +++ b/users/migrations/0033_remove_ldapuser_loginshell.py @@ -0,0 +1,18 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('users', '0032_auto_20160727_2122'), + ] + + operations = [ + migrations.RemoveField( + model_name='ldapuser', + name='loginShell', + ), + ] diff --git a/users/migrations/0034_user_uid_number.py b/users/migrations/0034_user_uid_number.py new file mode 100644 index 00000000..1761ab61 --- /dev/null +++ b/users/migrations/0034_user_uid_number.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('users', '0033_remove_ldapuser_loginshell'), + ] + + operations = [ + migrations.AddField( + model_name='user', + name='uid_number', + field=models.IntegerField(default=1), + preserve_default=False, + ), + ] diff --git a/users/migrations/0035_auto_20160731_0448.py b/users/migrations/0035_auto_20160731_0448.py new file mode 100644 index 00000000..eccea8ea --- /dev/null +++ b/users/migrations/0035_auto_20160731_0448.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import migrations, models +import users.models + + +class Migration(migrations.Migration): + + dependencies = [ + ('users', '0034_user_uid_number'), + ] + + operations = [ + migrations.AlterField( + model_name='user', + name='uid_number', + field=models.IntegerField(default=users.models.User.auto_uid), + ), + ] diff --git a/users/migrations/0036_auto_20160731_0448.py b/users/migrations/0036_auto_20160731_0448.py new file mode 100644 index 00000000..ade24828 --- /dev/null +++ b/users/migrations/0036_auto_20160731_0448.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import migrations, models +import users.models + + +class Migration(migrations.Migration): + + dependencies = [ + ('users', '0035_auto_20160731_0448'), + ] + + operations = [ + migrations.AlterField( + model_name='user', + name='uid_number', + field=models.IntegerField(default=users.models.User.auto_uid, unique=True), + ), + ] diff --git a/users/migrations/0037_ldapuser_login_shell.py b/users/migrations/0037_ldapuser_login_shell.py new file mode 100644 index 00000000..d412fa5c --- /dev/null +++ b/users/migrations/0037_ldapuser_login_shell.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import migrations, models +import ldapdb.models.fields + + +class Migration(migrations.Migration): + + dependencies = [ + ('users', '0036_auto_20160731_0448'), + ] + + operations = [ + migrations.AddField( + model_name='ldapuser', + name='login_shell', + field=ldapdb.models.fields.CharField(null=True, db_column='loginShell', blank=True, max_length=200), + ), + ] diff --git a/users/models.py b/users/models.py index a0abb260..63ef614b 100644 --- a/users/models.py +++ b/users/models.py @@ -8,7 +8,7 @@ from django.dispatch import receiver import ldapdb.models import ldapdb.models.fields -from re2o.settings import RIGHTS_LINK, REQ_EXPIRE_HRS, LDAP +from re2o.settings import RIGHTS_LINK, REQ_EXPIRE_HRS, LDAP, UID_RANGES import re, uuid import datetime @@ -98,6 +98,12 @@ class User(AbstractBaseUser): (2, 'STATE_ARCHIVED'), ) + def auto_uid(): + uids = list(range(int(min(UID_RANGES['users'])),int(max(UID_RANGES['users'])))) + used_uids = [ user.id for user in User.objects.all()] + free_uids = [ id for id in uids if id not in used_uids] + return min(free_uids) + name = models.CharField(max_length=255) surname = models.CharField(max_length=255) pseudo = models.CharField(max_length=32, unique=True, help_text="Doit contenir uniquement des lettres, chiffres, ou tirets", validators=[linux_user_validator]) @@ -109,6 +115,7 @@ class User(AbstractBaseUser): pwd_ntlm = models.CharField(max_length=255) state = models.IntegerField(choices=STATES, default=STATE_ACTIVE) registered = models.DateTimeField(auto_now_add=True) + uid_number = models.IntegerField(default=auto_uid, unique=True) USERNAME_FIELD = 'pseudo' REQUIRED_FIELDS = ['name', 'surname', 'email'] @@ -228,13 +235,13 @@ class User(AbstractBaseUser): def ldap_sync(self, base=True, access_refresh=True, mac_refresh=True): self.refresh_from_db() try: - user_ldap = LdapUser.objects.get(name=self.pseudo) + user_ldap = LdapUser.objects.get(uidNumber=self.id) except LdapUser.DoesNotExist: - user_ldap = LdapUser(name=self.pseudo) + user_ldap = LdapUser(uidNumber=self.id) if base: + user_ldap.name = self.pseudo user_ldap.sn = self.pseudo user_ldap.dialupAccess = str(self.has_access()) - user_ldap.uidNumber = self.id user_ldap.home_directory = '/home/' + self.pseudo user_ldap.mail = self.email user_ldap.given_name = str(self.surname).lower() + '_' + str(self.name).lower()[:3] @@ -242,7 +249,7 @@ class User(AbstractBaseUser): user_ldap.user_password = self.password user_ldap.sambat_nt_password = self.pwd_ntlm if self.shell: - user_ldap.loginShell = self.shell.shell + user_ldap.login_shell = self.shell.shell if access_refresh: user_ldap.dialupAccess = str(self.has_access()) if mac_refresh: @@ -425,7 +432,7 @@ class LdapUser(ldapdb.models.Model): 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) - loginShell = ldapdb.models.fields.CharField(db_column='loginShell', max_length=200, blank=True, null=True) + 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)