Added new flag to motion state to control access for different users.

This commit is contained in:
Norman Jäckel 2019-02-01 13:15:08 +01:00 committed by Emanuel Schütze
parent dc1e48329f
commit c355326466
7 changed files with 95 additions and 42 deletions

View File

@ -38,6 +38,7 @@ Motions:
recommendations [#4037, #4132]. recommendations [#4037, #4132].
- Added timestampes for motions [#4134]. - Added timestampes for motions [#4134].
- New config option to set reason as required field [#4232] - New config option to set reason as required field [#4232]
- Added new flag to motion state to control access for different users [#4235].
User: User:
- Added new admin group which grants all permissions. Users of existing group - Added new admin group which grants all permissions. Users of existing group

View File

@ -37,13 +37,17 @@ class MotionAccessPermissions(BaseAccessPermissions):
is_submitter = False is_submitter = False
# Check see permission for this motion. # Check see permission for this motion.
required_permission_to_see = full["state_required_permission_to_see"] from .models import State
permission = (
not required_permission_to_see if await async_has_perm(user_id, "motions.can_manage"):
or await async_has_perm(user_id, required_permission_to_see) level = State.MANAGERS_ONLY
or await async_has_perm(user_id, "motions.can_manage") elif await async_has_perm(user_id, "motions.can_manage_metadata"):
or is_submitter level = State.EXTENDED_MANAGERS
) elif is_submitter:
level = State.EXTENDED_MANAGERS_AND_SUBMITTER
else:
level = State.ALL
permission = level >= full["state_access_level"]
# Parse single motion. # Parse single motion.
if permission: if permission:
@ -57,7 +61,6 @@ class MotionAccessPermissions(BaseAccessPermissions):
data.append(full_copy) data.append(full_copy)
else: else:
data = [] data = []
return data return data

View File

@ -0,0 +1,43 @@
# Generated by Django 2.1.5 on 2019-02-01 11:58
from django.db import migrations, models
def transform_required_permission_to_see_field(apps, schema_editor):
"""
Sets new access_level of states to EXTENDED_MANAGERS_AND_SUBMITTER
if required_permission_to_see is given
"""
# We get the model from the versioned app registry;
# if we directly import it, it will be the wrong version.
State = apps.get_model("motions", "State")
for state in State.objects.all():
if state.required_permission_to_see:
state.access_level = 1
state.save(skip_autoupdate=True)
class Migration(migrations.Migration):
dependencies = [("motions", "0020_auto_20190119_1425")]
operations = [
migrations.AddField(
model_name="state",
name="access_level",
field=models.IntegerField(
choices=[
(0, "All users with permission to see motions"),
(
1,
"Submitters, managers and users with permission to manage metadata",
),
(2, "Only managers and users with permission to manage metadata"),
(3, "Only managers"),
],
default=0,
),
),
migrations.RunPython(transform_required_permission_to_see_field),
migrations.RemoveField(model_name="state", name="required_permission_to_see"),
]

View File

@ -1054,6 +1054,24 @@ class State(RESTModelMixin, models.Model):
state. state.
""" """
ALL = 0
EXTENDED_MANAGERS_AND_SUBMITTER = 1
EXTENDED_MANAGERS = 2
MANAGERS_ONLY = 3
ACCESS_LEVELS = (
(ALL, "All users with permission to see motions"),
(
EXTENDED_MANAGERS_AND_SUBMITTER,
"Submitters, managers and users with permission to manage metadata",
),
(
EXTENDED_MANAGERS,
"Only managers and users with permission to manage metadata",
),
(MANAGERS_ONLY, "Only managers"),
)
name = models.CharField(max_length=255) name = models.CharField(max_length=255)
"""A string representing the state.""" """A string representing the state."""
@ -1075,14 +1093,10 @@ class State(RESTModelMixin, models.Model):
Default value is 'primary' (blue). Default value is 'primary' (blue).
""" """
required_permission_to_see = models.CharField(max_length=255, blank=True) access_level = models.IntegerField(choices=ACCESS_LEVELS, default=0)
""" """
A permission string. If not empty, the user has to have this permission to Defines which users may see motions in this state e. g. only managers,
see a motion in this state. users with permission to manage metadata and submitters.
To use this feature change the database entry of a state object and add
your favourite permission string. You can do this e. g. by editing the
definitions in create_builtin_workflows() in openslides/motions/signals.py.
""" """
allow_support = models.BooleanField(default=False) allow_support = models.BooleanField(default=False)

View File

@ -102,7 +102,7 @@ class StateSerializer(ModelSerializer):
"name", "name",
"recommendation_label", "recommendation_label",
"css_class", "css_class",
"required_permission_to_see", "access_level",
"allow_support", "allow_support",
"allow_create_poll", "allow_create_poll",
"allow_submitter_edit", "allow_submitter_edit",
@ -389,7 +389,7 @@ class MotionSerializer(ModelSerializer):
polls = MotionPollSerializer(many=True, read_only=True) polls = MotionPollSerializer(many=True, read_only=True)
modified_final_version = CharField(allow_blank=True, required=False) modified_final_version = CharField(allow_blank=True, required=False)
reason = CharField(allow_blank=True, required=False) reason = CharField(allow_blank=True, required=False)
state_required_permission_to_see = SerializerMethodField() state_access_level = SerializerMethodField()
text = CharField(allow_blank=True) text = CharField(allow_blank=True)
title = CharField(max_length=255) title = CharField(max_length=255)
amendment_paragraphs = AmendmentParagraphsJSONSerializerField(required=False) amendment_paragraphs = AmendmentParagraphsJSONSerializerField(required=False)
@ -424,7 +424,7 @@ class MotionSerializer(ModelSerializer):
"supporters", "supporters",
"state", "state",
"state_extension", "state_extension",
"state_required_permission_to_see", "state_access_level",
"statute_paragraph", "statute_paragraph",
"workflow_id", "workflow_id",
"recommendation", "recommendation",
@ -531,12 +531,9 @@ class MotionSerializer(ModelSerializer):
return result return result
def get_state_required_permission_to_see(self, motion): def get_state_access_level(self, motion):
""" """
Returns the permission (as string) that is required for non Returns the access level of this state. The default is 0 so everybody
managers that are not submitters to see this motion in this state. with permission to see motions can see this motion.
Hint: Most states have and empty string here so this restriction is
disabled.
""" """
return motion.state.required_permission_to_see return motion.state.access_level

View File

@ -479,13 +479,11 @@ class RetrieveMotion(TestCase):
username=f"user_{index}", password="password" username=f"user_{index}", password="password"
) )
def test_guest_state_with_required_permission_to_see(self): def test_guest_state_with_access_level(self):
config["general_system_enable_anonymous"] = True config["general_system_enable_anonymous"] = True
guest_client = APIClient() guest_client = APIClient()
state = self.motion.state state = self.motion.state
state.required_permission_to_see = ( state.access_level = State.MANAGERS_ONLY
"permission_that_the_user_does_not_have_leeceiz9hi7iuta4ahY2"
)
state.save() state.save()
# The cache has to be cleared, see: # The cache has to be cleared, see:
# https://github.com/OpenSlides/OpenSlides/issues/3396 # https://github.com/OpenSlides/OpenSlides/issues/3396
@ -494,20 +492,17 @@ class RetrieveMotion(TestCase):
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, 404) self.assertEqual(response.status_code, 404)
def test_admin_state_with_required_permission_to_see(self): def test_admin_state_with_access_level(self):
state = self.motion.state state = self.motion.state
state.required_permission_to_see = ( state.access_level = State.MANAGERS_ONLY
"permission_that_the_user_does_not_have_coo1Iewu8Eing2xahfoo"
)
state.save() state.save()
response = self.client.get(reverse("motion-detail", args=[self.motion.pk])) response = self.client.get(reverse("motion-detail", args=[self.motion.pk]))
self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(response.status_code, status.HTTP_200_OK)
def test_submitter_state_with_required_permission_to_see(self): def test_submitter_state_with_access_level(self):
state = self.motion.state state = self.motion.state
state.required_permission_to_see = ( state.access_level = State.EXTENDED_MANAGERS_AND_SUBMITTER
"permission_that_the_user_does_not_have_eiW8af9caizoh1thaece"
)
state.save() state.save()
user = get_user_model().objects.create_user( user = get_user_model().objects.create_user(
username="username_ohS2opheikaSa5theijo", username="username_ohS2opheikaSa5theijo",

View File

@ -29,7 +29,7 @@ def all_data():
"supporters_id": [], "supporters_id": [],
"state_id": 1, "state_id": 1,
"state_extension": None, "state_extension": None,
"state_required_permission_to_see": "", "state_access_level": 0,
"statute_paragraph_id": None, "statute_paragraph_id": None,
"workflow_id": 1, "workflow_id": 1,
"recommendation_id": None, "recommendation_id": None,
@ -86,7 +86,7 @@ def all_data():
"name": "submitted", "name": "submitted",
"recommendation_label": None, "recommendation_label": None,
"css_class": "primary", "css_class": "primary",
"required_permission_to_see": "", "access_level": 0,
"allow_support": True, "allow_support": True,
"allow_create_poll": True, "allow_create_poll": True,
"allow_submitter_edit": True, "allow_submitter_edit": True,
@ -102,7 +102,7 @@ def all_data():
"name": "accepted", "name": "accepted",
"recommendation_label": "Acceptance", "recommendation_label": "Acceptance",
"css_class": "success", "css_class": "success",
"required_permission_to_see": "", "access_level": 0,
"allow_support": False, "allow_support": False,
"allow_create_poll": False, "allow_create_poll": False,
"allow_submitter_edit": False, "allow_submitter_edit": False,
@ -118,7 +118,7 @@ def all_data():
"name": "rejected", "name": "rejected",
"recommendation_label": "Rejection", "recommendation_label": "Rejection",
"css_class": "danger", "css_class": "danger",
"required_permission_to_see": "", "access_level": 0,
"allow_support": False, "allow_support": False,
"allow_create_poll": False, "allow_create_poll": False,
"allow_submitter_edit": False, "allow_submitter_edit": False,
@ -134,7 +134,7 @@ def all_data():
"name": "not decided", "name": "not decided",
"recommendation_label": "No decision", "recommendation_label": "No decision",
"css_class": "default", "css_class": "default",
"required_permission_to_see": "", "access_level": 0,
"allow_support": False, "allow_support": False,
"allow_create_poll": False, "allow_create_poll": False,
"allow_submitter_edit": False, "allow_submitter_edit": False,