Merge pull request #1642 from normanjaeckel/CleanupUsers

Cleaned up users app.
This commit is contained in:
Norman Jäckel 2015-09-21 16:46:33 +02:00
commit 9599e4c022
19 changed files with 175 additions and 308 deletions

View File

@ -91,13 +91,6 @@ INSTALLED_APPS = (
'openslides.mediafiles',
)
TEMPLATE_CONTEXT_PROCESSORS = (
'openslides.users.auth.auth',
'django.contrib.messages.context_processors.messages',
'django.core.context_processors.request',
'django.core.context_processors.i18n',
'django.core.context_processors.static',
)
CACHES = {
'default': {
@ -175,6 +168,6 @@ TEST_RUNNER = 'openslides.utils.test.OpenSlidesDiscoverRunner'
REST_FRAMEWORK = {
'DEFAULT_AUTHENTICATION_CLASSES': (
'rest_framework.authentication.SessionAuthentication',
'openslides.users.auth.AnonymousAuthentication',
'openslides.users.auth.RESTFrameworkAnonymousAuthentication',
)
}

View File

@ -1,10 +0,0 @@
from .models import Permission
def get_protected_perm():
"""
Returns the permission to manage users. This function is a helper
function used to protect manager users from locking out themselves.
"""
return Permission.objects.get_by_natural_key(
app_label='users', model='user', codename='can_manage')

View File

@ -10,12 +10,12 @@ class UsersAppConfig(AppConfig):
def ready(self):
# Load projector elements.
# Do this by just importing all from these files.
# Just import this file.
from . import projector # noqa
# Import all required stuff.
from openslides.core.signals import config_signal, post_permission_creation
from openslides.utils.rest_api import router
from ..core.signals import config_signal, post_permission_creation
from ..utils.rest_api import router
from .signals import create_builtin_groups_and_admin, setup_users_config
from .views import GroupViewSet, UserViewSet

View File

@ -1,63 +1,30 @@
from django.contrib.auth import get_user as _get_user
from django.contrib.auth import get_user_model
from django.contrib.auth.backends import ModelBackend
from django.contrib.auth.context_processors import auth as _auth
from django.contrib.auth.models import AnonymousUser as DjangoAnonymousUser
from django.contrib.auth.models import Permission
from django.db.models import Q
from django.utils.functional import SimpleLazyObject
from rest_framework.authentication import BaseAuthentication
from openslides.core.config import config
from ..core.config import config
class AnonymousUser(DjangoAnonymousUser):
"""
Class for anonymous user instances, which have the permissions from the
Group 'Anonymous' (pk=1).
"""
def get_all_permissions(self, obj=None):
"""
Return the permissions a user is granted by his group membership(s).
Try to return the permissions for the 'Anonymous' group (pk=1).
"""
perms = Permission.objects.filter(group__pk=1)
if perms is None:
return set()
# TODO: test without order_by()
perms = perms.values_list('content_type__app_label', 'codename').order_by()
return set(['%s.%s' % (content_type, codename) for content_type, codename in perms])
def has_perm(self, perm, obj=None):
"""
Check if the user has a specific permission
"""
return (perm in self.get_all_permissions())
def has_module_perms(self, app_label):
"""
Check if the user has permissions on the module app_label
"""
for perm in self.get_all_permissions():
if perm[:perm.index('.')] == app_label:
return True
return False
# Registered users
class CustomizedModelBackend(ModelBackend):
"""
Customized backend for authentication. Ensures that registered users have
all permission of the group 'Registered' (pk=2).
Customized backend for authentication. Ensures that registered users
have all permissions of the group 'Registered' (pk=2). See
AUTHENTICATION_BACKENDS settings.
"""
def get_group_permissions(self, user_obj, obj=None):
"""
Returns a set of permission strings that this user has through his/her
groups.
"""
# TODO: Refactor this after Django 1.8 release. Add also anonymous
# permission check to this backend.
# TODO: Refactor this after Django 1.8 is minimum requirement. Add
# also anonymous permission check to this backend.
if user_obj.is_anonymous() or obj is not None:
return set()
if not hasattr(user_obj, '_group_perm_cache'):
@ -74,11 +41,61 @@ class CustomizedModelBackend(ModelBackend):
return user_obj._group_perm_cache
class AuthenticationMiddleware(object):
# Anonymous users
class AnonymousUser(DjangoAnonymousUser):
"""
Class for anonymous user instances which have the permissions from the
group 'Anonymous' (pk=1).
"""
def get_all_permissions(self, obj=None):
"""
Returns the permissions a user is granted by his group membership(s).
Try to return the permissions for the 'Anonymous' group (pk=1).
"""
perms = Permission.objects.filter(group__pk=1)
if perms is None:
return set()
# TODO: Test without order_by()
perms = perms.values_list('content_type__app_label', 'codename').order_by()
return set(['%s.%s' % (content_type, codename) for content_type, codename in perms])
def has_perm(self, perm, obj=None):
"""
Checks if the user has a specific permission.
"""
return (perm in self.get_all_permissions())
def has_module_perms(self, app_label):
"""
Checks if the user has permissions on the module app_label.
"""
for perm in self.get_all_permissions():
if perm[:perm.index('.')] == app_label:
return True
return False
class RESTFrameworkAnonymousAuthentication(BaseAuthentication):
"""
Authentication class for the Django REST framework.
Sets the user to the our AnonymousUser but only if
general_system_enable_anonymous is set to True in the config.
"""
def authenticate(self, request):
if config['general_system_enable_anonymous']:
return (AnonymousUser(), None)
return None
class AuthenticationMiddleware:
"""
Middleware to get the logged in user in users.
Uses AnonymousUser instead of the django anonymous user.
Uses AnonymousUser instead of Django's anonymous user.
"""
def process_request(self, request):
"""
@ -94,20 +111,6 @@ class AuthenticationMiddleware(object):
request.user = SimpleLazyObject(lambda: get_user(request))
class AnonymousAuthentication(BaseAuthentication):
"""
Authentication class for the Django REST framework.
Sets the user to the our AnonymousUser but only if
general_system_enable_anonymous is set to True in the config.
"""
def authenticate(self, request):
if config['general_system_enable_anonymous']:
return (AnonymousUser(), None)
return None
def get_user(request):
"""
Gets the user from the request.
@ -124,22 +127,3 @@ def get_user(request):
return_user = AnonymousUser()
request._cached_user = return_user
return return_user
def auth(request):
"""
Contextmanager to handle auth.
Uses the django auth context manager to fill the context.
Alters the attribute user if the user is not authenticated.
"""
# Call the django standard auth function, like 'super()'
context = _auth(request)
# Change the django anonymous user with our anonymous user if anonymous auth
# is enabled
if config['general_system_enable_anonymous'] and isinstance(context['user'], DjangoAnonymousUser):
context['user'] = AnonymousUser()
return context

View File

@ -1,5 +1,5 @@
from openslides.utils.exceptions import OpenSlidesError
from ..utils.exceptions import OpenSlidesError
class UserError(OpenSlidesError):
class UsersError(OpenSlidesError):
pass

View File

@ -1,6 +1,6 @@
from django.core.management.base import NoArgsCommand
from openslides.users.models import User
from ...models import User
class Command(NoArgsCommand):

View File

@ -1,27 +1,29 @@
from random import choice
from django.contrib.auth.hashers import make_password
from django.contrib.auth.models import ( # noqa
from django.contrib.auth.models import (
AbstractBaseUser,
BaseUserManager,
Group,
Permission,
PermissionsMixin,
)
from django.db import models
from django.utils.translation import ugettext_lazy, ugettext_noop
from openslides.core.config import config
from openslides.utils.models import RESTModelMixin
from .exceptions import UserError
from ..core.config import config
from ..utils.models import RESTModelMixin
from .exceptions import UsersError
class UserManager(BaseUserManager):
"""
UserManager that creates new users only with a password and a username.
Customized manager that creates new users only with a password and a
username.
"""
def create_user(self, username, password, **kwargs):
"""
Creates a new user only with a password and a username.
"""
user = self.model(username=username, **kwargs)
user.set_password(password)
user.save(using=self._db)
@ -29,15 +31,15 @@ class UserManager(BaseUserManager):
def create_or_reset_admin_user(self):
"""
Creates an user with the username admin. If such a user exists, resets
it. The password is (re)set to 'admin'. The user becomes member of the
group 'Staff' (pk=4).
Creates an user with the username 'admin'. If such a user already
exists, resets it. The password is (re)set to 'admin'. The user
becomes member of the group 'Staff' (pk=4).
"""
try:
staff = Group.objects.get(pk=4)
except Group.DoesNotExist:
raise UserError("Admin user can not be created or reset because "
"the group 'Staff' is not available.")
raise UsersError("Admin user can not be created or reset because "
"the group 'Staff' (pk=4) is not available.")
admin, created = self.get_or_create(
username='admin',
defaults={'last_name': 'Administrator'})
@ -60,7 +62,7 @@ class UserManager(BaseUserManager):
base_name = first_name or last_name
if not base_name:
raise ValueError("Either 'first_name' or 'last_name' must not be "
"empty")
"empty.")
if not self.filter(username=base_name).exists():
generated_username = base_name
@ -77,7 +79,7 @@ class UserManager(BaseUserManager):
def generate_password(self):
"""
Generates a random passwort.
Generates a random passwort. Do not use l, o, I, O, 1 or 0.
"""
chars = 'abcdefghijkmnpqrstuvwxyzABCDEFGHJKLMNPQRSTUVWXYZ23456789'
size = 8
@ -86,56 +88,75 @@ class UserManager(BaseUserManager):
class User(RESTModelMixin, PermissionsMixin, AbstractBaseUser):
"""
Model for users in OpenSlides. A client can login as a user with
credentials. A user can also just be used as representation for a person
in other OpenSlides app like motion submitter or (assignment) election
Model for users in OpenSlides. A client can login as an user with
credentials. An user can also just be used as representation for a person
in other OpenSlides apps like motion submitter or (assignment) election
candidates.
"""
USERNAME_FIELD = 'username'
slide_callback_name = 'user'
username = models.CharField(
ugettext_lazy('Username'), max_length=255, unique=True, blank=True)
ugettext_lazy('Username'),
max_length=255,
unique=True,
blank=True)
first_name = models.CharField(
ugettext_lazy('First name'), max_length=255, blank=True)
ugettext_lazy('First name'),
max_length=255,
blank=True)
last_name = models.CharField(
ugettext_lazy('Last name'), max_length=255, blank=True)
ugettext_lazy('Last name'),
max_length=255,
blank=True)
# TODO: Try to remove the default argument in the following fields.
# TODO: try to remove the default argument in the following fields
structure_level = models.CharField(
max_length=255, blank=True, default='',
verbose_name=ugettext_lazy('Structure level'),
ugettext_lazy('Structure level'),
max_length=255,
blank=True,
default='',
help_text=ugettext_lazy('Will be shown after the name.'))
title = models.CharField(
max_length=50, blank=True, default='',
verbose_name=ugettext_lazy('Title'),
ugettext_lazy('Title'),
max_length=50,
blank=True,
default='',
help_text=ugettext_lazy('Will be shown before the name.'))
about_me = models.TextField(
blank=True, default='', verbose_name=ugettext_lazy('About me'),
help_text=ugettext_lazy('Your profile text'))
ugettext_lazy('About me'),
blank=True,
default='',
help_text=ugettext_lazy('Profile text.'))
comment = models.TextField(
blank=True, default='', verbose_name=ugettext_lazy('Comment'),
ugettext_lazy('Comment'),
blank=True,
default='',
help_text=ugettext_lazy('Only for notes.'))
default_password = models.CharField(
max_length=100, blank=True, default='',
verbose_name=ugettext_lazy('Default password'))
ugettext_lazy('Default password'),
max_length=100,
blank=True,
default='')
is_active = models.BooleanField(
ugettext_lazy('active'), default=True,
ugettext_lazy('Active'),
default=True,
help_text=ugettext_lazy(
'Designates whether this user should be treated as '
'active. Unselect this instead of deleting accounts.'))
'active. Unselect this instead of deleting the account.'))
is_present = models.BooleanField(
ugettext_lazy('present'), default=False,
help_text=ugettext_lazy('Designates whether this user is in the room '
'or not.'))
ugettext_lazy('Present'),
default=False,
help_text=ugettext_lazy(
'Designates whether this user is in the room or not.'))
objects = UserManager()
@ -145,19 +166,11 @@ class User(RESTModelMixin, PermissionsMixin, AbstractBaseUser):
('can_see_extra_data', ugettext_noop('Can see extra data of users')),
('can_manage', ugettext_noop('Can manage users')),
)
ordering = ('last_name',)
ordering = ('last_name', 'first_name', 'username', )
def __str__(self):
return self.get_full_name()
def get_slide_context(self, **context):
"""
Returns the context for the user slide.
"""
# Does not call super. In this case the context would override the name
# 'user'.
return {'shown_user': self}
def get_full_name(self):
"""
Returns a long form of the name.
@ -166,7 +179,6 @@ class User(RESTModelMixin, PermissionsMixin, AbstractBaseUser):
* Professor Dr. Enders, Christoph (Leipzig)
"""
structure = '(%s)' % self.structure_level if self.structure_level else ''
return ' '.join((self.title, self.get_short_name(), structure)).strip()
def get_short_name(self):
@ -190,15 +202,9 @@ class User(RESTModelMixin, PermissionsMixin, AbstractBaseUser):
# The user has only a first_name or a last_name or no name
else:
name = first_name or last_name or self.username
return name
def reset_password(self, password=None):
"""
Reset the password for the user to his default-password.
"""
if password is None:
password = self.default_password
self.set_password(password)
# Return result
return name
def get_view_class(self):
"""

View File

@ -14,9 +14,8 @@ from reportlab.platypus import (
TableStyle,
)
from openslides.core.config import config
from openslides.utils.pdf import stylesheet
from ..core.config import config
from ..utils.pdf import stylesheet
from .models import User

View File

@ -1,8 +1,7 @@
from django.utils.translation import ugettext as _
from openslides.core.exceptions import ProjectorException
from openslides.utils.projector import ProjectorElement, ProjectorRequirement
from ..core.exceptions import ProjectorException
from ..utils.projector import ProjectorElement, ProjectorRequirement
from .models import User
from .views import GroupViewSet, UserViewSet

View File

@ -6,8 +6,8 @@ from .models import User
class Index(indexes.SearchIndex, indexes.Indexable):
text = indexes.EdgeNgramField(document=True, use_template=True)
text = indexes.EdgeNgramField(document=True, use_template=True)
modelfilter_name = "Users" # verbose_name of model
modelfilter_value = "users.user" # 'app_name.model_name'
modelfilter_name = 'Users' # verbose_name of model
modelfilter_value = 'users.user' # 'app_name.model_name'
def get_model(self):
return User

View File

@ -1,15 +1,15 @@
from django.contrib.auth.hashers import make_password
from django.contrib.auth.models import Permission
from django.utils.translation import ugettext as _
from django.utils.translation import ugettext_lazy
from openslides.utils.rest_api import (
from ..utils.rest_api import (
ModelSerializer,
PrimaryKeyRelatedField,
RelatedField,
ValidationError,
)
from .models import Group, Permission, User
from .models import Group, User
class UserShortSerializer(ModelSerializer):
@ -28,7 +28,8 @@ class UserShortSerializer(ModelSerializer):
'last_name',
'structure_level',
'about_me',
'groups',)
'groups',
)
class UserFullSerializer(ModelSerializer):
@ -58,7 +59,8 @@ class UserFullSerializer(ModelSerializer):
'comment',
'groups',
'default_password',
'is_active',)
'is_active',
)
def validate(self, data):
"""
@ -70,7 +72,7 @@ class UserFullSerializer(ModelSerializer):
raise ValidationError(_('Username, first name and last name can not all be empty.'))
# Generate username. But only if it is not set and the serializer is not
# called in a patch-context.
# called in a PATCH context (partial_update).
try:
action = self.context['view'].action
except (KeyError, AttributeError):
@ -84,7 +86,7 @@ class UserFullSerializer(ModelSerializer):
def create(self, validated_data):
"""
Creates the user. Sets the default_password. Adds the new user to the
Creates the user. Sets the default password. Adds the new user to the
registered group.
"""
# Prepare setup password.
@ -139,4 +141,5 @@ class GroupSerializer(ModelSerializer):
fields = (
'id',
'name',
'permissions',)
'permissions',
)

