Fix tests for Python 3

- drop Python 2 __future__ imports
- fix tests to handle new Django and Python 3 module names
- reformat changed files with black
This commit is contained in:
Jan Dittberner 2019-01-30 21:27:25 +01:00
parent ddec6b4184
commit 3d18392b67
32 changed files with 2707 additions and 2675 deletions

View file

@ -2,20 +2,16 @@
This module defines the database models of operating system users.
"""
from __future__ import unicode_literals
import base64
from datetime import date
import logging
import os
import six
from django.db import models, transaction
from django.conf import settings
from django.core.exceptions import ValidationError
from django.dispatch import Signal
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
@ -27,11 +23,10 @@ from passlib.utils import generate_password
_LOGGER = logging.getLogger(__name__)
password_set = Signal(providing_args=['instance', 'password'])
password_set = Signal(providing_args=["instance", "password"])
CANNOT_USE_PRIMARY_GROUP_AS_ADDITIONAL = _(
"You can not use a user's primary group.")
CANNOT_USE_PRIMARY_GROUP_AS_ADDITIONAL = _("You can not use a user's primary group.")
class GroupManager(models.Manager):
@ -48,34 +43,31 @@ class GroupManager(models.Manager):
:rtype: int
"""
q = self.aggregate(models.Max('gid'))
if q['gid__max'] is None:
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)
return max(settings.OSUSER_MINGID, q["gid__max"] + 1)
@python_2_unicode_compatible
class Group(TimeStampedModel, models.Model):
"""
This entity class corresponds to an operating system group.
"""
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)
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')
verbose_name = _("Group")
verbose_name_plural = _("Groups")
def __str__(self):
return '{0} ({1})'.format(self.groupname, self.gid)
return "{0} ({1})".format(self.groupname, self.gid)
@transaction.atomic
def save(self, *args, **kwargs):
@ -122,10 +114,10 @@ class UserManager(models.Manager):
:rtype: int
"""
q = self.aggregate(models.Max('uid'))
if q['uid__max'] is None:
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)
return max(settings.OSUSER_MINUID, q["uid__max"] + 1)
def get_next_username(self):
"""
@ -137,23 +129,21 @@ class UserManager(models.Manager):
"""
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:
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)
nextuser = usernameformat.format(settings.OSUSER_USERNAME_PREFIX, count)
else:
break
return nextuser
@transaction.atomic
def create_user(
self, customer, username=None, password=None, commit=False
):
def create_user(self, customer, username=None, password=None, commit=False):
"""
Create a new user with a primary group named the same as the user and
an initial password.
@ -179,41 +169,42 @@ class UserManager(models.Manager):
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, customer=customer,
shell=settings.OSUSER_DEFAULT_SHELL)
user = self.create(
username=username,
group=group,
uid=uid,
homedir=homedir,
customer=customer,
shell=settings.OSUSER_DEFAULT_SHELL,
)
user.set_password(password)
if commit:
user.save()
return user
@python_2_unicode_compatible
class User(TimeStampedModel, models.Model):
"""
This entity class corresponds to an operating system user.
"""
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'), on_delete=models.CASCADE)
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)
customer = models.ForeignKey(
settings.AUTH_USER_MODEL, on_delete=models.CASCADE)
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"), on_delete=models.CASCADE)
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)
customer = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE)
objects = UserManager()
class Meta:
verbose_name = _('User')
verbose_name_plural = _('Users')
verbose_name = _("User")
verbose_name_plural = _("Users")
def __str__(self):
return '{0} ({1})'.format(self.username, self.uid)
return "{0} ({1})".format(self.username, self.uid)
@transaction.atomic
def set_password(self, password):
@ -226,17 +217,15 @@ class User(TimeStampedModel, models.Model):
:param str password: the new password
"""
if hasattr(self, 'shadow'):
if hasattr(self, "shadow"):
self.shadow.set_password(password)
else:
self.shadow = Shadow.objects.create_shadow(
user=self, password=password
)
password_set.send(
sender=self.__class__, password=password, instance=self)
self.shadow = Shadow.objects.create_shadow(user=self, password=password)
password_set.send(sender=self.__class__, password=password, instance=self)
return True
def is_sftp_user(self):
# noinspection PyUnresolvedReferences
return self.additionalgroup_set.filter(
group__groupname=settings.OSUSER_SFTP_GROUP
).exists()
@ -269,6 +258,7 @@ class User(TimeStampedModel, models.Model):
:py:meth:`django.db.Model.delete`
"""
# noinspection PyUnresolvedReferences
self.group.delete()
super(User, self).delete(*args, **kwargs)
@ -293,64 +283,84 @@ class ShadowManager(models.Manager):
"""
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
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):
"""
This entity class corresponds to an operating system user's shadow file
entry.
"""
user = models.OneToOneField(
User, primary_key=True, verbose_name=_('User'),
on_delete=models.CASCADE)
passwd = models.CharField(_('Encrypted password'), max_length=128)
User, primary_key=True, verbose_name=_("User"), on_delete=models.CASCADE
)
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)
_("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)
_("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)
_("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)
_("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)
_("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)
_("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')
verbose_name = _("Shadow password")
verbose_name_plural = _("Shadow passwords")
def __str__(self):
return 'for user {0}'.format(self.user)
return "for user {0}".format(self.user)
def set_password(self, password):
"""
@ -361,23 +371,23 @@ class Shadow(TimeStampedModel, models.Model):
self.passwd = sha512_crypt.encrypt(password)
@python_2_unicode_compatible
class AdditionalGroup(TimeStampedModel, models.Model):
"""
This entity class corresponds to additional group assignments for an
:py:class:`operating system user <osusers.models.User>`.
"""
user = models.ForeignKey(User, on_delete=models.CASCADE)
group = models.ForeignKey(Group, on_delete=models.CASCADE)
class Meta:
unique_together = ('user', 'group')
verbose_name = _('Additional group')
verbose_name_plural = _('Additional groups')
unique_together = ("user", "group")
verbose_name = _("Additional group")
verbose_name_plural = _("Additional groups")
def __str__(self):
return '{0} in {1}'.format(self.user, self.group)
return "{0} in {1}".format(self.user, self.group)
def clean(self):
"""
@ -385,6 +395,7 @@ class AdditionalGroup(TimeStampedModel, models.Model):
group.
"""
# noinspection PyUnresolvedReferences
if self.user.group == self.group:
raise ValidationError(CANNOT_USE_PRIMARY_GROUP_AS_ADDITIONAL)
@ -423,60 +434,62 @@ class SshPublicKeyManager(models.Manager):
"""
def parse_keytext(self, keytext):
def parse_key_text(self, key_text: str):
"""
Parse a SSH public key in OpenSSH or :rfc:`4716` format into its
components algorithm, key data and comment.
:param str keytext: key text
:param str key_text: key text
:return: triple of algorithm name, key data and comment
:rtype: triple of str
"""
if keytext.startswith('---- BEGIN SSH2 PUBLIC KEY ----'):
comment = ''
data = ''
continued = ''
if key_text.startswith("---- BEGIN SSH2 PUBLIC KEY ----"):
comment = ""
data = ""
continued = ""
headers = {}
for line in keytext.splitlines():
if line == '---- BEGIN SSH2 PUBLIC KEY ----':
header_tag = None
for line in key_text.splitlines():
if line == "---- BEGIN SSH2 PUBLIC KEY ----":
continue
elif ':' in line: # a header line
elif ":" in line: # a header line
header_tag, header_value = [
item.strip() for item in line.split(':', 1)]
if header_value.endswith('\\'):
item.strip() for item in line.split(":", 1)
]
if header_value.endswith("\\"):
continued = header_value[:-1]
else:
headers[header_tag.lower()] = header_value
elif continued:
if line.endswith('\\'):
if line.endswith("\\"):
continued += line[:-1]
continue
header_value = continued + line
headers[header_tag.lower()] = header_value
continued = ''
elif line == '---- END SSH2 PUBLIC KEY ----':
continued = ""
elif line == "---- END SSH2 PUBLIC KEY ----":
break
elif line: # ignore empty lines
data += line
if 'comment' in headers:
comment = headers['comment']
if "comment" in headers:
comment = headers["comment"]
else:
parts = keytext.split(None, 2)
parts = key_text.split(None, 2)
if len(parts) < 2:
raise ValueError('invalid SSH public key')
raise ValueError("invalid SSH public key")
data = parts[1]
comment = len(parts) == 3 and parts[2] or ""
try:
keybytes = base64.b64decode(data)
except TypeError:
raise ValueError('invalid SSH public key')
parts = keybytes.split(b'\x00' * 3)
raise ValueError("invalid SSH public key")
parts = keybytes.split(b"\x00" * 3)
if len(parts) < 2:
raise ValueError('invalid SSH public key')
alglength = six.byte2int(parts[1])
algname = parts[1][1:1+alglength]
return algname, data, comment
raise ValueError("invalid SSH public key")
key_length = int.from_bytes(parts[1], byteorder="big")
key_algorithm = parts[1][1 : 1 + key_length].decode("utf-8")
return key_algorithm, data, comment
def create_ssh_public_key(self, user, keytext):
"""
@ -490,31 +503,28 @@ class SshPublicKeyManager(models.Manager):
:retype: :py:class:`osusers.models.SshPublicKey`
"""
algorithm, data, comment = self.parse_keytext(keytext)
return self.create(
user=user, algorithm=algorithm, data=data, comment=comment)
algorithm, data, comment = self.parse_key_text(keytext)
return self.create(user=user, algorithm=algorithm, data=data, comment=comment)
@python_2_unicode_compatible
class SshPublicKey(TimeStampedModel):
"""
This entity class represents single SSH keys for an :py:class:`operating
system user <osusers.models.User>`.
"""
user = models.ForeignKey(
User, verbose_name=_('User'), on_delete=models.CASCADE)
algorithm = models.CharField(_('Algorithm'), max_length=20)
data = models.TextField(_('Key bytes'),
help_text=_('Base64 encoded key bytes'))
comment = models.TextField(_('Comment'), blank=True)
user = models.ForeignKey(User, verbose_name=_("User"), on_delete=models.CASCADE)
algorithm = models.CharField(_("Algorithm"), max_length=20)
data = models.TextField(_("Key bytes"), help_text=_("Base64 encoded key bytes"))
comment = models.TextField(_("Comment"), blank=True)
objects = SshPublicKeyManager()
class Meta:
verbose_name = _('SSH public key')
verbose_name_plural = _('SSH public keys')
unique_together = [('user', 'algorithm', 'data')]
verbose_name = _("SSH public key")
verbose_name_plural = _("SSH public keys")
unique_together = [("user", "algorithm", "data")]
def __str__(self):
return "{algorithm} {data} {comment}".format(