Remove utils.collections.Collection class and other cleanups

* Activate restricted_data_cache on inmemory cache
* Use ElementCache in rest-api get requests
* Get requests on the restapi return 404 when the user has no permission
* Added async function for has_perm and in_some_groups
* changed Cachable.get_restricted_data to be an ansync function
* rewrote required_user_system
* changed default implementation of access_permission.check_permission to
  check a given permission or check if anonymous is enabled
This commit is contained in:
Oskar Hahn 2018-11-01 17:30:18 +01:00
parent f48410024e
commit cd34d30866
44 changed files with 418 additions and 590 deletions

View File

@ -1,7 +1,7 @@
from typing import Any, Dict, Iterable, List, Optional from typing import Any, Dict, Iterable, List, Optional
from ..utils.access_permissions import BaseAccessPermissions from ..utils.access_permissions import BaseAccessPermissions
from ..utils.auth import has_perm from ..utils.auth import async_has_perm
from ..utils.collection import CollectionElement from ..utils.collection import CollectionElement
@ -9,11 +9,7 @@ class ItemAccessPermissions(BaseAccessPermissions):
""" """
Access permissions container for Item and ItemViewSet. Access permissions container for Item and ItemViewSet.
""" """
def check_permissions(self, user): base_permission = 'agenda.can_see'
"""
Returns True if the user has read access model instances.
"""
return has_perm(user, 'agenda.can_see')
def get_serializer_class(self, user=None): def get_serializer_class(self, user=None):
""" """
@ -26,7 +22,7 @@ class ItemAccessPermissions(BaseAccessPermissions):
# TODO: In the following method we use full_data['is_hidden'] and # TODO: In the following method we use full_data['is_hidden'] and
# full_data['is_internal'] but this can be out of date. # full_data['is_internal'] but this can be out of date.
def get_restricted_data( async def get_restricted_data(
self, self,
full_data: List[Dict[str, Any]], full_data: List[Dict[str, Any]],
user: Optional[CollectionElement]) -> List[Dict[str, Any]]: user: Optional[CollectionElement]) -> List[Dict[str, Any]]:
@ -47,11 +43,11 @@ class ItemAccessPermissions(BaseAccessPermissions):
return {key: full_data[key] for key in whitelist} return {key: full_data[key] for key in whitelist}
# Parse data. # Parse data.
if full_data and has_perm(user, 'agenda.can_see'): if full_data and await async_has_perm(user, 'agenda.can_see'):
if has_perm(user, 'agenda.can_manage') and has_perm(user, 'agenda.can_see_internal_items'): if await async_has_perm(user, 'agenda.can_manage') and await async_has_perm(user, 'agenda.can_see_internal_items'):
# Managers with special permission can see everything. # Managers with special permission can see everything.
data = full_data data = full_data
elif has_perm(user, 'agenda.can_see_internal_items'): elif await async_has_perm(user, 'agenda.can_see_internal_items'):
# Non managers with special permission can see everything but # Non managers with special permission can see everything but
# comments and hidden items. # comments and hidden items.
data = [full for full in full_data if not full['is_hidden']] # filter hidden items data = [full for full in full_data if not full['is_hidden']] # filter hidden items
@ -72,7 +68,7 @@ class ItemAccessPermissions(BaseAccessPermissions):
# In non internal case managers see everything and non managers see # In non internal case managers see everything and non managers see
# everything but comments. # everything but comments.
if has_perm(user, 'agenda.can_manage'): if await async_has_perm(user, 'agenda.can_manage'):
blocked_keys_non_internal_hidden_case: Iterable[str] = [] blocked_keys_non_internal_hidden_case: Iterable[str] = []
can_see_hidden = True can_see_hidden = True
else: else:

View File

@ -1,3 +1,5 @@
from typing import Any, Dict, Set
from django.apps import AppConfig from django.apps import AppConfig
from ..utils.projector import register_projector_elements from ..utils.projector import register_projector_elements
@ -12,15 +14,15 @@ class AgendaAppConfig(AppConfig):
def ready(self): def ready(self):
# Import all required stuff. # Import all required stuff.
from django.db.models.signals import pre_delete, post_save from django.db.models.signals import pre_delete, post_save
from ..core.signals import permission_change, user_data_required from ..core.signals import permission_change
from ..utils.rest_api import router from ..utils.rest_api import router
from .projector import get_projector_elements from .projector import get_projector_elements
from .signals import ( from .signals import (
get_permission_change_data, get_permission_change_data,
listen_to_related_object_post_delete, listen_to_related_object_post_delete,
listen_to_related_object_post_save, listen_to_related_object_post_save)
required_users)
from .views import ItemViewSet from .views import ItemViewSet
from ..utils.access_permissions import required_user
# Define projector elements. # Define projector elements.
register_projector_elements(get_projector_elements()) register_projector_elements(get_projector_elements())
@ -35,13 +37,13 @@ class AgendaAppConfig(AppConfig):
permission_change.connect( permission_change.connect(
get_permission_change_data, get_permission_change_data,
dispatch_uid='agenda_get_permission_change_data') dispatch_uid='agenda_get_permission_change_data')
user_data_required.connect(
required_users,
dispatch_uid='agenda_required_users')
# Register viewsets. # Register viewsets.
router.register(self.get_model('Item').get_collection_string(), ItemViewSet) router.register(self.get_model('Item').get_collection_string(), ItemViewSet)
# register required_users
required_user.add_collection_string(self.get_model('Item').get_collection_string(), required_users)
def get_config_variables(self): def get_config_variables(self):
from .config_variables import get_config_variables from .config_variables import get_config_variables
return get_config_variables() return get_config_variables()
@ -52,3 +54,10 @@ class AgendaAppConfig(AppConfig):
connection. connection.
""" """
yield self.get_model('Item') yield self.get_model('Item')
def required_users(element: Dict[str, Any]) -> Set[int]:
"""
Returns all user ids that are displayed as speaker in the given element.
"""
return set(speaker['user_id'] for speaker in element['speakers'])

View File

@ -182,6 +182,7 @@ class Item(RESTModelMixin, models.Model):
""" """
access_permissions = ItemAccessPermissions() access_permissions = ItemAccessPermissions()
objects = ItemManager() objects = ItemManager()
can_see_permission = 'agenda.can_see'
AGENDA_ITEM = 1 AGENDA_ITEM = 1
INTERNAL_ITEM = 2 INTERNAL_ITEM = 2

View File

@ -1,11 +1,7 @@
from typing import Set
from django.apps import apps from django.apps import apps
from django.contrib.contenttypes.models import ContentType from django.contrib.contenttypes.models import ContentType
from ..utils.auth import has_perm
from ..utils.autoupdate import inform_changed_data from ..utils.autoupdate import inform_changed_data
from ..utils.collection import Collection
from .models import Item from .models import Item
@ -64,17 +60,3 @@ def get_permission_change_data(sender, permissions, **kwargs):
and permission.codename in ('can_see', 'can_see_internal_items')): and permission.codename in ('can_see', 'can_see_internal_items')):
yield from agenda_app.get_startup_elements() yield from agenda_app.get_startup_elements()
break break
def required_users(sender, request_user, **kwargs):
"""
Returns all user ids that are displayed as speakers in any agenda item
if request_user can see the agenda. This function may return an empty
set.
"""
speakers: Set[int] = set()
if has_perm(request_user, 'agenda.can_see'):
for item_collection_element in Collection(Item.get_collection_string()).element_generator():
full_data = item_collection_element.get_full_data()
speakers.update(speaker['user_id'] for speaker in full_data['speakers'])
return speakers

View File

@ -50,7 +50,7 @@ class ItemViewSet(ListModelMixin, RetrieveModelMixin, UpdateModelMixin, GenericV
elif self.action in ('speak', 'sort_speakers'): elif self.action in ('speak', 'sort_speakers'):
result = (has_perm(self.request.user, 'agenda.can_see') and result = (has_perm(self.request.user, 'agenda.can_see') and
has_perm(self.request.user, 'agenda.can_manage_list_of_speakers')) has_perm(self.request.user, 'agenda.can_manage_list_of_speakers'))
elif self.action in ('numbering'): elif self.action in ('numbering', ):
result = (has_perm(self.request.user, 'agenda.can_see') and result = (has_perm(self.request.user, 'agenda.can_see') and
has_perm(self.request.user, 'agenda.can_manage')) has_perm(self.request.user, 'agenda.can_manage'))
else: else:

View File

@ -1,7 +1,7 @@
from typing import Any, Dict, List, Optional from typing import Any, Dict, List, Optional
from ..utils.access_permissions import BaseAccessPermissions from ..utils.access_permissions import BaseAccessPermissions
from ..utils.auth import has_perm from ..utils.auth import async_has_perm, has_perm
from ..utils.collection import CollectionElement from ..utils.collection import CollectionElement
@ -9,11 +9,7 @@ class AssignmentAccessPermissions(BaseAccessPermissions):
""" """
Access permissions container for Assignment and AssignmentViewSet. Access permissions container for Assignment and AssignmentViewSet.
""" """
def check_permissions(self, user): base_permission = 'assignments.can_see'
"""
Returns True if the user has read access model instances.
"""
return has_perm(user, 'assignments.can_see')
def get_serializer_class(self, user=None): def get_serializer_class(self, user=None):
""" """
@ -27,7 +23,7 @@ class AssignmentAccessPermissions(BaseAccessPermissions):
serializer_class = AssignmentShortSerializer serializer_class = AssignmentShortSerializer
return serializer_class return serializer_class
def get_restricted_data( async def get_restricted_data(
self, self,
full_data: List[Dict[str, Any]], full_data: List[Dict[str, Any]],
user: Optional[CollectionElement]) -> List[Dict[str, Any]]: user: Optional[CollectionElement]) -> List[Dict[str, Any]]:
@ -37,9 +33,9 @@ class AssignmentAccessPermissions(BaseAccessPermissions):
only get a result like the AssignmentShortSerializer would give them. only get a result like the AssignmentShortSerializer would give them.
""" """
# Parse data. # Parse data.
if has_perm(user, 'assignments.can_see') and has_perm(user, 'assignments.can_manage'): if await async_has_perm(user, 'assignments.can_see') and await async_has_perm(user, 'assignments.can_manage'):
data = full_data data = full_data
elif has_perm(user, 'assignments.can_see'): elif await async_has_perm(user, 'assignments.can_see'):
# Exclude unpublished poll votes. # Exclude unpublished poll votes.
data = [] data = []
for full in full_data: for full in full_data:

View File

@ -1,4 +1,4 @@
from typing import List from typing import Any, Dict, List, Set
from django.apps import AppConfig from django.apps import AppConfig
from mypy_extensions import TypedDict from mypy_extensions import TypedDict
@ -14,11 +14,12 @@ class AssignmentsAppConfig(AppConfig):
def ready(self): def ready(self):
# Import all required stuff. # Import all required stuff.
from ..core.signals import permission_change, user_data_required from ..core.signals import permission_change
from ..utils.rest_api import router from ..utils.rest_api import router
from .projector import get_projector_elements from .projector import get_projector_elements
from .signals import get_permission_change_data, required_users from .signals import get_permission_change_data
from .views import AssignmentViewSet, AssignmentPollViewSet from .views import AssignmentViewSet, AssignmentPollViewSet
from ..utils.access_permissions import required_user
# Define projector elements. # Define projector elements.
register_projector_elements(get_projector_elements()) register_projector_elements(get_projector_elements())
@ -27,14 +28,14 @@ class AssignmentsAppConfig(AppConfig):
permission_change.connect( permission_change.connect(
get_permission_change_data, get_permission_change_data,
dispatch_uid='assignments_get_permission_change_data') dispatch_uid='assignments_get_permission_change_data')
user_data_required.connect(
required_users,
dispatch_uid='assignments_required_users')
# Register viewsets. # Register viewsets.
router.register(self.get_model('Assignment').get_collection_string(), AssignmentViewSet) router.register(self.get_model('Assignment').get_collection_string(), AssignmentViewSet)
router.register('assignments/poll', AssignmentPollViewSet) router.register('assignments/poll', AssignmentPollViewSet)
# Register required_users
required_user.add_collection_string(self.get_model('Assignment').get_collection_string(), required_users)
def get_config_variables(self): def get_config_variables(self):
from .config_variables import get_config_variables from .config_variables import get_config_variables
return get_config_variables() return get_config_variables()
@ -56,3 +57,14 @@ class AssignmentsAppConfig(AppConfig):
'display_name': phase[1], 'display_name': phase[1],
}) })
return {'AssignmentPhases': phases} return {'AssignmentPhases': phases}
def required_users(element: Dict[str, Any]) -> Set[int]:
"""
Returns all user ids that are displayed as candidates (including poll
options) in the assignment element.
"""
candidates = set(related_user['user_id'] for related_user in element['assignment_related_users'])
for poll in element['polls']:
candidates.update(option['candidate_id'] for option in poll['options'])
return candidates