View File

@ -1,10 +1,10 @@
from django.contrib.auth.models import Permission
from django.db.models import Q
from django.utils.translation import ugettext as _
from django.utils.translation import ugettext_lazy, ugettext_noop
from openslides.core.config import ConfigVariable
from .models import Group, Permission, User
from ..core.config import ConfigVariable
from .models import Group, User
def setup_users_config(sender, **kwargs):
@ -126,9 +126,10 @@ def create_builtin_groups_and_admin(**kwargs):
'users.can_manage',
'users.can_see_extra_data',
'users.can_see_name', )
permission_dict = {}
permission_query = Q()
permission_dict = {}
# Load all permissions
for permission_string in permission_strings:
app_label, codename = permission_string.split('.')
query_part = Q(content_type__app_label=app_label) & Q(codename=codename)
@ -183,11 +184,12 @@ def create_builtin_groups_and_admin(**kwargs):
group_staff = Group.objects.create(name=ugettext_noop('Staff'), pk=4)
group_staff.permissions.add(*staff_permissions)
# Add users.can_see_name and users.can_see_extra_data permissions
# Add users.can_see_name and users.can_see_extra_data permissions to staff
# group to ensure proper management possibilities
# TODO: Remove this redundancy after cleanup of the permission system.
group_staff.permissions.add(
permission_dict['users.can_see_extra_data'],
permission_dict['users.can_see_name'])
# Admin user
# Create or reset admin user
User.objects.create_or_reset_admin_user()

