Refactored state access level by renaming state field to restriction.

This commit is contained in:
Norman Jäckel 2019-03-20 17:21:01 +01:00 committed by FinnStutzenstein
parent 3fd5d19daa
commit 6f24b7c169
9 changed files with 153 additions and 56 deletions

View File

@ -35,8 +35,9 @@ Motions:
follow recommendation, manage submitters and supporters, change motion follow recommendation, manage submitters and supporters, change motion
category, motion block and origin and manage motion polls [#3913]. category, motion block and origin and manage motion polls [#3913].
- Added new permission to create amendments [#4128]. - Added new permission to create amendments [#4128].
- Added new flag to motion state to control access for different users. Added - Added new flag to motion state to control access restrictions for different
new permission to see motions in some internal state [#4235, #4518]. users. Added new permission to see motions in some internal state
[#4235, #4518, #4521].
- Allowed submitters to set state of new motions in complex and customized - Allowed submitters to set state of new motions in complex and customized
workflow [#4236]. workflow [#4236].
- Added multi select action to manage submitters, tags, states and - Added multi select action to manage submitters, tags, states and

View File

@ -37,19 +37,27 @@ class MotionAccessPermissions(BaseAccessPermissions):
is_submitter = False is_submitter = False
# Check see permission for this motion. # Check see permission for this motion.
from .models import State restriction = full["state_restriction"]
if await async_has_perm(user_id, "motions.can_manage"): # Managers can see all motions.
level = State.MANAGERS_ONLY permission = await async_has_perm(user_id, "motions.can_manage")
elif await async_has_perm( # If restriction field is an empty list, everybody can see the motion.
user_id, "motions.can_manage_metadata" permission = permission or not restriction
) or await async_has_perm(user_id, "motions.can_see_internal"):
level = State.EXTENDED_MANAGERS if not permission:
elif is_submitter: # Parse values of restriction field.
level = State.EXTENDED_MANAGERS_AND_SUBMITTER for value in restriction:
else: if value == "managers_only":
level = State.ALL # permission remains false
permission = level >= full["state_access_level"] break
elif value in ("motions.can_see_internal", "motions.can_manage_metadata"):
if await async_has_perm(user_id, value):
permission = True
break
elif value == "is_submitter":
if is_submitter:
permission = True
break
# Parse single motion. # Parse single motion.
if permission: if permission:

View File

@ -0,0 +1,25 @@
# Generated by Django 2.1.7 on 2019-03-20 15:49
from django.db import migrations
import jsonfield.encoder
import jsonfield.fields
class Migration(migrations.Migration):
dependencies = [("motions", "0022_auto_20190320_0840")]
operations = [
migrations.AddField(
model_name="state",
name="restriction",
field=jsonfield.fields.JSONField(
default=list,
dump_kwargs={
"cls": jsonfield.encoder.JSONEncoder,
"separators": (",", ":"),
},
load_kwargs={},
),
)
]

View File

@ -0,0 +1,36 @@
# Generated by Django 2.1.7 on 2019-03-20 15:49
from django.db import migrations
import jsonfield.encoder
import jsonfield.fields
def copy_access_level(apps, schema_editor):
"""
Sets new restriction field of states according to access_level.
"""
# 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.access_level == 3:
state.restriction = ["managers_only"]
elif state.access_level == 2:
state.restriction = [
"motions.can_see_internal",
"motions.can_manage_metadata",
]
elif state.access_level == 1:
state.restriction = [
"motions.can_see_internal",
"motions.can_manage_metadata",
"is_submitter",
]
state.save(skip_autoupdate=True)
class Migration(migrations.Migration):
dependencies = [("motions", "0023_state_restriction_1")]
operations = [migrations.RunPython(copy_access_level)]

View File

@ -0,0 +1,10 @@
# Generated by Django 2.1.7 on 2019-03-20 15:55
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [("motions", "0023_state_restriction_2")]
operations = [migrations.RemoveField(model_name="state", name="access_level")]

View File

@ -985,24 +985,6 @@ 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, authorized users (with permission to see internal motions), managers and users with permission to manage metadata",
),
(
EXTENDED_MANAGERS,
"Only authorized users (with permission to see internal motions), 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."""
@ -1024,11 +1006,22 @@ class State(RESTModelMixin, models.Model):
Default value is 'primary' (blue). Default value is 'primary' (blue).
""" """
access_level = models.IntegerField(choices=ACCESS_LEVELS, default=0) restriction = JSONField(default=list)
""" """
Defines which users may see motions in this state e. g. only managers, Defines which users may see motions in this state:
authorized users with permission to see internal motiosn, users with permission
to manage metadata and submitters. Contains a list of one or more of the following strings:
* motions.can_see_internal
* motions.can_manage_metadata
* is_submitter
* managers_only
If the list is empty, everybody with the general permission to see motions
can see this motion. If the list contains 'managers_only', only managers with
motions.can_manage permission may see this motion. In all other cases the user
shall have one of the given permissions respectivly is submitter of the motion.
Default: Empty list so everybody can see the motion.
""" """
allow_support = models.BooleanField(default=False) allow_support = models.BooleanField(default=False)

View File

@ -1,5 +1,6 @@
from typing import Dict, Optional from typing import Dict, Optional
import jsonschema
from django.db import transaction from django.db import transaction
from ..core.config import config from ..core.config import config
@ -101,7 +102,7 @@ class StateSerializer(ModelSerializer):
"name", "name",
"recommendation_label", "recommendation_label",
"css_class", "css_class",
"access_level", "restriction",
"allow_support", "allow_support",
"allow_create_poll", "allow_create_poll",
"allow_submitter_edit", "allow_submitter_edit",
@ -113,6 +114,30 @@ class StateSerializer(ModelSerializer):
"workflow", "workflow",
) )
def validate_restriction(self, value):
"""
Ensures that the value is a list and only contains valid values.
"""
schema = {
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "Motion workflow state restriction field schema",
"description": "An array containing one or more explicit strings to control restriction for motions in this state.",
"type": "array",
"enum": [
"motions.can_see_internal",
"motions.can_manage_metadata",
"is_submitter",
"managers_only",
],
}
# Validate value.
try:
jsonschema.validate(request.data, schema)
except jsonschema.ValidationError as err:
raise ValidationError({"detail": str(err)})
return value
class WorkflowSerializer(ModelSerializer): class WorkflowSerializer(ModelSerializer):
""" """
@ -369,7 +394,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_access_level = SerializerMethodField() state_restriction = 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)
@ -404,7 +429,7 @@ class MotionSerializer(ModelSerializer):
"supporters", "supporters",
"state", "state",
"state_extension", "state_extension",
"state_access_level", "state_restriction",
"statute_paragraph", "statute_paragraph",
"workflow_id", "workflow_id",
"recommendation", "recommendation",
@ -509,9 +534,9 @@ class MotionSerializer(ModelSerializer):
return result return result
def get_state_access_level(self, motion): def get_state_restriction(self, motion):
""" """
Returns the access level of this state. The default is 0 so everybody Returns the restriction of this state. The default is an empty list so everybody
with permission to see motions can see this motion. with permission to see motions can see this motion.
""" """
return motion.state.access_level return motion.state.restriction

View File

@ -477,11 +477,11 @@ class RetrieveMotion(TestCase):
username=f"user_{index}", password="password" username=f"user_{index}", password="password"
) )
def test_guest_state_with_access_level(self): def test_guest_state_with_restriction(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.access_level = State.MANAGERS_ONLY state.restriction = ["managers_only"]
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
@ -490,17 +490,16 @@ 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_access_level(self): def test_admin_state_with_restriction(self):
state = self.motion.state state = self.motion.state
state.access_level = State.MANAGERS_ONLY state.restriction = ["managers_only"]
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_access_level(self): def test_submitter_state_with_restriction(self):
state = self.motion.state state = self.motion.state
state.access_level = State.EXTENDED_MANAGERS_AND_SUBMITTER state.restriction = ["is_submitter"]
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_access_level": 0, "state_restriction": [],
"statute_paragraph_id": None, "statute_paragraph_id": None,
"workflow_id": 1, "workflow_id": 1,
"recommendation_id": None, "recommendation_id": None,
@ -118,7 +118,7 @@ def all_data():
"supporters_id": [], "supporters_id": [],
"state_id": 1, "state_id": 1,
"state_extension": None, "state_extension": None,
"state_access_level": 0, "state_restriction": [],
"statute_paragraph_id": None, "statute_paragraph_id": None,
"workflow_id": 1, "workflow_id": 1,
"recommendation_id": None, "recommendation_id": None,
@ -151,7 +151,7 @@ def all_data():
"supporters_id": [], "supporters_id": [],
"state_id": 1, "state_id": 1,
"state_extension": None, "state_extension": None,
"state_access_level": 0, "state_restriction": [],
"statute_paragraph_id": 1, "statute_paragraph_id": 1,
"workflow_id": 1, "workflow_id": 1,
"recommendation_id": None, "recommendation_id": None,
@ -178,7 +178,7 @@ def all_data():
"name": "submitted", "name": "submitted",
"recommendation_label": None, "recommendation_label": None,
"css_class": "primary", "css_class": "primary",
"access_level": 0, "restriction": [],
"allow_support": True, "allow_support": True,
"allow_create_poll": True, "allow_create_poll": True,
"allow_submitter_edit": True, "allow_submitter_edit": True,
@ -194,7 +194,7 @@ def all_data():
"name": "accepted", "name": "accepted",
"recommendation_label": "Acceptance", "recommendation_label": "Acceptance",
"css_class": "success", "css_class": "success",
"access_level": 0, "restriction": [],
"allow_support": False, "allow_support": False,
"allow_create_poll": False, "allow_create_poll": False,
"allow_submitter_edit": False, "allow_submitter_edit": False,
@ -210,7 +210,7 @@ def all_data():
"name": "rejected", "name": "rejected",
"recommendation_label": "Rejection", "recommendation_label": "Rejection",
"css_class": "danger", "css_class": "danger",
"access_level": 0, "restriction": [],
"allow_support": False, "allow_support": False,
"allow_create_poll": False, "allow_create_poll": False,
"allow_submitter_edit": False, "allow_submitter_edit": False,
@ -226,7 +226,7 @@ def all_data():
"name": "not decided", "name": "not decided",
"recommendation_label": "No decision", "recommendation_label": "No decision",
"css_class": "default", "css_class": "default",
"access_level": 0, "restriction": [],
"allow_support": False, "allow_support": False,
"allow_create_poll": False, "allow_create_poll": False,
"allow_submitter_edit": False, "allow_submitter_edit": False,