View File

@ -91,6 +91,7 @@ class Assignment(RESTModelMixin, models.Model):
Model for assignments. Model for assignments.
""" """
access_permissions = AssignmentAccessPermissions() access_permissions = AssignmentAccessPermissions()
can_see_permission = 'assignments.can_see'
objects = AssignmentManager() objects = AssignmentManager()

View File

@ -1,11 +1,5 @@
from typing import Any, Set
from django.apps import apps from django.apps import apps
from ..utils.auth import has_perm
from ..utils.collection import Collection
from .models import Assignment
def get_permission_change_data(sender, permissions=None, **kwargs): def get_permission_change_data(sender, permissions=None, **kwargs):
""" """
@ -16,19 +10,3 @@ def get_permission_change_data(sender, permissions=None, **kwargs):
# There could be only one 'assignment.can_see' and then we want to return data. # There could be only one 'assignment.can_see' and then we want to return data.
if permission.content_type.app_label == assignments_app.label and permission.codename == 'can_see': if permission.content_type.app_label == assignments_app.label and permission.codename == 'can_see':
yield from assignments_app.get_startup_elements() yield from assignments_app.get_startup_elements()
def required_users(sender, request_user, **kwargs):
"""
Returns all user ids that are displayed as candidates (including poll
options) in any assignment if request_user can see assignments. This
function may return an empty set.
"""
candidates: Set[Any] = set()
if has_perm(request_user, 'assignments.can_see'):
for assignment_collection_element in Collection(Assignment.get_collection_string()).element_generator():
full_data = assignment_collection_element.get_full_data()
candidates.update(related_user['user_id'] for related_user in full_data['assignment_related_users'])
for poll in full_data['polls']:
candidates.update(option['candidate_id'] for option in poll['options'])
return candidates

View File

@ -1,18 +1,11 @@
from django.contrib.auth.models import AnonymousUser
from ..utils.access_permissions import BaseAccessPermissions from ..utils.access_permissions import BaseAccessPermissions
from ..utils.auth import anonymous_is_enabled, has_perm
class ProjectorAccessPermissions(BaseAccessPermissions): class ProjectorAccessPermissions(BaseAccessPermissions):
""" """
Access permissions container for Projector and ProjectorViewSet. Access permissions container for Projector and ProjectorViewSet.
""" """
def check_permissions(self, user): base_permission = 'core.can_see_projector'
"""
Returns True if the user has read access model instances.
"""
return has_perm(user, 'core.can_see_projector')
def get_serializer_class(self, user=None): def get_serializer_class(self, user=None):
""" """
@ -27,13 +20,6 @@ class TagAccessPermissions(BaseAccessPermissions):
""" """
Access permissions container for Tag and TagViewSet. Access permissions container for Tag and TagViewSet.
""" """
def check_permissions(self, user):
"""
Returns True if the user has read access model instances.
"""
# Every authenticated user can retrieve tags. Anonymous users can do
# so if they are enabled.
return not isinstance(user, AnonymousUser) or anonymous_is_enabled()
def get_serializer_class(self, user=None): def get_serializer_class(self, user=None):
""" """
@ -48,13 +34,7 @@ class ChatMessageAccessPermissions(BaseAccessPermissions):
""" """
Access permissions container for ChatMessage and ChatMessageViewSet. Access permissions container for ChatMessage and ChatMessageViewSet.
""" """
def check_permissions(self, user): base_permission = 'core.can_use_chat'
"""
Returns True if the user has read access model instances.
"""
# Anonymous users can see the chat if the anonymous group has the
# permission core.can_use_chat. But they can not use it. See views.py.
return has_perm(user, 'core.can_use_chat')
def get_serializer_class(self, user=None): def get_serializer_class(self, user=None):
""" """
@ -69,11 +49,7 @@ class ProjectorMessageAccessPermissions(BaseAccessPermissions):
""" """
Access permissions for ProjectorMessage. Access permissions for ProjectorMessage.
""" """
def check_permissions(self, user): base_permission = 'core.can_see_projector'
"""
Returns True if the user has read access model instances.
"""
return has_perm(user, 'core.can_see_projector')
def get_serializer_class(self, user=None): def get_serializer_class(self, user=None):
""" """
@ -88,11 +64,7 @@ class CountdownAccessPermissions(BaseAccessPermissions):
""" """
Access permissions for Countdown. Access permissions for Countdown.
""" """
def check_permissions(self, user): base_permission = 'core.can_see_projector'
"""
Returns True if the user has read access model instances.
"""
return has_perm(user, 'core.can_see_projector')
def get_serializer_class(self, user=None): def get_serializer_class(self, user=None):
""" """
@ -108,13 +80,6 @@ class ConfigAccessPermissions(BaseAccessPermissions):
Access permissions container for the config (ConfigStore and Access permissions container for the config (ConfigStore and
ConfigViewSet). ConfigViewSet).
""" """
def check_permissions(self, user):
"""
Returns True if the user has read access model instances.
"""
# Every authenticated user can see the metadata and list or retrieve
# the config. Anonymous users can do so if they are enabled.
return not isinstance(user, AnonymousUser) or anonymous_is_enabled()
def get_serializer_class(self, user=None): def get_serializer_class(self, user=None):
""" """

View File

@ -1,6 +1,6 @@
from collections import OrderedDict from collections import OrderedDict
from operator import attrgetter from operator import attrgetter
from typing import Any, Dict, List from typing import Any, Dict, List, Set
from django.apps import AppConfig from django.apps import AppConfig
from django.conf import settings from django.conf import settings
@ -28,8 +28,6 @@ class CoreAppConfig(AppConfig):
get_permission_change_data, get_permission_change_data,
permission_change, permission_change,
post_permission_creation, post_permission_creation,
required_users,
user_data_required,
) )
from .views import ( from .views import (
ChatMessageViewSet, ChatMessageViewSet,
@ -47,6 +45,7 @@ class CoreAppConfig(AppConfig):
AutoupdateWebsocketClientMessage, AutoupdateWebsocketClientMessage,
) )
from ..utils.websocket import register_client_message from ..utils.websocket import register_client_message
from ..utils.access_permissions import required_user
# Collect all config variables before getting the constants. # Collect all config variables before getting the constants.
config.collect_config_variables_from_apps() config.collect_config_variables_from_apps()
@ -68,9 +67,6 @@ class CoreAppConfig(AppConfig):
permission_change.connect( permission_change.connect(
get_permission_change_data, get_permission_change_data,
dispatch_uid='core_get_permission_change_data') dispatch_uid='core_get_permission_change_data')
user_data_required.connect(
required_users,
dispatch_uid='core_required_users')
post_migrate.connect(call_save_default_values, sender=self, dispatch_uid='core_save_config_default_values') post_migrate.connect(call_save_default_values, sender=self, dispatch_uid='core_save_config_default_values')
@ -95,6 +91,9 @@ class CoreAppConfig(AppConfig):
register_client_message(GetElementsWebsocketClientMessage()) register_client_message(GetElementsWebsocketClientMessage())
register_client_message(AutoupdateWebsocketClientMessage()) register_client_message(AutoupdateWebsocketClientMessage())
# register required_users
required_user.add_collection_string(self.get_model('ChatMessage').get_collection_string(), required_users)
def get_config_variables(self): def get_config_variables(self):
from .config_variables import get_config_variables from .config_variables import get_config_variables
return get_config_variables() return get_config_variables()
@ -154,3 +153,10 @@ class CoreAppConfig(AppConfig):
def call_save_default_values(**kwargs): def call_save_default_values(**kwargs):
from .config import config from .config import config
config.save_default_values() config.save_default_values()
def required_users(element: Dict[str, Any]) -> Set[int]:
"""
Returns all user ids that are displayed as chatters.
"""
return set(element['user_id'])

View File

@ -239,6 +239,7 @@ class ChatMessage(RESTModelMixin, models.Model):
At the moment we only have one global chat room for managers. At the moment we only have one global chat room for managers.
""" """
access_permissions = ChatMessageAccessPermissions() access_permissions = ChatMessageAccessPermissions()
can_see_permission = 'core.can_use_chat'
message = models.TextField() message = models.TextField()

View File

@ -4,10 +4,6 @@ from django.contrib.contenttypes.models import ContentType
from django.db.models import Q from django.db.models import Q
from django.dispatch import Signal from django.dispatch import Signal
from ..utils.auth import has_perm
from ..utils.collection import Collection
from .models import ChatMessage
# This signal is send when the migrate command is done. That means it is sent # This signal is send when the migrate command is done. That means it is sent
# after post_migrate sending and creating all Permission objects. Don't use it # after post_migrate sending and creating all Permission objects. Don't use it
@ -18,12 +14,6 @@ post_permission_creation = Signal()
# permission). Connected receivers may yield Collections. # permission). Connected receivers may yield Collections.
permission_change = Signal() permission_change = Signal()
# This signal is sent if someone wants to see basic user data. Connected
# receivers may answer a set of user ids that are required for the request
# user (this can be anything that is allowd as argument for
# utils.auth.has_perm()) e. g. as motion submitter or assignment candidate.
user_data_required = Signal(providing_args=['request_user'])
def delete_django_app_permissions(sender, **kwargs): def delete_django_app_permissions(sender, **kwargs):
""" """
@ -51,16 +41,3 @@ def get_permission_change_data(sender, permissions, **kwargs):
yield core_app.get_model('Countdown') yield core_app.get_model('Countdown')
elif permission.codename == 'can_use_chat': elif permission.codename == 'can_use_chat':
yield core_app.get_model('ChatMessage') yield core_app.get_model('ChatMessage')
def required_users(sender, request_user, **kwargs):
"""
Returns all user ids that are displayed as chatters if request_user can
use the chat. This function may return an empty set.
"""
chatters = set()
if has_perm(request_user, 'core.can_use_chat'):
for chat_message_collection_element in Collection(ChatMessage.get_collection_string()).element_generator():
full_data = chat_message_collection_element.get_full_data()
chatters.add(full_data['user_id'])
return chatters

View File

@ -45,7 +45,7 @@ class NotifyWebsocketClientMessage(BaseWebsocketClientMessage):
"type": "send_notify", "type": "send_notify",
"incomming": content, "incomming": content,
"senderReplyChannelName": consumer.channel_name, "senderReplyChannelName": consumer.channel_name,
"senderUserId": consumer.scope['user'].id or 0, "senderUserId": consumer.scope['user'].id if consumer.scope['user'] else 0,
}, },
) )

View File

@ -1,7 +1,7 @@
from typing import Any, Dict, List, Optional from typing import Any, Dict, List, Optional
from ..utils.access_permissions import BaseAccessPermissions from ..utils.access_permissions import BaseAccessPermissions
from ..utils.auth import has_perm from ..utils.auth import async_has_perm
from ..utils.collection import CollectionElement from ..utils.collection import CollectionElement
@ -9,11 +9,7 @@ class MediafileAccessPermissions(BaseAccessPermissions):
""" """
Access permissions container for Mediafile and MediafileViewSet. Access permissions container for Mediafile and MediafileViewSet.
""" """
def check_permissions(self, user): base_permission = 'mediafiles.can_see'
"""
Returns True if the user has read access model instances.
"""
return has_perm(user, 'mediafiles.can_see')
def get_serializer_class(self, user=None): def get_serializer_class(self, user=None):
""" """
@ -23,7 +19,7 @@ class MediafileAccessPermissions(BaseAccessPermissions):
return MediafileSerializer return MediafileSerializer
def get_restricted_data( async def get_restricted_data(
self, self,
full_data: List[Dict[str, Any]], full_data: List[Dict[str, Any]],
user: Optional[CollectionElement]) -> List[Dict[str, Any]]: user: Optional[CollectionElement]) -> List[Dict[str, Any]]:
@ -32,9 +28,9 @@ class MediafileAccessPermissions(BaseAccessPermissions):
for the user. Removes hidden mediafiles for some users. for the user. Removes hidden mediafiles for some users.
""" """
# Parse data. # Parse data.
if has_perm(user, 'mediafiles.can_see') and has_perm(user, 'mediafiles.can_see_hidden'): if await async_has_perm(user, 'mediafiles.can_see') and await async_has_perm(user, 'mediafiles.can_see_hidden'):
data = full_data data = full_data
elif has_perm(user, 'mediafiles.can_see'): elif await async_has_perm(user, 'mediafiles.can_see'):
# Exclude hidden mediafiles. # Exclude hidden mediafiles.
data = [full for full in full_data if not full['hidden']] data = [full for full in full_data if not full['hidden']]
else: else:

View File

@ -1,3 +1,5 @@
from typing import Any, Dict, Set
from django.apps import AppConfig from django.apps import AppConfig
from ..utils.projector import register_projector_elements from ..utils.projector import register_projector_elements
@ -11,11 +13,12 @@ class MediafilesAppConfig(AppConfig):
def ready(self): def ready(self):
# Import all required stuff. # Import all required stuff.
from openslides.core.signals import permission_change, user_data_required from openslides.core.signals import permission_change
from openslides.utils.rest_api import router from openslides.utils.rest_api import router
from .projector import get_projector_elements from .projector import get_projector_elements
from .signals import get_permission_change_data, required_users from .signals import get_permission_change_data
from .views import MediafileViewSet from .views import MediafileViewSet
from ..utils.access_permissions import required_user
# Define projector elements. # Define projector elements.
register_projector_elements(get_projector_elements()) register_projector_elements(get_projector_elements())
@ -24,16 +27,25 @@ class MediafilesAppConfig(AppConfig):
permission_change.connect( permission_change.connect(
get_permission_change_data, get_permission_change_data,
dispatch_uid='mediafiles_get_permission_change_data') dispatch_uid='mediafiles_get_permission_change_data')
user_data_required.connect(
required_users,
dispatch_uid='mediafiles_required_users')
# Register viewsets. # Register viewsets.
router.register(self.get_model('Mediafile').get_collection_string(), MediafileViewSet) router.register(self.get_model('Mediafile').get_collection_string(), MediafileViewSet)
# register required_users
required_user.add_collection_string(self.get_model('Mediafile').get_collection_string(), required_users)
def get_startup_elements(self): def get_startup_elements(self):
""" """
Yields all Cachables required on startup i. e. opening the websocket Yields all Cachables required on startup i. e. opening the websocket
connection. connection.
""" """
yield self.get_model('Mediafile') yield self.get_model('Mediafile')
def required_users(element: Dict[str, Any]) -> Set[int]:
"""
Returns all user ids that are displayed as uploaders in any mediafile
if request_user can see mediafiles. This function may return an empty
set.
"""
return set(element['uploader_id'])

View File

@ -14,6 +14,7 @@ class Mediafile(RESTModelMixin, models.Model):
Class for uploaded files which can be delivered under a certain url. Class for uploaded files which can be delivered under a certain url.
""" """
access_permissions = MediafileAccessPermissions() access_permissions = MediafileAccessPermissions()
can_see_permission = 'mediafiles.can_see'
mediafile = models.FileField(upload_to='file') mediafile = models.FileField(upload_to='file')
""" """

View File

@ -1,9 +1,5 @@
from django.apps import apps from django.apps import apps
from ..utils.auth import has_perm
from ..utils.collection import Collection
from .models import Mediafile
def get_permission_change_data(sender, permissions=None, **kwargs): def get_permission_change_data(sender, permissions=None, **kwargs):
""" """
@ -14,17 +10,3 @@ def get_permission_change_data(sender, permissions=None, **kwargs):
# There could be only one 'mediafiles.can_see' and then we want to return data. # There could be only one 'mediafiles.can_see' and then we want to return data.
if permission.content_type.app_label == mediafiles_app.label and permission.codename == 'can_see': if permission.content_type.app_label == mediafiles_app.label and permission.codename == 'can_see':
yield from mediafiles_app.get_startup_elements() yield from mediafiles_app.get_startup_elements()
def required_users(sender, request_user, **kwargs):
"""
Returns all user ids that are displayed as uploaders in any mediafile
if request_user can see mediafiles. This function may return an empty
set.
"""
uploaders = set()
if has_perm(request_user, 'mediafiles.can_see'):
for mediafile_collection_element in Collection(Mediafile.get_collection_string()).element_generator():
full_data = mediafile_collection_element.get_full_data()
uploaders.add(full_data['uploader_id'])
return uploaders

View File

@ -2,7 +2,7 @@ from copy import deepcopy
from typing import Any, Dict, List, Optional from typing import Any, Dict, List, Optional
from ..utils.access_permissions import BaseAccessPermissions from ..utils.access_permissions import BaseAccessPermissions
from ..utils.auth import has_perm, in_some_groups from ..utils.auth import async_has_perm, async_in_some_groups
from ..utils.collection import CollectionElement from ..utils.collection import CollectionElement
@ -10,11 +10,7 @@ class MotionAccessPermissions(BaseAccessPermissions):
""" """
Access permissions container for Motion and MotionViewSet. Access permissions container for Motion and MotionViewSet.
""" """
def check_permissions(self, user): base_permission = 'motions.can_see'
"""
Returns True if the user has read access model instances.
"""
return has_perm(user, 'motions.can_see')
def get_serializer_class(self, user=None): def get_serializer_class(self, user=None):
""" """
@ -24,7 +20,7 @@ class MotionAccessPermissions(BaseAccessPermissions):
return MotionSerializer return MotionSerializer
def get_restricted_data( async def get_restricted_data(
self, self,
full_data: List[Dict[str, Any]], full_data: List[Dict[str, Any]],
user: Optional[CollectionElement]) -> List[Dict[str, Any]]: user: Optional[CollectionElement]) -> List[Dict[str, Any]]:
@ -36,7 +32,7 @@ class MotionAccessPermissions(BaseAccessPermissions):
personal notes. personal notes.
""" """
# Parse data. # Parse data.
if has_perm(user, 'motions.can_see'): if await async_has_perm(user, 'motions.can_see'):
# TODO: Refactor this after personal_notes system is refactored. # TODO: Refactor this after personal_notes system is refactored.
data = [] data = []
for full in full_data: for full in full_data:
@ -52,8 +48,8 @@ class MotionAccessPermissions(BaseAccessPermissions):
required_permission_to_see = full['state_required_permission_to_see'] required_permission_to_see = full['state_required_permission_to_see']
permission = ( permission = (
not required_permission_to_see or not required_permission_to_see or
has_perm(user, required_permission_to_see) or await async_has_perm(user, required_permission_to_see) or
has_perm(user, 'motions.can_manage') or await async_has_perm(user, 'motions.can_manage') or
is_submitter) is_submitter)
# Parse single motion. # Parse single motion.
@ -61,7 +57,7 @@ class MotionAccessPermissions(BaseAccessPermissions):
full_copy = deepcopy(full) full_copy = deepcopy(full)
full_copy['comments'] = [] full_copy['comments'] = []
for comment in full['comments']: for comment in full['comments']:
if in_some_groups(user, comment['read_groups_id']): if await async_in_some_groups(user, comment['read_groups_id']):
full_copy['comments'].append(comment) full_copy['comments'].append(comment)
data.append(full_copy) data.append(full_copy)
else: else:
@ -74,11 +70,7 @@ class MotionChangeRecommendationAccessPermissions(BaseAccessPermissions):
""" """
Access permissions container for MotionChangeRecommendation and MotionChangeRecommendationViewSet. Access permissions container for MotionChangeRecommendation and MotionChangeRecommendationViewSet.
""" """
def check_permissions(self, user): base_permission = 'motions.can_see'
"""
Returns True if the user has read access model instances.
"""
return has_perm(user, 'motions.can_see')
def get_serializer_class(self, user=None): def get_serializer_class(self, user=None):
""" """
@ -88,7 +80,7 @@ class MotionChangeRecommendationAccessPermissions(BaseAccessPermissions):
return MotionChangeRecommendationSerializer return MotionChangeRecommendationSerializer
def get_restricted_data( async def get_restricted_data(
self, self,
full_data: List[Dict[str, Any]], full_data: List[Dict[str, Any]],
user: Optional[CollectionElement]) -> List[Dict[str, Any]]: user: Optional[CollectionElement]) -> List[Dict[str, Any]]:
@ -98,8 +90,8 @@ class MotionChangeRecommendationAccessPermissions(BaseAccessPermissions):
the can_see permission. the can_see permission.
""" """
# Parse data. # Parse data.
if has_perm(user, 'motions.can_see'): if await async_has_perm(user, 'motions.can_see'):
has_manage_perms = has_perm(user, 'motion.can_manage') has_manage_perms = await async_has_perm(user, 'motion.can_manage')
data = [] data = []
for full in full_data: for full in full_data:
if not full['internal'] or has_manage_perms: if not full['internal'] or has_manage_perms:
@ -114,11 +106,7 @@ class MotionCommentSectionAccessPermissions(BaseAccessPermissions):
""" """
Access permissions container for MotionCommentSection and MotionCommentSectionViewSet. Access permissions container for MotionCommentSection and MotionCommentSectionViewSet.
""" """
def check_permissions(self, user): base_permission = 'motions.can_see'
"""
Returns True if the user has read access model instances.
"""
return has_perm(user, 'motions.can_see')
def get_serializer_class(self, user=None): def get_serializer_class(self, user=None):
""" """
@ -128,7 +116,7 @@ class MotionCommentSectionAccessPermissions(BaseAccessPermissions):
return MotionCommentSectionSerializer return MotionCommentSectionSerializer
def get_restricted_data( async def get_restricted_data(
self, self,
full_data: List[Dict[str, Any]], full_data: List[Dict[str, Any]],
user: Optional[CollectionElement]) -> List[Dict[str, Any]]: user: Optional[CollectionElement]) -> List[Dict[str, Any]]:
@ -137,12 +125,12 @@ class MotionCommentSectionAccessPermissions(BaseAccessPermissions):
will be removed, when the user is not in at least one of the read_groups. will be removed, when the user is not in at least one of the read_groups.
""" """
data: List[Dict[str, Any]] = [] data: List[Dict[str, Any]] = []
if has_perm(user, 'motions.can_manage'): if await async_has_perm(user, 'motions.can_manage'):
data = full_data data = full_data
else: else:
for full in full_data: for full in full_data:
read_groups = full.get('read_groups_id', []) read_groups = full.get('read_groups_id', [])
if in_some_groups(user, read_groups): if await async_in_some_groups(user, read_groups):
data.append(full) data.append(full)
return data return data
@ -151,11 +139,7 @@ class StatuteParagraphAccessPermissions(BaseAccessPermissions):
""" """
Access permissions container for StatuteParagraph and StatuteParagraphViewSet. Access permissions container for StatuteParagraph and StatuteParagraphViewSet.
""" """
def check_permissions(self, user): base_permission = 'motions.can_see'
"""
Returns True if the user has read access model instances.
"""
return has_perm(user, 'motions.can_see')
def get_serializer_class(self, user=None): def get_serializer_class(self, user=None):
""" """
@ -170,11 +154,7 @@ class CategoryAccessPermissions(BaseAccessPermissions):
""" """
Access permissions container for Category and CategoryViewSet. Access permissions container for Category and CategoryViewSet.
""" """
def check_permissions(self, user): base_permission = 'motions.can_see'
"""
Returns True if the user has read access model instances.
"""
return has_perm(user, 'motions.can_see')
def get_serializer_class(self, user=None): def get_serializer_class(self, user=None):
""" """
@ -189,11 +169,7 @@ class MotionBlockAccessPermissions(BaseAccessPermissions):
""" """
Access permissions container for Category and CategoryViewSet. Access permissions container for Category and CategoryViewSet.
""" """
def check_permissions(self, user): base_permission = 'motions.can_see'
"""
Returns True if the user has read access model instances.
"""
return has_perm(user, 'motions.can_see')
def get_serializer_class(self, user=None): def get_serializer_class(self, user=None):
""" """
@ -208,11 +184,7 @@ class WorkflowAccessPermissions(BaseAccessPermissions):
""" """
Access permissions container for Workflow and WorkflowViewSet. Access permissions container for Workflow and WorkflowViewSet.
""" """
def check_permissions(self, user): base_permission = 'motions.can_see'
"""
Returns True if the user has read access model instances.
"""
return has_perm(user, 'motions.can_see')
def get_serializer_class(self, user=None): def get_serializer_class(self, user=None):
""" """