View File

@ -5,16 +5,7 @@ from . import views
urlpatterns = patterns(
'',
# PDF
url(r'^print/$',
views.UsersListPDF.as_view(),
name='user_listpdf'),
url(r'^passwords/print/$',
views.UsersPasswordsPDF.as_view(),
name='user_passwordspdf'),
# auth
# Auth
url(r'^login/$',
views.UserLoginView.as_view(),
name='user_login'),
@ -26,4 +17,13 @@ urlpatterns = patterns(
url(r'^whoami/$',
views.WhoAmIView.as_view(),
name='user_whoami'),
# PDF
url(r'^print/$',
views.UsersListPDF.as_view(),
name='user_listpdf'),
url(r'^passwords/print/$',
views.UsersPasswordsPDF.as_view(),
name='user_passwordspdf'),
)

View File

@ -3,12 +3,10 @@ from django.contrib.auth import logout as auth_logout
from django.contrib.auth.forms import AuthenticationForm
from django.utils.translation import ugettext as _
from django.utils.translation import ugettext_lazy
from rest_framework import status
from openslides.core.config import config
from openslides.utils.rest_api import ModelViewSet, Response, detail_route
from openslides.utils.views import APIView, PDFView
from ..core.config import config
from ..utils.rest_api import ModelViewSet, Response, detail_route, status
from ..utils.views import APIView, PDFView
from .models import Group, User
from .pdf import users_passwords_to_pdf, users_to_pdf
from .serializers import (
@ -185,7 +183,7 @@ class GroupViewSet(ModelViewSet):
class UserLoginView(APIView):
"""
Login the user via Ajax.
Login the user.
"""
http_method_names = ['post']
@ -208,7 +206,7 @@ class UserLoginView(APIView):
class UserLogoutView(APIView):
"""
Logout the user via Ajax.
Logout the user.
"""
http_method_names = ['post']
@ -225,7 +223,7 @@ class WhoAmIView(APIView):
def get_context_data(self, **context):
"""
Appends the user id into the context.
Appends the user id to the context.
Uses None for the anonymous user.
"""
@ -238,11 +236,11 @@ class WhoAmIView(APIView):
class UsersListPDF(PDFView):
"""
Generate the userliste as PDF.
Generate a list of all users as PDF.
"""
required_permission = 'users.can_see_extra_data'
filename = ugettext_lazy("user-list")
document_title = ugettext_lazy('List of Users')
filename = ugettext_lazy('user-list')
document_title = ugettext_lazy('List of users')
def append_to_pdf(self, pdf):
"""
@ -256,7 +254,7 @@ class UsersPasswordsPDF(PDFView):
Generate the access data welcome paper for all users as PDF.
"""
required_permission = 'users.can_manage'
filename = ugettext_lazy("User-access-data")
filename = ugettext_lazy('user-access-data')
top_space = 0
def build_document(self, pdf_document, story):

View File

@ -2,6 +2,7 @@ import re
from collections import OrderedDict
from urllib.parse import urlparse
from rest_framework import status # noqa
from rest_framework.decorators import detail_route, list_route # noqa
from rest_framework.metadata import SimpleMetadata # noqa
from rest_framework.mixins import DestroyModelMixin, UpdateModelMixin # noqa

View File

@ -1,25 +1,10 @@
import roman
from django.contrib.auth.models import Permission
def delete_default_permissions(**kwargs):
"""
Deletes the permissions, django creates by default for the admin.
"""
# TODO: Find a way not to create the permissions in the first place.
# Meta.default_permissions does not work, because django will
# nevertheless create permissions for its own models like "group"
for p in Permission.objects.all():
if (p.codename.startswith('add') or
p.codename.startswith('delete') or
p.codename.startswith('change')):
p.delete()
def to_roman(number):
"""
Converts an arabic number within range from 1 to 4999 to the corresponding roman number.
Returns None on error conditions.
Converts an arabic number within range from 1 to 4999 to the
corresponding roman number. Returns None on error conditions.
"""
try:
return roman.toRoman(number)

View File

@ -1,19 +0,0 @@
from unittest import TestCase
from unittest.mock import patch
from openslides.users.api import get_protected_perm
@patch('openslides.users.api.Permission')
class GetProtectedPerm(TestCase):
def test_normal(self, mock_permission):
mock_permission.objects.get_by_natural_key.return_value = 'test_permission'
value = get_protected_perm()
mock_permission.objects.get_by_natural_key.assert_called_once_with(
app_label='users', model='user', codename='can_manage')
self.assertEqual(
value,
'test_permission',
"The function should return the user.can_manage permission")

View File

@ -1,7 +1,7 @@
from unittest import TestCase
from unittest.mock import MagicMock, patch
from openslides.users.auth import AnonymousUser, auth, get_user
from openslides.users.auth import AnonymousUser, get_user
class TestAnonymousUser(TestCase):
@ -98,40 +98,3 @@ class TestGetUser(TestCase):
request._cached_user,
'django_anonymous_user',
"The django user should be cached")
@patch('openslides.users.auth.config')
@patch('openslides.users.auth._auth')
class TestAuth(TestCase):
def test_anonymous_enabled(self, mock_auth, mock_config):
mock_config.__getitem__.return_value = True
request = MagicMock()
mock_auth.return_value = {'user': AnonymousUser()}
context = auth(request)
self.assertEqual(
context,
{'user': AnonymousUser()})
def test_anonymous_disabled(self, mock_auth, mock_config):
mock_config.__getitem__.return_value = False
request = MagicMock()
mock_auth.return_value = {'user': AnonymousUser()}
context = auth(request)
self.assertEqual(
context,
{'user': AnonymousUser()})
def test_logged_in_user_in_request(self, mock_auth, mock_config):
mock_config.__getitem__.return_value = True
request = MagicMock()
mock_auth.return_value = {'user': 'logged_in_user'}
context = auth(request)
self.assertEqual(
context,
{'user': 'logged_in_user'})

View File

@ -18,19 +18,6 @@ class UserTest(TestCase):
'Test Value IJee1yoet1ooGhesh5li',
"The str representation of User is not user.get_full_name().")
def test_get_slide_context(self):
"""
Tests, that get_slide_context returns:
{'shown_user': self}
"""
user = User()
self.assertEqual(
user.get_slide_context(),
{'shown_user': user},
"User.get_slide_context returns a wrong context.")
class UserGetFullName(TestCase):
def test_get_full_name_with_structure_level_and_title(self):
@ -185,30 +172,6 @@ class UserGetShortName(TestCase):
"User.get_short_name() has to strip whitespaces from the name parts.")
class UserResetPassword(TestCase):
def test_reset_password_no_attribute(self):
"""
Tests reset_password with no attribute.
"""
user = User(default_password='test_default_password')
user.set_password = MagicMock()
user.reset_password()
user.set_password.assert_called_once_with('test_default_password')
def test_reset_password_with_attribute(self):
"""
Tests reset_password with no attribute.
"""
user = User(default_password='test_default_password')
user.set_password = MagicMock()
user.reset_password('test_password')
user.set_password.assert_called_once_with('test_password')
class UserManagerTest(TestCase):
def test_create_user(self):
"""