from __future__ import unicode_literals from datetime import date import logging import os from django.db import models, transaction from django.conf import settings from django.core.exceptions import ValidationError from django.utils import timezone from django.utils.encoding import python_2_unicode_compatible from django.utils.translation import ugettext as _ from model_utils.models import TimeStampedModel from celery.result import AsyncResult from passlib.hash import sha512_crypt from passlib.utils import generate_password from .tasks import ( add_ldap_user_to_group, create_ldap_group, create_ldap_user, delete_file_mail_userdir, delete_file_sftp_userdir, delete_ldap_group_if_empty, delete_ldap_user, remove_ldap_user_from_group, setup_file_mail_userdir, setup_file_sftp_userdir, ) logger = logging.getLogger(__name__) CANNOT_USE_PRIMARY_GROUP_AS_ADDITIONAL = _( "You can not use a user's primary group.") class GroupManager(models.Manager): def get_next_gid(self): q = self.aggregate(models.Max('gid')) if q['gid__max'] is None: return settings.OSUSER_MINGID return max(settings.OSUSER_MINGID, q['gid__max'] + 1) @python_2_unicode_compatible class Group(TimeStampedModel, models.Model): groupname = models.CharField( _('Group name'), max_length=16, unique=True) gid = models.PositiveSmallIntegerField( _('Group ID'), unique=True, primary_key=True) descr = models.TextField(_('Description'), blank=True) passwd = models.CharField( _('Group password'), max_length=128, blank=True) objects = GroupManager() class Meta: verbose_name = _('Group') verbose_name_plural = _('Groups') def __str__(self): return '{0} ({1})'.format(self.groupname, self.gid) @transaction.atomic def save(self, *args, **kwargs): super(Group, self).save(*args, **kwargs) dn = create_ldap_group.delay( self.groupname, self.gid, self.descr).get() logger.info("created LDAP group with dn %s", dn) return self @transaction.atomic def delete(self, *args, **kwargs): delete_ldap_group_if_empty.delay(self.groupname).get() super(Group, self).delete(*args, **kwargs) class UserManager(models.Manager): def get_next_uid(self): q = self.aggregate(models.Max('uid')) if q['uid__max'] is None: return settings.OSUSER_MINUID return max(settings.OSUSER_MINUID, q['uid__max'] + 1) def get_next_username(self): count = 1 usernameformat = "{0}{1:02d}" nextuser = usernameformat.format(settings.OSUSER_USERNAME_PREFIX, count) for user in self.values('username').filter( username__startswith=settings.OSUSER_USERNAME_PREFIX).order_by( 'username'): if user['username'] == nextuser: count += 1 nextuser = usernameformat.format( settings.OSUSER_USERNAME_PREFIX, count) else: break return nextuser @transaction.atomic def create_user(self, username=None, password=None, commit=False): uid = self.get_next_uid() gid = Group.objects.get_next_gid() if username is None: username = self.get_next_username() if password is None: password = generate_password() homedir = os.path.join(settings.OSUSER_HOME_BASEPATH, username) group = Group.objects.create(groupname=username, gid=gid) user = self.create(username=username, group=group, uid=uid, homedir=homedir, shell=settings.OSUSER_DEFAULT_SHELL) user.set_password(password) if commit: user.save() return user @python_2_unicode_compatible class User(TimeStampedModel, models.Model): username = models.CharField( _('User name'), max_length=64, unique=True) uid = models.PositiveSmallIntegerField( _('User ID'), unique=True, primary_key=True) group = models.ForeignKey(Group, verbose_name=_('Group')) gecos = models.CharField(_('Gecos field'), max_length=128, blank=True) homedir = models.CharField(_('Home directory'), max_length=256) shell = models.CharField(_('Login shell'), max_length=64) objects = UserManager() class Meta: verbose_name = _('User') verbose_name_plural = _('Users') def __str__(self): return '{0} ({1})'.format(self.username, self.uid) @transaction.atomic def set_password(self, password): if hasattr(self, 'shadow'): self.shadow.set_password(password) else: self.shadow = Shadow.objects.create_shadow( user=self, password=password ) dn = create_ldap_user.delay( self.username, self.uid, self.group.gid, self.gecos, self.homedir, self.shell, password ).get() logging.info("set LDAP password for %s", dn) @transaction.atomic def save(self, *args, **kwargs): dn = create_ldap_user.delay( self.username, self.uid, self.group.gid, self.gecos, self.homedir, self.shell, password=None).get() sftp_dir = setup_file_sftp_userdir.delay(self.username).get() mail_dir = setup_file_mail_userdir.delay(self.username).get() logger.info( "created user %(user)s with LDAP dn %(dn)s, home directory " "%(homedir)s and mail base directory %(maildir)s.", { 'user': self, 'dn': dn, 'homedir': sftp_dir, 'maildir': mail_dir }) return super(User, self).save(*args, **kwargs) @transaction.atomic def delete(self, *args, **kwargs): delete_file_mail_userdir.delay(self.username).get() delete_file_sftp_userdir.delay(self.username).get() for group in [ag.group for ag in self.additionalgroup_set.all()]: remove_ldap_user_from_group.delay( self.username, group.groupname).get() delete_ldap_user.delay(self.username).get() self.group.delete() super(User, self).delete(*args, **kwargs) class ShadowManager(models.Manager): @transaction.atomic def create_shadow(self, user, password): changedays = (timezone.now().date() - date(1970, 1, 1)).days shadow = self.create( user=user, changedays=changedays, minage=0, maxage=None, gracedays=7, inactdays=30, expiredays=None ) shadow.set_password(password) shadow.save() return shadow @python_2_unicode_compatible class Shadow(TimeStampedModel, models.Model): user = models.OneToOneField(User, primary_key=True, verbose_name=_('User')) passwd = models.CharField(_('Encrypted password'), max_length=128) changedays = models.PositiveSmallIntegerField( _('Date of last change'), help_text=_('This is expressed in days since Jan 1, 1970'), blank=True, null=True) minage = models.PositiveSmallIntegerField( _('Minimum age'), help_text=_('Minimum number of days before the password can be' ' changed'), blank=True, null=True) maxage = models.PositiveSmallIntegerField( _('Maximum age'), help_text=_('Maximum number of days after which the password has to' ' be changed'), blank=True, null=True) gracedays = models.PositiveSmallIntegerField( _('Grace period'), help_text=_('The number of days before the password is going to' ' expire'), blank=True, null=True) inactdays = models.PositiveSmallIntegerField( _('Inactivity period'), help_text=_('The number of days after the password has expired during' ' which the password should still be accepted'), blank=True, null=True) expiredays = models.PositiveSmallIntegerField( _('Account expiration date'), help_text=_('The date of expiration of the account, expressed as' ' number of days since Jan 1, 1970'), blank=True, null=True, default=None) objects = ShadowManager() class Meta: verbose_name = _('Shadow password') verbose_name_plural = _('Shadow passwords') def __str__(self): return 'for user {0}'.format(self.user) def set_password(self, password): self.passwd = sha512_crypt.encrypt(password) @python_2_unicode_compatible class AdditionalGroup(TimeStampedModel, models.Model): user = models.ForeignKey(User) group = models.ForeignKey(Group) class Meta: unique_together = ('user', 'group') verbose_name = _('Additional group') verbose_name_plural = _('Additional groups') def clean(self): if self.user.group == self.group: raise ValidationError(CANNOT_USE_PRIMARY_GROUP_AS_ADDITIONAL) @transaction.atomic def save(self, *args, **kwargs): add_ldap_user_to_group.delay( self.user.username, self.group.groupname).get() super(AdditionalGroup, self).save(*args, **kwargs) @transaction.atomic def delete(self, *args, **kwargs): remove_ldap_user_from_group.delay( self.user.username, self.group.groupname).get() super(AdditionalGroup, self).delete(*args, **kwargs) def __str__(self): return '{0} in {1}'.format(self.user, self.group)