View File

@ -1,3 +1,5 @@
from typing import Any, Dict, Set
from django.apps import AppConfig from django.apps import AppConfig
from django.db.models.signals import post_migrate from django.db.models.signals import post_migrate
@ -12,13 +14,12 @@ class MotionsAppConfig(AppConfig):
def ready(self): def ready(self):
# Import all required stuff. # Import all required stuff.
from openslides.core.signals import permission_change, user_data_required from openslides.core.signals import permission_change
from openslides.utils.rest_api import router from openslides.utils.rest_api import router
from .projector import get_projector_elements from .projector import get_projector_elements
from .signals import ( from .signals import (
create_builtin_workflows, create_builtin_workflows,
get_permission_change_data, get_permission_change_data,
required_users,
) )
from .views import ( from .views import (
CategoryViewSet, CategoryViewSet,
@ -31,6 +32,7 @@ class MotionsAppConfig(AppConfig):
StateViewSet, StateViewSet,
WorkflowViewSet, WorkflowViewSet,
) )
from ..utils.access_permissions import required_user
# Define projector elements. # Define projector elements.
register_projector_elements(get_projector_elements()) register_projector_elements(get_projector_elements())
@ -42,9 +44,6 @@ class MotionsAppConfig(AppConfig):
permission_change.connect( permission_change.connect(
get_permission_change_data, get_permission_change_data,
dispatch_uid='motions_get_permission_change_data') dispatch_uid='motions_get_permission_change_data')
user_data_required.connect(
required_users,
dispatch_uid='motions_required_users')
# Register viewsets. # Register viewsets.
router.register(self.get_model('Category').get_collection_string(), CategoryViewSet) router.register(self.get_model('Category').get_collection_string(), CategoryViewSet)
@ -58,6 +57,9 @@ class MotionsAppConfig(AppConfig):
router.register(self.get_model('MotionPoll').get_collection_string(), MotionPollViewSet) router.register(self.get_model('MotionPoll').get_collection_string(), MotionPollViewSet)
router.register(self.get_model('State').get_collection_string(), StateViewSet) router.register(self.get_model('State').get_collection_string(), StateViewSet)
# Register required_users
required_user.add_collection_string(self.get_model('Motion').get_collection_string(), required_users)
def get_config_variables(self): def get_config_variables(self):
from .config_variables import get_config_variables from .config_variables import get_config_variables
return get_config_variables() return get_config_variables()
@ -70,3 +72,14 @@ class MotionsAppConfig(AppConfig):
for model_name in ('Category', 'StatuteParagraph', 'Motion', 'MotionBlock', for model_name in ('Category', 'StatuteParagraph', 'Motion', 'MotionBlock',
'Workflow', 'MotionChangeRecommendation', 'MotionCommentSection'): 'Workflow', 'MotionChangeRecommendation', 'MotionCommentSection'):
yield self.get_model(model_name) yield self.get_model(model_name)
def required_users(element: Dict[str, Any]) -> Set[int]:
"""
Returns all user ids that are displayed as as submitter or supporter in
any motion if request_user can see motions. This function may return an
empty set.
"""
submitters_supporters = set([submitter['user_id'] for submitter in element['submitters']])
submitters_supporters.update(element['supporters_id'])
return submitters_supporters

View File

@ -93,6 +93,7 @@ class Motion(RESTModelMixin, models.Model):
This class is the main entry point to all other classes related to a motion. This class is the main entry point to all other classes related to a motion.
""" """
access_permissions = MotionAccessPermissions() access_permissions = MotionAccessPermissions()
can_see_permission = 'motions.can_see'
objects = MotionManager() objects = MotionManager()

View File

@ -1,11 +1,7 @@
from typing import Set
from django.apps import apps from django.apps import apps
from django.utils.translation import ugettext_noop from django.utils.translation import ugettext_noop
from ..utils.auth import has_perm from .models import State, Workflow
from ..utils.collection import Collection
from .models import Motion, State, Workflow
def create_builtin_workflows(sender, **kwargs): def create_builtin_workflows(sender, **kwargs):
@ -108,19 +104,3 @@ def get_permission_change_data(sender, permissions, **kwargs):
# There could be only one 'motions.can_see' and then we want to return data. # There could be only one 'motions.can_see' and then we want to return data.
if permission.content_type.app_label == motions_app.label and permission.codename == 'can_see': if permission.content_type.app_label == motions_app.label and permission.codename == 'can_see':
yield from motions_app.get_startup_elements() yield from motions_app.get_startup_elements()
def required_users(sender, request_user, **kwargs):
"""
Returns all user ids that are displayed as as submitter or supporter in
any motion if request_user can see motions. This function may return an
empty set.
"""
submitters_supporters: Set[int] = set()
if has_perm(request_user, 'motions.can_see'):
for motion_collection_element in Collection(Motion.get_collection_string()).element_generator():
full_data = motion_collection_element.get_full_data()
submitters_supporters.update(
[submitter['user_id'] for submitter in full_data['submitters']])
submitters_supporters.update(full_data['supporters_id'])
return submitters_supporters

View File

@ -1,16 +1,11 @@
from ..utils.access_permissions import BaseAccessPermissions from ..utils.access_permissions import BaseAccessPermissions
from ..utils.auth import has_perm
class TopicAccessPermissions(BaseAccessPermissions): class TopicAccessPermissions(BaseAccessPermissions):
""" """
Access permissions container for Topic and TopicViewSet. Access permissions container for Topic and TopicViewSet.
""" """
def check_permissions(self, user): base_permission = 'agenda.can_see'
"""
Returns True if the user has read access model instances.
"""
return has_perm(user, 'agenda.can_see')
def get_serializer_class(self, user=None): def get_serializer_class(self, user=None):
""" """

View File

@ -1,22 +1,17 @@
from typing import Any, Dict, List, Optional from typing import Any, Dict, List, Optional, Set
from django.contrib.auth.models import AnonymousUser from ..utils.access_permissions import BaseAccessPermissions, required_user
from ..utils.auth import async_has_perm
from ..core.signals import user_data_required from ..utils.collection import (
from ..utils.access_permissions import BaseAccessPermissions CollectionElement,
from ..utils.auth import anonymous_is_enabled, has_perm get_model_from_collection_string,
from ..utils.collection import CollectionElement )
class UserAccessPermissions(BaseAccessPermissions): class UserAccessPermissions(BaseAccessPermissions):
""" """
Access permissions container for User and UserViewSet. Access permissions container for User and UserViewSet.
""" """
def check_permissions(self, user):
"""
Every user has read access for their model instnces.
"""
return True
def get_serializer_class(self, user=None): def get_serializer_class(self, user=None):
""" """
@ -26,7 +21,7 @@ class UserAccessPermissions(BaseAccessPermissions):
return UserFullSerializer return UserFullSerializer
def get_restricted_data( async def get_restricted_data(
self, self,
full_data: List[Dict[str, Any]], full_data: List[Dict[str, Any]],
user: Optional[CollectionElement]) -> List[Dict[str, Any]]: user: Optional[CollectionElement]) -> List[Dict[str, Any]]:
@ -62,9 +57,9 @@ class UserAccessPermissions(BaseAccessPermissions):
litte_data_fields.discard('groups') litte_data_fields.discard('groups')
# Check user permissions. # Check user permissions.
if has_perm(user, 'users.can_see_name'): if await async_has_perm(user, 'users.can_see_name'):
if has_perm(user, 'users.can_see_extra_data'): if await async_has_perm(user, 'users.can_see_extra_data'):
if has_perm(user, 'users.can_manage'): if await async_has_perm(user, 'users.can_manage'):
data = [filtered_data(full, all_data_fields) for full in full_data] data = [filtered_data(full, all_data_fields) for full in full_data]
else: else:
data = [filtered_data(full, many_data_fields) for full in full_data] data = [filtered_data(full, many_data_fields) for full in full_data]
@ -73,23 +68,21 @@ class UserAccessPermissions(BaseAccessPermissions):
else: else:
# Build a list of users, that can be seen without any permissions (with little fields). # Build a list of users, that can be seen without any permissions (with little fields).
user_ids = set()
# Everybody can see himself. Also everybody can see every user # Everybody can see himself. Also everybody can see every user
# that is required e. g. as speaker, motion submitter or # that is required e. g. as speaker, motion submitter or
# assignment candidate. # assignment candidate.
can_see_collection_strings: Set[str] = set()
for collection_string in required_user.get_collection_strings():
if await async_has_perm(user, get_model_from_collection_string(collection_string).can_see_permission):
can_see_collection_strings.add(collection_string)
user_ids = await required_user.get_required_users(can_see_collection_strings)
# Add oneself. # Add oneself.
if user is not None: if user is not None:
user_ids.add(user.id) user_ids.add(user.id)
# Get a list of all users, that are required by another app.
receiver_responses = user_data_required.send(
sender=self.__class__,
request_user=user)
for receiver, response in receiver_responses:
user_ids.update(response)
# Parse data. # Parse data.
data = [ data = [
filtered_data(full, litte_data_fields) filtered_data(full, litte_data_fields)
@ -104,13 +97,6 @@ class GroupAccessPermissions(BaseAccessPermissions):
""" """
Access permissions container for Groups. Everyone can see them Access permissions container for Groups. Everyone can see them
""" """
def check_permissions(self, user):
"""
Returns True if the user has read access model instances.
"""
# Every authenticated user can retrieve groups. Anonymous users can do
# so if they are enabled.
return not isinstance(user, AnonymousUser) or anonymous_is_enabled()
def get_serializer_class(self, user=None): def get_serializer_class(self, user=None):
""" """
@ -126,12 +112,6 @@ class PersonalNoteAccessPermissions(BaseAccessPermissions):
Access permissions container for personal notes. Every authenticated user Access permissions container for personal notes. Every authenticated user
can handle personal notes. can handle personal notes.
""" """
def check_permissions(self, user):
"""
Returns True if the user has read access model instances.
"""
# Every authenticated user can retrieve personal notes.
return not isinstance(user, AnonymousUser)
def get_serializer_class(self, user=None): def get_serializer_class(self, user=None):
""" """
@ -141,7 +121,7 @@ class PersonalNoteAccessPermissions(BaseAccessPermissions):
return PersonalNoteSerializer return PersonalNoteSerializer
def get_restricted_data( async def get_restricted_data(
self, self,
full_data: List[Dict[str, Any]], full_data: List[Dict[str, Any]],
user: Optional[CollectionElement]) -> List[Dict[str, Any]]: user: Optional[CollectionElement]) -> List[Dict[str, Any]]:

View File

@ -30,7 +30,7 @@ from ..utils.autoupdate import (
inform_data_collection_element_list, inform_data_collection_element_list,
) )
from ..utils.cache import element_cache from ..utils.cache import element_cache
from ..utils.collection import Collection, CollectionElement from ..utils.collection import CollectionElement
from ..utils.rest_api import ( from ..utils.rest_api import (
ModelViewSet, ModelViewSet,
Response, Response,
@ -329,9 +329,11 @@ class GroupViewSet(ModelViewSet):
if len(new_permissions) > 0: if len(new_permissions) > 0:
collection_elements: List[CollectionElement] = [] collection_elements: List[CollectionElement] = []
signal_results = permission_change.send(None, permissions=new_permissions, action='added') signal_results = permission_change.send(None, permissions=new_permissions, action='added')
all_full_data = async_to_sync(element_cache.get_all_full_data)()
for receiver, signal_collections in signal_results: for receiver, signal_collections in signal_results:
for cachable in signal_collections: for cachable in signal_collections:
collection_elements.extend(Collection(cachable.get_collection_string()).element_generator()) for element in all_full_data.get(cachable.get_collection_string(), {}):
collection_elements.append(CollectionElement.from_values(cachable.get_collection_string(), element['id']))
inform_data_collection_element_list(collection_elements) inform_data_collection_element_list(collection_elements)
# TODO: Some permissions are deleted. # TODO: Some permissions are deleted.
@ -462,8 +464,10 @@ class UserLoginView(APIView):
else: else:
# self.request.method == 'POST' # self.request.method == 'POST'
context['user_id'] = self.user.pk context['user_id'] = self.user.pk
user_collection = CollectionElement.from_instance(self.user) context['user'] = async_to_sync(element_cache.get_element_restricted_data)(
context['user'] = user_collection.as_dict_for_user(self.user) CollectionElement.from_instance(self.user),
self.user.get_collection_string(),
self.user.pk)
return super().get_context_data(**context) return super().get_context_data(**context)
@ -494,8 +498,10 @@ class WhoAmIView(APIView):
""" """
user_id = self.request.user.pk user_id = self.request.user.pk
if user_id is not None: if user_id is not None:
user_collection = CollectionElement.from_instance(self.request.user) user_data = async_to_sync(element_cache.get_element_restricted_data)(
user_data = user_collection.as_dict_for_user(self.request.user) user_to_collection_user(self.request.user),
self.request.user.get_collection_string(),
user_id)
else: else:
user_data = None user_data = None
return super().get_context_data( return super().get_context_data(

View File

@ -1,8 +1,15 @@
from typing import Any, Dict, List, Optional from typing import Any, Callable, Dict, List, Optional, Set
from asgiref.sync import async_to_sync
from django.db.models import Model from django.db.models import Model
from rest_framework.serializers import Serializer from rest_framework.serializers import Serializer
from .auth import (
async_anonymous_is_enabled,
async_has_perm,
user_to_collection_user,
)
from .cache import element_cache
from .collection import CollectionElement from .collection import CollectionElement
@ -14,11 +21,30 @@ class BaseAccessPermissions:
from this base class for every autoupdate root model. from this base class for every autoupdate root model.
""" """
base_permission = ''
"""
Set to a permission the user needs to see the element.
If this string is empty, all users can see it.
"""
def check_permissions(self, user: Optional[CollectionElement]) -> bool: def check_permissions(self, user: Optional[CollectionElement]) -> bool:
""" """
Returns True if the user has read access to model instances. Returns True if the user has read access to model instances.
""" """
return False # Convert user to right type
# TODO: Remove this and make sure, that user has always the right type
user = user_to_collection_user(user)
return async_to_sync(self.async_check_permissions)(user)
async def async_check_permissions(self, user: Optional[CollectionElement]) -> bool:
"""
Returns True if the user has read access to model instances.
"""
if self.base_permission:
return await async_has_perm(user, self.base_permission)
else:
return user is not None or await async_anonymous_is_enabled()
def get_serializer_class(self, user: CollectionElement = None) -> Serializer: def get_serializer_class(self, user: CollectionElement = None) -> Serializer:
""" """
@ -37,7 +63,7 @@ class BaseAccessPermissions:
""" """
return self.get_serializer_class(user=None)(instance).data return self.get_serializer_class(user=None)(instance).data
def get_restricted_data( async def get_restricted_data(
self, full_data: List[Dict[str, Any]], self, full_data: List[Dict[str, Any]],
user: Optional[CollectionElement]) -> List[Dict[str, Any]]: user: Optional[CollectionElement]) -> List[Dict[str, Any]]:
""" """
@ -55,4 +81,51 @@ class BaseAccessPermissions:
have access restrictions in your view or viewset in methods like have access restrictions in your view or viewset in methods like
retrieve() or list(). retrieve() or list().
""" """
return full_data if self.check_permissions(user) else [] return full_data if await self.async_check_permissions(user) else []
class RequiredUsers:
"""
Helper class to find all users that are required by another element.
"""
callables: Dict[str, Callable[[Dict[str, Any]], Set[int]]] = {}
def get_collection_strings(self) -> Set[str]:
"""
Returns all collection strings for elements that could have required users.
"""
return set(self.callables.keys())
def add_collection_string(self, collection_string: str, callable: Callable[[Dict[str, Any]], Set[int]]) -> None:
"""
Add a callable for a collection_string to get the required users of the
elements.
"""
self.callables[collection_string] = callable
async def get_required_users(self, collection_strings: Set[str]) -> Set[int]:
"""
Returns the user ids that are required by other elements.
Returns only user ids required by elements with a collection_string
in the argument collection_strings.
"""
user_ids: Set[int] = set()
all_full_data = await element_cache.get_all_full_data()
for collection_string in collection_strings:
# Get the callable for the collection_string
get_user_ids = self.callables.get(collection_string)
elements = all_full_data.get(collection_string, {})
if not (get_user_ids and elements):
# if the collection_string is unknown or it has no data, do nothing
continue
for element in elements:
user_ids.update(get_user_ids(element))
return user_ids
required_user = RequiredUsers()

View File

@ -1,5 +1,6 @@
from typing import Dict, List, Optional, Union, cast from typing import Dict, List, Optional, Union, cast
from asgiref.sync import async_to_sync
from django.apps import apps from django.apps import apps
from django.conf import settings from django.conf import settings
from django.contrib.auth import get_user_model from django.contrib.auth import get_user_model
@ -35,17 +36,28 @@ def has_perm(user: Optional[CollectionElement], perm: str) -> bool:
User can be a CollectionElement of a user or None. User can be a CollectionElement of a user or None.
""" """
group_collection_string = 'users/group' # This is the hard coded collection string for openslides.users.models.Group
# Convert user to right type # Convert user to right type
# TODO: Remove this and make use, that user has always the right type # TODO: Remove this and make use, that user has always the right type
user = user_to_collection_user(user) user = user_to_collection_user(user)
if user is None and not anonymous_is_enabled(): return async_to_sync(async_has_perm)(user, perm)
async def async_has_perm(user: Optional[CollectionElement], perm: str) -> bool:
"""
Checks that user has a specific permission.
User can be a CollectionElement of a user or None.
"""
group_collection_string = 'users/group' # This is the hard coded collection string for openslides.users.models.Group
if user is None and not await async_anonymous_is_enabled():
has_perm = False has_perm = False
elif user is None: elif user is None:
# Use the permissions from the default group. # Use the permissions from the default group.
default_group = CollectionElement.from_values(group_collection_string, GROUP_DEFAULT_PK) default_group = await element_cache.get_element_full_data(group_collection_string, GROUP_DEFAULT_PK)
has_perm = perm in default_group.get_full_data()['permissions'] if default_group is None:
raise RuntimeError('Default Group does not exist.')
has_perm = perm in default_group['permissions']
elif GROUP_ADMIN_PK in user.get_full_data()['groups_id']: elif GROUP_ADMIN_PK in user.get_full_data()['groups_id']:
# User in admin group (pk 2) grants all permissions. # User in admin group (pk 2) grants all permissions.
has_perm = True has_perm = True
@ -54,8 +66,11 @@ def has_perm(user: Optional[CollectionElement], perm: str) -> bool:
# permission. If the user has no groups, then use the default group. # permission. If the user has no groups, then use the default group.
group_ids = user.get_full_data()['groups_id'] or [GROUP_DEFAULT_PK] group_ids = user.get_full_data()['groups_id'] or [GROUP_DEFAULT_PK]
for group_id in group_ids: for group_id in group_ids:
group = CollectionElement.from_values(group_collection_string, group_id) group = await element_cache.get_element_full_data(group_collection_string, group_id)
if perm in group.get_full_data()['permissions']: if group is None:
raise RuntimeError('User is in non existing group with id {}.'.format(group_id))
if perm in group['permissions']:
has_perm = True has_perm = True
break break
else: else:
@ -78,7 +93,22 @@ def in_some_groups(user: Optional[CollectionElement], groups: List[int]) -> bool
# Convert user to right type # Convert user to right type
# TODO: Remove this and make use, that user has always the right type # TODO: Remove this and make use, that user has always the right type
user = user_to_collection_user(user) user = user_to_collection_user(user)
if user is None and not anonymous_is_enabled(): return async_to_sync(async_in_some_groups)(user, groups)
async def async_in_some_groups(user: Optional[CollectionElement], groups: List[int]) -> bool:
"""
Checks that user is in at least one given group. Groups can be given as a list
of ids. If the user is in the admin group (pk = 2) the result
is always true.
User can be a CollectionElement of a user or None.
"""
if not len(groups):
return False # early end here, if no groups are given.
if user is None and not await async_anonymous_is_enabled():
in_some_groups = False in_some_groups = False
elif user is None: elif user is None:
# Use the permissions from the default group. # Use the permissions from the default group.

View File

@ -14,7 +14,7 @@ from typing import (
Type, Type,
) )
from asgiref.sync import async_to_sync, sync_to_async from asgiref.sync import async_to_sync
from django.conf import settings from django.conf import settings
from .cache_providers import ( from .cache_providers import (
@ -23,8 +23,8 @@ from .cache_providers import (
MemmoryCacheProvider, MemmoryCacheProvider,
RedisCacheProvider, RedisCacheProvider,
get_all_cachables, get_all_cachables,
no_redis_dependency,
) )
from .redis import use_redis
from .utils import get_element_id, get_user_id, split_element_id from .utils import get_element_id, get_user_id, split_element_id
@ -60,7 +60,6 @@ class ElementCache:
def __init__( def __init__(
self, self,
redis: str,
use_restricted_data_cache: bool = False, use_restricted_data_cache: bool = False,
cache_provider_class: Type[ElementCacheProvider] = RedisCacheProvider, cache_provider_class: Type[ElementCacheProvider] = RedisCacheProvider,
cachable_provider: Callable[[], List[Cachable]] = get_all_cachables, cachable_provider: Callable[[], List[Cachable]] = get_all_cachables,
@ -71,7 +70,7 @@ class ElementCache:
When restricted_data_cache is false, no restricted data is saved. When restricted_data_cache is false, no restricted data is saved.
""" """
self.use_restricted_data_cache = use_restricted_data_cache self.use_restricted_data_cache = use_restricted_data_cache
self.cache_provider = cache_provider_class(redis) self.cache_provider = cache_provider_class()
self.cachable_provider = cachable_provider self.cachable_provider = cachable_provider
self._cachables: Optional[Dict[str, Cachable]] = None self._cachables: Optional[Dict[str, Cachable]] = None
@ -210,8 +209,6 @@ class ElementCache:
""" """
Returns one element as full data. Returns one element as full data.
If the cache is empty, it is created.
Returns None if the element does not exist. Returns None if the element does not exist.
""" """
element = await self.cache_provider.get_element(get_element_id(collection_string, id)) element = await self.cache_provider.get_element(get_element_id(collection_string, id))
@ -276,7 +273,7 @@ class ElementCache:
mapping = {} mapping = {}
for collection_string, full_data in full_data_elements.items(): for collection_string, full_data in full_data_elements.items():
restricter = self.cachables[collection_string].restrict_elements restricter = self.cachables[collection_string].restrict_elements
elements = await sync_to_async(restricter)(user, full_data) elements = await restricter(user, full_data)
for element in elements: for element in elements:
mapping.update( mapping.update(
{get_element_id(collection_string, element['id']): {get_element_id(collection_string, element['id']):
@ -303,7 +300,7 @@ class ElementCache:
all_restricted_data = {} all_restricted_data = {}
for collection_string, full_data in (await self.get_all_full_data()).items(): for collection_string, full_data in (await self.get_all_full_data()).items():
restricter = self.cachables[collection_string].restrict_elements restricter = self.cachables[collection_string].restrict_elements
elements = await sync_to_async(restricter)(user, full_data) elements = await restricter(user, full_data)
all_restricted_data[collection_string] = elements all_restricted_data[collection_string] = elements
return all_restricted_data return all_restricted_data
@ -335,7 +332,7 @@ class ElementCache:
restricted_data = {} restricted_data = {}
for collection_string, full_data in changed_elements.items(): for collection_string, full_data in changed_elements.items():
restricter = self.cachables[collection_string].restrict_elements restricter = self.cachables[collection_string].restrict_elements
elements = await sync_to_async(restricter)(user, full_data) elements = await restricter(user, full_data)
restricted_data[collection_string] = elements restricted_data[collection_string] = elements
return restricted_data, deleted_elements return restricted_data, deleted_elements
@ -358,6 +355,25 @@ class ElementCache:
for collection_string, value_list in raw_changed_elements.items()}, for collection_string, value_list in raw_changed_elements.items()},
deleted_elements) deleted_elements)
async def get_element_restricted_data(self, user: Optional['CollectionElement'], collection_string: str, id: int) -> Optional[Dict[str, Any]]:
"""
Returns the restricted_data of one element.
Returns None, if the element does not exists or the user has no permission to see it.
"""
if not self.use_restricted_data_cache:
full_data = await self.get_element_full_data(collection_string, id)
if full_data is None:
return None
restricter = self.cachables[collection_string].restrict_elements
restricted_data = await restricter(user, [full_data])
return restricted_data[0] if restricted_data else None
await self.update_restricted_data(user)
out = await self.cache_provider.get_element(get_element_id(collection_string, id), get_user_id(user))
return json.loads(out.decode()) if out else None
async def get_current_change_id(self) -> int: async def get_current_change_id(self) -> int:
""" """
Returns the current change id. Returns the current change id.
@ -383,19 +399,18 @@ class ElementCache:
return value return value
def load_element_cache(redis_addr: str = '', restricted_data: bool = True) -> ElementCache: def load_element_cache(restricted_data: bool = True) -> ElementCache:
""" """
Generates an element cache instance. Generates an element cache instance.
""" """
if not redis_addr: if use_redis:
return ElementCache(redis='', cache_provider_class=MemmoryCacheProvider) cache_provider_class: Type[ElementCacheProvider] = RedisCacheProvider
else:
cache_provider_class = MemmoryCacheProvider
if no_redis_dependency: return ElementCache(cache_provider_class=cache_provider_class, use_restricted_data_cache=restricted_data)
raise ImportError("OpenSlides is configured to use redis as cache backend, but aioredis is not installed.")
return ElementCache(redis=redis_addr, use_restricted_data_cache=restricted_data)
# Set the element_cache # Set the element_cache
redis_address = getattr(settings, 'REDIS_ADDRESS', '')
use_restricted_data = getattr(settings, 'RESTRICTED_DATA_CACHE', True) use_restricted_data = getattr(settings, 'RESTRICTED_DATA_CACHE', True)
element_cache = load_element_cache(redis_addr=redis_address, restricted_data=use_restricted_data) element_cache = load_element_cache(restricted_data=use_restricted_data)

View File

@ -13,20 +13,18 @@ from typing import (
from django.apps import apps from django.apps import apps
from typing_extensions import Protocol from typing_extensions import Protocol
from .redis import use_redis
from .utils import split_element_id, str_dict_to_bytes from .utils import split_element_id, str_dict_to_bytes
if use_redis:
from .redis import get_connection, aioredis
if TYPE_CHECKING: if TYPE_CHECKING:
# Dummy import Collection for mypy, can be fixed with python 3.7 # Dummy import Collection for mypy, can be fixed with python 3.7
from .collection import CollectionElement # noqa from .collection import CollectionElement # noqa
try:
import aioredis
except ImportError:
no_redis_dependency = True
else:
no_redis_dependency = False
class ElementCacheProvider(Protocol): class ElementCacheProvider(Protocol):
""" """
@ -35,8 +33,6 @@ class ElementCacheProvider(Protocol):
See RedisCacheProvider as reverence implementation. See RedisCacheProvider as reverence implementation.
""" """
def __init__(self, *args: Any) -> None: ...
async def clear_cache(self) -> None: ... async def clear_cache(self) -> None: ...
async def reset_full_cache(self, data: Dict[str, str]) -> None: ... async def reset_full_cache(self, data: Dict[str, str]) -> None: ...
@ -57,7 +53,7 @@ class ElementCacheProvider(Protocol):
user_id: Optional[int] = None, user_id: Optional[int] = None,
max_change_id: int = -1) -> Tuple[Dict[str, List[bytes]], List[str]]: ... max_change_id: int = -1) -> Tuple[Dict[str, List[bytes]], List[str]]: ...
async def get_element(self, element_id: str) -> Optional[bytes]: ... async def get_element(self, element_id: str, user_id: Optional[int] = None) -> Optional[bytes]: ...
async def del_restricted_data(self, user_id: int) -> None: ... async def del_restricted_data(self, user_id: int) -> None: ...
@ -76,42 +72,15 @@ class ElementCacheProvider(Protocol):
async def get_lowest_change_id(self) -> Optional[int]: ... async def get_lowest_change_id(self) -> Optional[int]: ...
class RedisConnectionContextManager:
"""
Async context manager for connections
"""
# TODO: contextlib.asynccontextmanager can be used in python 3.7
def __init__(self, redis_address: str) -> None:
self.redis_address = redis_address
async def __aenter__(self) -> 'aioredis.RedisConnection':
self.conn = await aioredis.create_redis(self.redis_address)
return self.conn
async def __aexit__(self, exc_type: Any, exc: Any, tb: Any) -> None:
self.conn.close()
class RedisCacheProvider: class RedisCacheProvider:
""" """
Cache provider that loads and saves the data to redis. Cache provider that loads and saves the data to redis.
""" """
redis_pool: Optional[aioredis.RedisConnection] = None
full_data_cache_key: str = 'full_data' full_data_cache_key: str = 'full_data'
restricted_user_cache_key: str = 'restricted_data:{user_id}' restricted_user_cache_key: str = 'restricted_data:{user_id}'
change_id_cache_key: str = 'change_id' change_id_cache_key: str = 'change_id'
prefix: str = 'element_cache_' prefix: str = 'element_cache_'
def __init__(self, redis: str) -> None:
self.redis_address = redis
def get_connection(self) -> RedisConnectionContextManager:
"""
Returns contextmanager for a redis connection.
"""
return RedisConnectionContextManager(self.redis_address)
def get_full_data_cache_key(self) -> str: def get_full_data_cache_key(self) -> str:
return "".join((self.prefix, self.full_data_cache_key)) return "".join((self.prefix, self.full_data_cache_key))
@ -125,14 +94,14 @@ class RedisCacheProvider:
""" """
Deleted all cache entries created with this element cache. Deleted all cache entries created with this element cache.
""" """
async with self.get_connection() as redis: async with get_connection() as redis:
await redis.eval("return redis.call('del', 'fake_key', unpack(redis.call('keys', ARGV[1])))", keys=[], args=["{}*".format(self.prefix)]) await redis.eval("return redis.call('del', 'fake_key', unpack(redis.call('keys', ARGV[1])))", keys=[], args=["{}*".format(self.prefix)])
async def reset_full_cache(self, data: Dict[str, str]) -> None: async def reset_full_cache(self, data: Dict[str, str]) -> None:
""" """
Deletes the full_data_cache and write new data in it. Deletes the full_data_cache and write new data in it.
""" """
async with self.get_connection() as redis: async with get_connection() as redis:
tr = redis.multi_exec() tr = redis.multi_exec()
tr.delete(self.get_full_data_cache_key()) tr.delete(self.get_full_data_cache_key())
tr.hmset_dict(self.get_full_data_cache_key(), data) tr.hmset_dict(self.get_full_data_cache_key(), data)
@ -145,7 +114,7 @@ class RedisCacheProvider:
If user_id is None, the method tests for full_data. If user_id is an int, it tests If user_id is None, the method tests for full_data. If user_id is an int, it tests
for the restricted_data_cache for the user with the user_id. 0 is for anonymous. for the restricted_data_cache for the user with the user_id. 0 is for anonymous.
""" """
async with self.get_connection() as redis: async with get_connection() as redis:
if user_id is None: if user_id is None:
cache_key = self.get_full_data_cache_key() cache_key = self.get_full_data_cache_key()
else: else:
@ -159,7 +128,7 @@ class RedisCacheProvider:
elements is a list with an even len. the odd values are the element_ids and the even elements is a list with an even len. the odd values are the element_ids and the even
values are the elements. The elements have to be encoded, for example with json. values are the elements. The elements have to be encoded, for example with json.
""" """
async with self.get_connection() as redis: async with get_connection() as redis:
await redis.hmset( await redis.hmset(
self.get_full_data_cache_key(), self.get_full_data_cache_key(),
*elements) *elements)
@ -173,7 +142,7 @@ class RedisCacheProvider:
If user_id is None, the elements are deleted from the full_data cache. If user_id is an If user_id is None, the elements are deleted from the full_data cache. If user_id is an
int, the elements are deleted one restricted_data_cache. 0 is for anonymous. int, the elements are deleted one restricted_data_cache. 0 is for anonymous.
""" """
async with self.get_connection() as redis: async with get_connection() as redis:
if user_id is None: if user_id is None:
cache_key = self.get_full_data_cache_key() cache_key = self.get_full_data_cache_key()
else: else:
@ -188,7 +157,7 @@ class RedisCacheProvider:
Generates and returns the change_id. Generates and returns the change_id.
""" """
async with self.get_connection() as redis: async with get_connection() as redis:
return int(await redis.eval( return int(await redis.eval(
lua_script_change_data, lua_script_change_data,
keys=[self.get_change_id_cache_key()], keys=[self.get_change_id_cache_key()],
@ -206,18 +175,24 @@ class RedisCacheProvider:
cache_key = self.get_full_data_cache_key() cache_key = self.get_full_data_cache_key()
else: else:
cache_key = self.get_restricted_data_cache_key(user_id) cache_key = self.get_restricted_data_cache_key(user_id)
async with self.get_connection() as redis:
async with get_connection() as redis:
return await redis.hgetall(cache_key) return await redis.hgetall(cache_key)
async def get_element(self, element_id: str) -> Optional[bytes]: async def get_element(self, element_id: str, user_id: Optional[int] = None) -> Optional[bytes]:
""" """
Returns one element from the full_data_cache. Returns one element from the cache.
Returns None, when the element does not exist. Returns None, when the element does not exist.
""" """
async with self.get_connection() as redis: if user_id is None:
cache_key = self.get_full_data_cache_key()
else:
cache_key = self.get_restricted_data_cache_key(user_id)
async with get_connection() as redis:
return await redis.hget( return await redis.hget(
self.get_full_data_cache_key(), cache_key,
element_id) element_id)
async def get_data_since( async def get_data_since(
@ -244,7 +219,7 @@ class RedisCacheProvider:
# Convert max_change_id to a string. If its negative, use the string '+inf' # Convert max_change_id to a string. If its negative, use the string '+inf'
redis_max_change_id = "+inf" if max_change_id < 0 else str(max_change_id) redis_max_change_id = "+inf" if max_change_id < 0 else str(max_change_id)
async with self.get_connection() as redis: async with get_connection() as redis:
# lua script that returns gets all element_ids from change_id_cache_key # lua script that returns gets all element_ids from change_id_cache_key
# and then uses each element_id on full_data or restricted_data. # and then uses each element_id on full_data or restricted_data.
# It returns a list where the odd values are the change_id and the # It returns a list where the odd values are the change_id and the
@ -282,7 +257,7 @@ class RedisCacheProvider:
""" """
Deletes all restricted_data for an user. 0 is for the anonymous user. Deletes all restricted_data for an user. 0 is for the anonymous user.
""" """
async with self.get_connection() as redis: async with get_connection() as redis:
await redis.delete(self.get_restricted_data_cache_key(user_id)) await redis.delete(self.get_restricted_data_cache_key(user_id))
async def set_lock(self, lock_name: str) -> bool: async def set_lock(self, lock_name: str) -> bool:
@ -294,14 +269,14 @@ class RedisCacheProvider:
Returns False when the lock was already set. Returns False when the lock was already set.
""" """
# TODO: Improve lock. See: https://redis.io/topics/distlock # TODO: Improve lock. See: https://redis.io/topics/distlock
async with self.get_connection() as redis: async with get_connection() as redis:
return await redis.setnx("{}lock_{}".format(self.prefix, lock_name), 1) return await redis.setnx("{}lock_{}".format(self.prefix, lock_name), 1)
async def get_lock(self, lock_name: str) -> bool: async def get_lock(self, lock_name: str) -> bool:
""" """
Returns True, when the lock for the restricted_data of an user is set. Else False. Returns True, when the lock for the restricted_data of an user is set. Else False.
""" """
async with self.get_connection() as redis: async with get_connection() as redis:
return await redis.get("{}lock_{}".format(self.prefix, lock_name)) return await redis.get("{}lock_{}".format(self.prefix, lock_name))
async def del_lock(self, lock_name: str) -> None: async def del_lock(self, lock_name: str) -> None:
@ -309,7 +284,7 @@ class RedisCacheProvider:
Deletes the lock for the restricted_data of an user. Does nothing when the Deletes the lock for the restricted_data of an user. Does nothing when the
lock is not set. lock is not set.
""" """
async with self.get_connection() as redis: async with get_connection() as redis:
await redis.delete("{}lock_{}".format(self.prefix, lock_name)) await redis.delete("{}lock_{}".format(self.prefix, lock_name))
async def get_change_id_user(self, user_id: int) -> Optional[int]: async def get_change_id_user(self, user_id: int) -> Optional[int]:
@ -318,7 +293,7 @@ class RedisCacheProvider:
This is the change_id where the restricted_data was last calculated. This is the change_id where the restricted_data was last calculated.
""" """
async with self.get_connection() as redis: async with get_connection() as redis:
return await redis.hget(self.get_restricted_data_cache_key(user_id), '_config:change_id') return await redis.hget(self.get_restricted_data_cache_key(user_id), '_config:change_id')
async def update_restricted_data(self, user_id: int, data: Dict[str, str]) -> None: async def update_restricted_data(self, user_id: int, data: Dict[str, str]) -> None:
@ -328,14 +303,14 @@ class RedisCacheProvider:
data has to be a dict where the key is an element_id and the value the (json-) encoded data has to be a dict where the key is an element_id and the value the (json-) encoded
element. element.
""" """
async with self.get_connection() as redis: async with get_connection() as redis:
await redis.hmset_dict(self.get_restricted_data_cache_key(user_id), data) await redis.hmset_dict(self.get_restricted_data_cache_key(user_id), data)
async def get_current_change_id(self) -> List[Tuple[str, int]]: async def get_current_change_id(self) -> List[Tuple[str, int]]:
""" """
Get the highest change_id from redis. Get the highest change_id from redis.
""" """
async with self.get_connection() as redis: async with get_connection() as redis:
return await redis.zrevrangebyscore( return await redis.zrevrangebyscore(
self.get_change_id_cache_key(), self.get_change_id_cache_key(),
withscores=True, withscores=True,
@ -348,7 +323,7 @@ class RedisCacheProvider:
Returns None if lowest score does not exist. Returns None if lowest score does not exist.
""" """
async with self.get_connection() as redis: async with get_connection() as redis:
return await redis.zscore( return await redis.zscore(
self.get_change_id_cache_key(), self.get_change_id_cache_key(),
'_config:lowest_change_id') '_config:lowest_change_id')
@ -364,7 +339,7 @@ class MemmoryCacheProvider:
When you use different processes they will use diffrent data. When you use different processes they will use diffrent data.
""" """
def __init__(self, *args: Any, **kwargs: Any) -> None: def __init__(self) -> None:
self.set_data_dicts() self.set_data_dicts()
def set_data_dicts(self) -> None: def set_data_dicts(self) -> None:
@ -428,8 +403,13 @@ class MemmoryCacheProvider:
return str_dict_to_bytes(cache_dict) return str_dict_to_bytes(cache_dict)
async def get_element(self, element_id: str) -> Optional[bytes]: async def get_element(self, element_id: str, user_id: Optional[int] = None) -> Optional[bytes]:
value = self.full_data.get(element_id, None) if user_id is None:
cache_dict = self.full_data
else:
cache_dict = self.restricted_data.get(user_id, {})
value = cache_dict.get(element_id, None)
return value.encode() if value is not None else None return value.encode() if value is not None else None
async def get_data_since( async def get_data_since(
@ -516,7 +496,7 @@ class Cachable(Protocol):
Returns all elements of the cachable. Returns all elements of the cachable.
""" """
def restrict_elements( async def restrict_elements(
self, self,
user: Optional['CollectionElement'], user: Optional['CollectionElement'],
elements: List[Dict[str, Any]]) -> List[Dict[str, Any]]: elements: List[Dict[str, Any]]) -> List[Dict[str, Any]]:

View File

@ -85,15 +85,6 @@ class CollectionElement:
return (self.collection_string == collection_element.collection_string and return (self.collection_string == collection_element.collection_string and
self.id == collection_element.id) self.id == collection_element.id)
def as_dict_for_user(self, user: Optional['CollectionElement']) -> Optional[Dict[str, Any]]:
"""
Returns a dict with the data for a user. Can be used for the rest api.
Returns None if the user does not has the permission to see the element.
"""
restricted_data = self.get_access_permissions().get_restricted_data([self.get_full_data()], user)
return restricted_data[0] if restricted_data else None
def get_model(self) -> Type[Model]: def get_model(self) -> Type[Model]:
""" """
Returns the django model that is used for this collection. Returns the django model that is used for this collection.
@ -106,20 +97,6 @@ class CollectionElement:
""" """
return self.get_model().get_access_permissions() return self.get_model().get_access_permissions()
def get_element_from_db(self) -> Optional[Dict[str, Any]]:
# Hack for django 2.0 and channels 2.1 to stay in the same thread.
# This is needed for the tests.
try:
query = self.get_model().objects.get_full_queryset()
except AttributeError:
# If the model des not have to method get_full_queryset(), then use
# the default queryset from django.
query = self.get_model().objects
try:
return self.get_access_permissions().get_full_data(query.get(pk=self.id))
except self.get_model().DoesNotExist:
return None
def get_full_data(self) -> Dict[str, Any]: def get_full_data(self) -> Dict[str, Any]:
""" """
Returns the full_data of this collection_element from with all other Returns the full_data of this collection_element from with all other
@ -151,95 +128,6 @@ class CollectionElement:
return self.deleted return self.deleted
class Collection:
"""
Represents all elements of one collection.
"""
def __init__(self, collection_string: str, full_data: List[Dict[str, Any]] = None) -> None:
"""
Initiates a Collection. A collection_string has to be given. If
full_data (a list of dictionaries) is not given the method
get_full_data() exposes all data by iterating over all
CollectionElements.
"""
self.collection_string = collection_string
self.full_data = full_data
def get_model(self) -> Type[Model]:
"""
Returns the django model that is used for this collection.
"""
return get_model_from_collection_string(self.collection_string)
def get_access_permissions(self) -> 'BaseAccessPermissions':
"""
Returns the get_access_permissions object for the this collection.
"""
return self.get_model().get_access_permissions()
def element_generator(self) -> Generator[CollectionElement, None, None]:
"""
Generator that yields all collection_elements of this collection.
"""
for full_data in self.get_full_data():
yield CollectionElement.from_values(
self.collection_string,
full_data['id'],
full_data=full_data)
def get_elements_from_db(self) ->Dict[str, List[Dict[str, Any]]]:
# Hack for django 2.0 and channels 2.1 to stay in the same thread.
# This is needed for the tests.
try:
query = self.get_model().objects.get_full_queryset()
except AttributeError:
# If the model des not have to method get_full_queryset(), then use
# the default queryset from django.
query = self.get_model().objects
return {self.collection_string: [self.get_model().get_access_permissions().get_full_data(instance) for instance in query.all()]}
def get_full_data(self) -> List[Dict[str, Any]]:
"""
Returns a list of dictionaries with full_data of all collection
elements.
"""
if self.full_data is None:
# The type of all_full_data has to be set for mypy
all_full_data: Dict[str, List[Dict[str, Any]]] = {}
all_full_data = async_to_sync(element_cache.get_all_full_data)()
self.full_data = all_full_data.get(self.collection_string, [])
return self.full_data # type: ignore
def as_list_for_user(self, user: Optional[CollectionElement]) -> List[Dict[str, Any]]:
"""
Returns a list of dictonaries to send them to a user, for example over
the rest api.
"""
return self.get_access_permissions().get_restricted_data(self.get_full_data(), user)
def get_collection_string(self) -> str:
"""
Returns the collection_string.
"""
return self.collection_string
def get_elements(self) -> List[Dict[str, Any]]:
"""
Returns all elements of the Collection as full_data.
"""
return self.get_full_data()
def restrict_elements(
self,
user: Optional['CollectionElement'],
elements: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
"""
Converts the full_data to restricted data.
"""
return self.get_model().get_access_permissions().get_restricted_data(user, elements)
_models_to_collection_string: Dict[str, Type[Model]] = {} _models_to_collection_string: Dict[str, Type[Model]] = {}
@ -268,5 +156,5 @@ def get_model_from_collection_string(collection_string: str) -> Type[Model]:
try: try:
model = _models_to_collection_string[collection_string] model = _models_to_collection_string[collection_string]
except KeyError: except KeyError:
raise ValueError('Invalid message. A valid collection_string is missing.') raise ValueError('Invalid message. A valid collection_string is missing. Got {}'.format(collection_string))
return model return model

View File

@ -23,8 +23,12 @@ class SiteConsumer(ProtocollAsyncJsonWebsocketConsumer):
Sends the startup data to the user. Sends the startup data to the user.
""" """
# If the user is the anonymous user, change the value to None
if self.scope['user'].id is None:
self.scope['user'] = None
change_id = None change_id = None
if not await async_anonymous_is_enabled() and self.scope['user'].id is None: if not await async_anonymous_is_enabled() and self.scope['user'] is None:
await self.close() await self.close()
return return
@ -61,7 +65,7 @@ class SiteConsumer(ProtocollAsyncJsonWebsocketConsumer):
""" """
Send a notify message to the user. Send a notify message to the user.
""" """
user_id = self.scope['user'].id or 0 user_id = self.scope['user'].id if self.scope['user'] else 0
out = [] out = []
for item in event['incomming']: for item in event['incomming']:

View File

@ -134,11 +134,11 @@ class RESTModelMixin:
return [cls.get_access_permissions().get_full_data(instance) for instance in query.all()] return [cls.get_access_permissions().get_full_data(instance) for instance in query.all()]
@classmethod @classmethod
def restrict_elements( async def restrict_elements(
cls, cls,
user: Optional['CollectionElement'], user: Optional['CollectionElement'],
elements: List[Dict[str, Any]]) -> List[Dict[str, Any]]: elements: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
""" """
Converts a list of elements from full_data to restricted_data. Converts a list of elements from full_data to restricted_data.
""" """
return cls.get_access_permissions().get_restricted_data(elements, user) return await cls.get_access_permissions().get_restricted_data(elements, user)

37
openslides/utils/redis.py Normal file
View File

@ -0,0 +1,37 @@
from typing import Any
from django.conf import settings
try:
import aioredis
except ImportError:
use_redis = False
else:
# set use_redis to true, if there is a value for REDIS_ADDRESS in the settings
redis_address = getattr(settings, 'REDIS_ADDRESS', '')
use_redis = bool(redis_address)
class RedisConnectionContextManager:
"""
Async context manager for connections
"""
# TODO: contextlib.asynccontextmanager can be used in python 3.7
def __init__(self, redis_address: str) -> None:
self.redis_address = redis_address
async def __aenter__(self) -> 'aioredis.RedisConnection':
self.conn = await aioredis.create_redis(self.redis_address)
return self.conn
async def __aexit__(self, exc_type: Any, exc: Any, tb: Any) -> None:
self.conn.close()
def get_connection() -> RedisConnectionContextManager:
"""
Returns contextmanager for a redis connection.
"""
return RedisConnectionContextManager(redis_address)

View File

@ -1,6 +1,7 @@
from collections import OrderedDict from collections import OrderedDict
from typing import Any, Dict, Iterable, Optional, Type from typing import Any, Dict, Iterable, Optional, Type
from asgiref.sync import async_to_sync
from django.http import Http404 from django.http import Http404
from rest_framework import status from rest_framework import status
from rest_framework.decorators import detail_route, list_route from rest_framework.decorators import detail_route, list_route
@ -42,7 +43,7 @@ from rest_framework.viewsets import (
from .access_permissions import BaseAccessPermissions from .access_permissions import BaseAccessPermissions
from .auth import user_to_collection_user from .auth import user_to_collection_user
from .collection import Collection, CollectionElement from .cache import element_cache
__all__ = ['detail_route', 'DecimalField', 'list_route', 'SimpleMetadata', __all__ = ['detail_route', 'DecimalField', 'list_route', 'SimpleMetadata',
@ -187,11 +188,6 @@ class ModelSerializer(_ModelSerializer):
class ListModelMixin(_ListModelMixin): class ListModelMixin(_ListModelMixin):
""" """
Mixin to add the caching system to list requests. Mixin to add the caching system to list requests.
It is not allowed to use the method get_queryset() in derivated classes.
The attribute queryset has to be used in the following form:
queryset = Model.objects.all()
""" """
def list(self, request: Any, *args: Any, **kwargs: Any) -> Response: def list(self, request: Any, *args: Any, **kwargs: Any) -> Response:
model = self.get_queryset().model model = self.get_queryset().model
@ -201,20 +197,14 @@ class ListModelMixin(_ListModelMixin):
# The corresponding queryset does not support caching. # The corresponding queryset does not support caching.
response = super().list(request, *args, **kwargs) response = super().list(request, *args, **kwargs)
else: else:
collection = Collection(collection_string) all_restricted_data = async_to_sync(element_cache.get_all_restricted_data)(user_to_collection_user(request.user))
user = user_to_collection_user(request.user) response = Response(all_restricted_data.get(collection_string, []))
response = Response(collection.as_list_for_user(user))
return response return response
class RetrieveModelMixin(_RetrieveModelMixin): class RetrieveModelMixin(_RetrieveModelMixin):
""" """
Mixin to add the caching system to retrieve requests. Mixin to add the caching system to retrieve requests.
It is not allowed to use the method get_queryset() in derivated classes.
The attribute queryset has to be used in the following form:
queryset = Model.objects.all()
""" """
def retrieve(self, request: Any, *args: Any, **kwargs: Any) -> Response: def retrieve(self, request: Any, *args: Any, **kwargs: Any) -> Response:
model = self.get_queryset().model model = self.get_queryset().model
@ -225,15 +215,10 @@ class RetrieveModelMixin(_RetrieveModelMixin):
response = super().retrieve(request, *args, **kwargs) response = super().retrieve(request, *args, **kwargs)
else: else:
lookup_url_kwarg = self.lookup_url_kwarg or self.lookup_field lookup_url_kwarg = self.lookup_url_kwarg or self.lookup_field
collection_element = CollectionElement.from_values(
collection_string, self.kwargs[lookup_url_kwarg])
user = user_to_collection_user(request.user) user = user_to_collection_user(request.user)
try: content = async_to_sync(element_cache.get_element_restricted_data)(user, collection_string, self.kwargs[lookup_url_kwarg])
content = collection_element.as_dict_for_user(user)
except collection_element.get_model().DoesNotExist:
raise Http404
if content is None: if content is None:
self.permission_denied(request) raise Http404
response = Response(content) response = Response(content)
return response return response

View File

@ -1,4 +1,5 @@
import pytest import pytest
from asgiref.sync import async_to_sync
from django.test import TestCase, TransactionTestCase from django.test import TestCase, TransactionTestCase
from pytest_django.django_compat import is_django_unittest from pytest_django.django_compat import is_django_unittest
from pytest_django.plugin import validate_django_db from pytest_django.plugin import validate_django_db
@ -68,4 +69,5 @@ def reset_cache(request):
""" """
if 'django_db' in request.node.keywords or is_django_unittest(request): if 'django_db' in request.node.keywords or is_django_unittest(request):
# When the db is created, use the original cachables # When the db is created, use the original cachables
async_to_sync(element_cache.cache_provider.clear_cache)()
element_cache.ensure_cache(reset=True) element_cache.ensure_cache(reset=True)

View File

@ -42,7 +42,7 @@ class RetrieveItem(TestCase):
def test_hidden_by_anonymous_without_manage_perms(self): def test_hidden_by_anonymous_without_manage_perms(self):
response = self.client.get(reverse('item-detail', args=[self.item.pk])) response = self.client.get(reverse('item-detail', args=[self.item.pk]))
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) self.assertEqual(response.status_code, 404)
def test_hidden_by_anonymous_with_manage_perms(self): def test_hidden_by_anonymous_with_manage_perms(self):
group = Group.objects.get(pk=1) # Group with pk 1 is for anonymous users. group = Group.objects.get(pk=1) # Group with pk 1 is for anonymous users.

View File

@ -27,7 +27,7 @@ class TConfig:
config.key_to_id[item.name] = id+1 config.key_to_id[item.name] = id+1
return elements return elements
def restrict_elements( async def restrict_elements(
self, self,
user: Optional['CollectionElement'], user: Optional['CollectionElement'],
elements: List[Dict[str, Any]]) -> List[Dict[str, Any]]: elements: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
@ -50,7 +50,7 @@ class TUser:
'last_email_send': None, 'comment': '', 'is_active': True, 'default_password': 'admin', 'last_email_send': None, 'comment': '', 'is_active': True, 'default_password': 'admin',
'session_auth_hash': '362d4f2de1463293cb3aaba7727c967c35de43ee'}] 'session_auth_hash': '362d4f2de1463293cb3aaba7727c967c35de43ee'}]
def restrict_elements( async def restrict_elements(
self, self,
user: Optional['CollectionElement'], user: Optional['CollectionElement'],
elements: List[Dict[str, Any]]) -> List[Dict[str, Any]]: elements: List[Dict[str, Any]]) -> List[Dict[str, Any]]:

View File

@ -428,7 +428,7 @@ class RetrieveMotion(TestCase):
inform_changed_data(self.motion) inform_changed_data(self.motion)
response = guest_client.get(reverse('motion-detail', args=[self.motion.pk])) response = guest_client.get(reverse('motion-detail', args=[self.motion.pk]))
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) self.assertEqual(response.status_code, 404)
def test_admin_state_with_required_permission_to_see(self): def test_admin_state_with_required_permission_to_see(self):
state = self.motion.state state = self.motion.state
@ -461,6 +461,7 @@ class RetrieveMotion(TestCase):
config['general_system_enable_anonymous'] = True config['general_system_enable_anonymous'] = True
guest_client = APIClient() guest_client = APIClient()
inform_changed_data(group) inform_changed_data(group)
inform_changed_data(self.motion)
response_1 = guest_client.get(reverse('motion-detail', args=[self.motion.pk])) response_1 = guest_client.get(reverse('motion-detail', args=[self.motion.pk]))
self.assertEqual(response_1.status_code, status.HTTP_200_OK) self.assertEqual(response_1.status_code, status.HTTP_200_OK)
@ -473,7 +474,7 @@ class RetrieveMotion(TestCase):
password='password_ooth7taechai5Oocieya') password='password_ooth7taechai5Oocieya')
response_3 = guest_client.get(reverse('user-detail', args=[extra_user.pk])) response_3 = guest_client.get(reverse('user-detail', args=[extra_user.pk]))
self.assertEqual(response_3.status_code, status.HTTP_403_FORBIDDEN) self.assertEqual(response_3.status_code, 404)
class UpdateMotion(TestCase): class UpdateMotion(TestCase):
@ -983,6 +984,7 @@ class TestMotionCommentSection(TestCase):
section.save() section.save()
response = self.client.get(reverse('motioncommentsection-list')) response = self.client.get(reverse('motioncommentsection-list'))
self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertTrue(isinstance(response.data, list)) self.assertTrue(isinstance(response.data, list))
self.assertEqual(len(response.data), 1) self.assertEqual(len(response.data), 1)
@ -995,10 +997,12 @@ class TestMotionCommentSection(TestCase):
""" """
self.admin.groups.remove(self.group_in) # group_in has motions.can_manage permission self.admin.groups.remove(self.group_in) # group_in has motions.can_manage permission
self.admin.groups.add(self.group_out) # group_out does not. self.admin.groups.add(self.group_out) # group_out does not.
inform_changed_data(self.admin)
section = MotionCommentSection(name='test_name_f3mMD28LMcm29Coelwcm') section = MotionCommentSection(name='test_name_f3mMD28LMcm29Coelwcm')
section.save() section.save()
section.read_groups.add(self.group_out, self.group_in) section.read_groups.add(self.group_out, self.group_in)
inform_changed_data(section)
response = self.client.get(reverse('motioncommentsection-list')) response = self.client.get(reverse('motioncommentsection-list'))
self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(response.status_code, status.HTTP_200_OK)

View File

@ -69,7 +69,7 @@ class UserGetTest(TestCase):
response = guest_client.get('/rest/users/user/1/') response = guest_client.get('/rest/users/user/1/')
self.assertEqual(response.status_code, 403) self.assertEqual(response.status_code, 404)
class UserCreate(TestCase): class UserCreate(TestCase):
@ -535,7 +535,7 @@ class PersonalNoteTest(TestCase):
config['general_system_enable_anonymous'] = True config['general_system_enable_anonymous'] = True
guest_client = APIClient() guest_client = APIClient()
response = guest_client.get(reverse('personalnote-detail', args=[personal_note.pk])) response = guest_client.get(reverse('personalnote-detail', args=[personal_note.pk]))
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) self.assertEqual(response.status_code, 404)
def test_admin_send_JSON(self): def test_admin_send_JSON(self):
admin_client = APIClient() admin_client = APIClient()

View File

@ -26,37 +26,3 @@ class TestCollectionElementCache(TestCase):
""" """
with self.assertRaises(Topic.DoesNotExist): with self.assertRaises(Topic.DoesNotExist):
collection.CollectionElement.from_values('topics/topic', 999) collection.CollectionElement.from_values('topics/topic', 999)
class TestCollectionCache(TestCase):
def test_with_cache(self):
"""
Tests that no db query is used when the list is received twice.
"""
Topic.objects.create(title='test topic1')
Topic.objects.create(title='test topic2')
Topic.objects.create(title='test topic3')
topic_collection = collection.Collection('topics/topic')
list(topic_collection.get_full_data())
with self.assertNumQueries(0):
instance_list = list(topic_collection.get_full_data())
self.assertEqual(len(instance_list), 3)
def test_deletion(self):
"""
When an element is deleted, the cache should be updated automaticly via
the autoupdate system. So there should be no db queries.
"""
Topic.objects.create(title='test topic1')
Topic.objects.create(title='test topic2')
topic3 = Topic.objects.create(title='test topic3')
topic_collection = collection.Collection('topics/topic')
list(topic_collection.get_full_data())
collection.CollectionElement.from_instance(topic3, deleted=True)
topic3.delete()
with self.assertNumQueries(0):
instance_list = list(collection.Collection('topics/topic').get_full_data())
self.assertEqual(len(instance_list), 2)

View File

@ -75,3 +75,6 @@ MOTION_IDENTIFIER_MIN_DIGITS = 1
PASSWORD_HASHERS = [ PASSWORD_HASHERS = [
'django.contrib.auth.hashers.MD5PasswordHasher', 'django.contrib.auth.hashers.MD5PasswordHasher',
] ]
# Deactivate restricted_data_cache
RESTRICTED_DATA_CACHE = False

View File

@ -1,20 +0,0 @@
from unittest import TestCase
from openslides.users.access_permissions import PersonalNoteAccessPermissions
from openslides.utils.collection import CollectionElement
class TestPersonalNoteAccessPermissions(TestCase):
def test_get_restricted_data(self):
ap = PersonalNoteAccessPermissions()
rd = ap.get_restricted_data(
[{'user_id': 1}],
CollectionElement.from_values('users/user', 5, full_data={}))
self.assertEqual(rd, [])
def test_get_restricted_data_for_anonymous(self):
ap = PersonalNoteAccessPermissions()
rd = ap.get_restricted_data(
[{'user_id': 1}],
None)
self.assertEqual(rd, [])

View File

@ -32,7 +32,7 @@ class Collection1:
{'id': 1, 'value': 'value1'}, {'id': 1, 'value': 'value1'},
{'id': 2, 'value': 'value2'}] {'id': 2, 'value': 'value2'}]
def restrict_elements(self, user: Optional[CollectionElement], elements: List[Dict[str, Any]]) -> List[Dict[str, Any]]: async def restrict_elements(self, user: Optional[CollectionElement], elements: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
return restrict_elements(user, elements) return restrict_elements(user, elements)
@ -45,7 +45,7 @@ class Collection2:
{'id': 1, 'key': 'value1'}, {'id': 1, 'key': 'value1'},
{'id': 2, 'key': 'value2'}] {'id': 2, 'key': 'value2'}]
def restrict_elements(self, user: Optional[CollectionElement], elements: List[Dict[str, Any]]) -> List[Dict[str, Any]]: async def restrict_elements(self, user: Optional[CollectionElement], elements: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
return restrict_elements(user, elements) return restrict_elements(user, elements)

View File

@ -30,7 +30,6 @@ def sort_dict(encoded_dict: Dict[str, List[Dict[str, Any]]]) -> Dict[str, List[D
@pytest.fixture @pytest.fixture
def element_cache(): def element_cache():
element_cache = ElementCache( element_cache = ElementCache(
'test_redis',
cache_provider_class=TTestCacheProvider, cache_provider_class=TTestCacheProvider,
cachable_provider=get_cachable_provider(), cachable_provider=get_cachable_provider(),
start_time=0) start_time=0)