diff --git a/docs/actions/chat_group.create.md b/docs/actions/chat_group.create.md index 263f6a1bc..e3d170a47 100644 --- a/docs/actions/chat_group.create.md +++ b/docs/actions/chat_group.create.md @@ -15,5 +15,7 @@ Creates a new chat group in the given meeting. Only enabled, if `organization/enable_chat` **and** `meeting/enable_chat` is true. The `weight` must be set to `max(weight)+1` of all chat groups of the meeting. The name of a chat group is unique. +The `write_group_ids` may not contain the meetings `anonymous_group_id`. + ## Permissions The request user needs `chat.can_manage`. diff --git a/docs/actions/chat_group.update.md b/docs/actions/chat_group.update.md index 6a36f6714..25a364ed8 100644 --- a/docs/actions/chat_group.update.md +++ b/docs/actions/chat_group.update.md @@ -14,5 +14,7 @@ ## Action Updates the chat group. Only enabled, if `organization/enable_chat` **and** `meeting/enable_chat` is true. The name of a chat group is unique. +The `write_group_ids` may not contain the meetings `anonymous_group_id`. + ## Permissions The request user needs `chat.can_manage`. diff --git a/docs/actions/group.delete.md b/docs/actions/group.delete.md index 94b1a6c69..147da7bde 100644 --- a/docs/actions/group.delete.md +++ b/docs/actions/group.delete.md @@ -7,7 +7,7 @@ ``` ## Action -Deletes the group. If the group has users or `default_group_for_meeting_id` or `admin_group_for_meeting_id` set, the deletion is not allowed. +Deletes the group. If the group has users or `default_group_for_meeting_id`, `anonymous_group_for_meeting_id` or `admin_group_for_meeting_id` set, the deletion is not allowed. ## Permissions The user needs `user.can_manage`. diff --git a/docs/actions/group.update.md b/docs/actions/group.update.md index cd95620f5..75b52e7e6 100644 --- a/docs/actions/group.update.md +++ b/docs/actions/group.update.md @@ -14,5 +14,22 @@ ## Action Updates the group. Permissions are restricted to the following enum: https://github.com/OpenSlides/openslides-backend/blob/fae36a0b055bbaa463da4768343080c285fe8178/global/meta/models.yml#L1621-L1656 +If the group is the meetings anonymous group, the name may not be changed and the permissions have to be in the following whitelist: +- agenda_item.can_see, +- agenda_item.can_see_internal, +- agenda_item.can_see_moderator_notes, +- assignment.can_see, +- list_of_speakers.can_see, +- mediafile.can_see, +- meeting.can_see_autopilot, +- meeting.can_see_frontpage, +- meeting.can_see_history, +- meeting.can_see_livestream, +- motion.can_see, +- motion.can_see_internal, +- projector.can_see, +- user.can_see, +- user.can_see_sensitive_data + ## Permissions The user needs `user.can_manage` to change `name` and `permission`, for `external_id` meeting admin rights are mandatory. diff --git a/docs/actions/meeting.update.md b/docs/actions/meeting.update.md index 3208a5710..1e8562bff 100644 --- a/docs/actions/meeting.update.md +++ b/docs/actions/meeting.update.md @@ -195,7 +195,11 @@ Updates the meeting. If `set_as_template` is `True`, `template_for_organization_id` has to be set to `1`. If it is `False`, it has to be set to `None`. `reference_projector_id` can only be set to a projector, which is not internal. -This action doesn't allow for a meeting to be set as a template and have `locked_from_inside` set to true at the same time. if this would be the result of an action call, an exception will be thrown. +This action doesn't allow for a meeting to be set as a template and have `locked_from_inside` set to true at the same time. if this would be the result of an action call, an exception will be thrown. Same for `enable_anonymous` and `locked_from_inside` being true at the same time + +If `enable_anonymous` is set, this action will create an anonymous group for the meeting. This will have the name `Anonymous` and otherwise differ from the other groups in the meeting due to having `anonymous_group_for_meeting_id` set. + +The meetings `anonymous_group_id` may not be used for the `assignment_poll_default_group_ids`, `topic_poll_default_group_ids` and `motion_poll_default_group_ids` fields. ## Permissions - Users with `meeting.can_manage_settings` can modify group A diff --git a/docs/actions/meeting_user.create.md b/docs/actions/meeting_user.create.md index 622704dc4..e836d91e4 100644 --- a/docs/actions/meeting_user.create.md +++ b/docs/actions/meeting_user.create.md @@ -34,6 +34,8 @@ The action creates a meeting_user item. `vote_delegated_to_id` and `vote_delegations_from_ids` have special checks, see user checks. If `locked_out` is set, it checks against the present `user.can_manage` and all admin statuses and throws an error if any are present. +Will throw an error if the `group_ids` contain the meetings `anonymous_group_id`. + ## Permissions Group A: The request user needs `user.can_manage`. diff --git a/docs/actions/meeting_user.update.md b/docs/actions/meeting_user.update.md index ccd1dd0d7..91999659c 100644 --- a/docs/actions/meeting_user.update.md +++ b/docs/actions/meeting_user.update.md @@ -38,4 +38,7 @@ ``` ## Internal action Updates a meeting_user. `vote_delegated_to_id` and `vote_delegations_from_ids` has special checks, see user checks. -The action checks, whether at the end the field `locked_out` will be set together with any of `user.can_manage` or any admin statuses on the updated meeting_user and throws an error if that is the case. \ No newline at end of file + +Will throw an error if the `group_ids` contain the meetings `anonymous_group_id`. + +The action checks, whether at the end the field `locked_out` will be set together with any of `user.can_manage` or any admin statuses on the updated meeting_user and throws an error if that is the case. diff --git a/docs/actions/motion_comment_section.create.md b/docs/actions/motion_comment_section.create.md index 30437d8b9..3656f9597 100644 --- a/docs/actions/motion_comment_section.create.md +++ b/docs/actions/motion_comment_section.create.md @@ -15,5 +15,7 @@ ## Action Creates a new comment section. The `weight` must be set to `max+1` of all comment sections of the meeting. The given groups must belong to the same meeting. +The `write_group_ids` may not contain the meetings `anonymous_group_id`. + ## Permissions The request user needs `motion.can_manage`. diff --git a/docs/actions/motion_comment_section.update.md b/docs/actions/motion_comment_section.update.md index 0d90d7d08..03fada15a 100644 --- a/docs/actions/motion_comment_section.update.md +++ b/docs/actions/motion_comment_section.update.md @@ -16,5 +16,7 @@ ## Action Updates the comment section. The given groups must belong to the same meeting. +The `write_group_ids` may not contain the meetings `anonymous_group_id`. + ## Permissions The request user needs `motion.can_manage`. diff --git a/docs/actions/participant.json_upload.md b/docs/actions/participant.json_upload.md index 84e7e26bf..ebdba051e 100644 --- a/docs/actions/participant.json_upload.md +++ b/docs/actions/participant.json_upload.md @@ -62,7 +62,7 @@ Same as in [account.json_upload#user-matching](account.json_upload.md#user-match This action is the first part of the actions for the import of participants (mean: users in a meeting). It should use the `JsonUploadMixin` and is a single payload action. -The `groups` field includes a list of group names. The group names will be looked up in the meeting. +The `groups` field includes a list of group names. The group names will be looked up among the groups in the meeting, with the exception of the meetings anonymous group, which will be ignored. If a group is found, info will be *done* and id is the id of the group. If no group is found, info will be *warning*. If no group in groups is found at all, the entry state will be *error* and import shouldn't be possible. diff --git a/docs/actions/poll.create.md b/docs/actions/poll.create.md index 5521f1689..04e57fd35 100644 --- a/docs/actions/poll.create.md +++ b/docs/actions/poll.create.md @@ -62,6 +62,8 @@ If the `type` is `pseudoanonymous`, `is_pseudoanonymized` has to be set to `true If the `content_object_id` points to a `motion` and the `motion_state` of the motion misses `allow_create_poll`, it is forbidden to create a poll. +The `entitled_group_ids` may not contain the meetings `anonymous_group_id`. + ## Permissions The request user needs: - `motion.can_manage_polls` if the poll's content object is a motion diff --git a/docs/actions/poll.update.md b/docs/actions/poll.update.md index 965440b02..c880c442d 100644 --- a/docs/actions/poll.update.md +++ b/docs/actions/poll.update.md @@ -37,6 +37,8 @@ For analog polls: If the state is created and at least one vote value is given ( For electronic polls some fields can only be updated, if the state is *created*. +The `entitled_group_ids` may not contain the meetings `anonymous_group_id`. + ## Permissions The request user needs: - `motion.can_manage_polls` if the poll's content object is a motion diff --git a/docs/actions/user.create.md b/docs/actions/user.create.md index e91274070..a9a7d4039 100644 --- a/docs/actions/user.create.md +++ b/docs/actions/user.create.md @@ -60,6 +60,7 @@ Creates a user. * The given `gender` must be present in `organization/genders` * If `saml_id` is set in payload, there may be no `password` or `default_password` set or generated and `set_change_own_password` will be set to False. * The `member_number` must be unique within all users. +* Will throw an error if the `group_ids` contain the meetings `anonymous_group_id`. * The action checks, whether at the end the field `locked_out` will be set together with any of `user.can_manage` or any admin statuses on the created user and throws an error if that is the case. ### Generate a username diff --git a/docs/actions/user.update.md b/docs/actions/user.update.md index aaf8d3fee..50a12d29e 100644 --- a/docs/actions/user.update.md +++ b/docs/actions/user.update.md @@ -68,6 +68,7 @@ Updates a user. * Remove starting and trailing spaces from `username`, `first_name` and `last_name` * The given `gender` must be present in `organization/genders` * The `member_number` must be unique within all users. +* Will throw an error if the `group_ids` contain the meetings `anonymous_group_id`. * The action checks, whether at the end the field `locked_out` will be set together with any of `user.can_manage` or any admin statuses on the updated user and throws an error if that is the case. Note: `is_present_in_meeting_ids` is not available in update, since there is no possibility to partially update this field. This can be done via [user.set_present](user.set_present.md). diff --git a/global/meta b/global/meta index b27314048..5921875b0 160000 --- a/global/meta +++ b/global/meta @@ -1 +1 @@ -Subproject commit b273140488acc6c397b38e1b02dd8f9e4f40338d +Subproject commit 5921875b0b4759f9b4acff2efa7af67b3ce4236c diff --git a/openslides_backend/action/actions/chat_group/create.py b/openslides_backend/action/actions/chat_group/create.py index 20a8b15b2..9884321ad 100644 --- a/openslides_backend/action/actions/chat_group/create.py +++ b/openslides_backend/action/actions/chat_group/create.py @@ -3,6 +3,7 @@ from ....models.models import ChatGroup from ....permissions.permissions import Permissions from ...generics.create import CreateAction +from ...mixins.forbid_anonymous_group_mixin import ForbidAnonymousGroupMixin from ...mixins.weight_mixin import WeightMixin from ...util.default_schema import DefaultSchema from ...util.register import register_action @@ -11,7 +12,11 @@ @register_action("chat_group.create") class ChatGroupCreate( - WeightMixin, ChatEnabledMixin, CheckUniqueNameMixin, CreateAction + WeightMixin, + ChatEnabledMixin, + CheckUniqueNameMixin, + CreateAction, + ForbidAnonymousGroupMixin, ): """ Action to create a chat group. @@ -28,4 +33,5 @@ def update_instance(self, instance: dict[str, Any]) -> dict[str, Any]: instance = super().update_instance(instance) self.check_name_unique(instance) instance["weight"] = self.get_weight(instance["meeting_id"]) + self.check_anonymous_not_in_list_fields(instance, ["write_group_ids"]) return instance diff --git a/openslides_backend/action/actions/chat_group/update.py b/openslides_backend/action/actions/chat_group/update.py index d30e08471..410b6a95e 100644 --- a/openslides_backend/action/actions/chat_group/update.py +++ b/openslides_backend/action/actions/chat_group/update.py @@ -4,13 +4,16 @@ from ....permissions.permissions import Permissions from ....shared.patterns import fqid_from_collection_and_id from ...generics.update import UpdateAction +from ...mixins.forbid_anonymous_group_mixin import ForbidAnonymousGroupMixin from ...util.default_schema import DefaultSchema from ...util.register import register_action from .mixins import ChatEnabledMixin, CheckUniqueNameMixin @register_action("chat_group.update") -class ChatGroupUpdate(ChatEnabledMixin, CheckUniqueNameMixin, UpdateAction): +class ChatGroupUpdate( + ChatEnabledMixin, CheckUniqueNameMixin, UpdateAction, ForbidAnonymousGroupMixin +): """ Action to update a chat group. """ @@ -31,4 +34,5 @@ def update_instance(self, instance: dict[str, Any]) -> dict[str, Any]: ) if instance["name"] != chat_group.get("name"): self.check_name_unique(instance) + self.check_anonymous_not_in_list_fields(instance, ["write_group_ids"]) return instance diff --git a/openslides_backend/action/actions/group/update.py b/openslides_backend/action/actions/group/update.py index e4c12517c..8f394757b 100644 --- a/openslides_backend/action/actions/group/update.py +++ b/openslides_backend/action/actions/group/update.py @@ -1,10 +1,15 @@ from typing import Any -from openslides_backend.shared.exceptions import PermissionDenied +from openslides_backend.shared.exceptions import ActionException, PermissionDenied from ....models.models import Group -from ....permissions.permission_helper import filter_surplus_permissions, is_admin +from ....permissions.permission_helper import ( + check_if_perms_are_allowed_for_anonymous, + filter_surplus_permissions, + is_admin, +) from ....permissions.permissions import Permissions +from ....shared.patterns import fqid_from_collection_and_id from ...generics.update import UpdateAction from ...util.default_schema import DefaultSchema from ...util.register import register_action @@ -28,6 +33,14 @@ def update_instance(self, instance: dict[str, Any]) -> dict[str, Any]: instance["permissions"] = filter_surplus_permissions( instance["permissions"] ) + if self.datastore.get( + fqid_from_collection_and_id("group", instance["id"]), + ["anonymous_group_for_meeting_id"], + ).get("anonymous_group_for_meeting_id"): + if perms := instance.get("permissions", []): + check_if_perms_are_allowed_for_anonymous(perms) + if "name" in instance: + raise ActionException("Cannot change name of anonymous group.") return instance def check_permissions(self, instance: dict[str, Any]) -> None: diff --git a/openslides_backend/action/actions/meeting/update.py b/openslides_backend/action/actions/meeting/update.py index e4ef55ece..cfcc24dba 100644 --- a/openslides_backend/action/actions/meeting/update.py +++ b/openslides_backend/action/actions/meeting/update.py @@ -1,4 +1,4 @@ -from typing import Any +from typing import Any, cast from openslides_backend.action.mixins.check_unique_name_mixin import ( CheckUniqueInContextMixin, @@ -20,10 +20,12 @@ from ....shared.patterns import fqid_from_collection_and_id from ....shared.util import ONE_ORGANIZATION_FQID from ...generics.update import UpdateAction +from ...mixins.forbid_anonymous_group_mixin import ForbidAnonymousGroupMixin from ...mixins.send_email_mixin import EmailCheckMixin, EmailSenderCheckMixin from ...util.assert_belongs_to_meeting import assert_belongs_to_meeting from ...util.default_schema import DefaultSchema from ...util.register import register_action +from ..group.create import GroupCreate from .mixins import GetMeetingIdFromIdMixin, MeetingCheckTimesMixin meeting_settings_keys = [ @@ -173,6 +175,7 @@ class MeetingUpdate( UpdateAction, GetMeetingIdFromIdMixin, MeetingCheckTimesMixin, + ForbidAnonymousGroupMixin, ): model = Meeting() schema = DefaultSchema(Meeting()).get_update_schema( @@ -210,24 +213,7 @@ def validate_instance(self, instance: dict[str, Any]) -> None: def update_instance(self, instance: dict[str, Any]) -> dict[str, Any]: # handle set_as_template set_as_template = instance.pop("set_as_template", None) - db_meeting = self.datastore.get( - fqid_from_collection_and_id("meeting", instance["id"]), - ["template_for_organization_id", "locked_from_inside"], - lock_result=False, - ) - lock_meeting = ( - instance.get("locked_from_inside") - if instance.get("locked_from_inside") is not None - else db_meeting.get("locked_from_inside") - ) - if lock_meeting and ( - set_as_template - if set_as_template is not None - else db_meeting.get("template_for_organization_id") - ): - raise ActionException( - "A meeting cannot be locked from the inside and a template at the same time." - ) + self.check_locking(instance, set_as_template) organization = self.datastore.get( ONE_ORGANIZATION_FQID, ["require_duplicate_from"], lock_result=False ) @@ -283,6 +269,35 @@ def update_instance(self, instance: dict[str, Any]) -> dict[str, Any]: raise ActionException("It is not allowed to end jitsi_domain with '/'.") self.check_start_and_end_time(instance) + + anonymous_group_id = self.datastore.get( + fqid_from_collection_and_id("meeting", instance["id"]), + ["anonymous_group_id"], + ).get("anonymous_group_id") + + if instance.get("enable_anonymous") and not anonymous_group_id: + group_result = self.execute_other_action( + GroupCreate, + [ + { + "name": "Anonymous", + "meeting_id": instance["id"], + } + ], + ) + instance["anonymous_group_id"] = anonymous_group_id = cast( + list[dict[str, Any]], group_result + )[0]["id"] + self.check_anonymous_not_in_list_fields( + instance, + [ + "assignment_poll_default_group_ids", + "topic_poll_default_group_ids", + "motion_poll_default_group_ids", + ], + anonymous_group_id, + ) + instance = super().update_instance(instance) return instance @@ -366,3 +381,32 @@ def get_committee_id(self, meeting_id: int) -> int: lock_result=False, )["committee_id"] return self._committee_id + + def check_locking(self, instance: dict[str, Any], set_as_template: bool) -> None: + db_meeting = self.datastore.get( + fqid_from_collection_and_id("meeting", instance["id"]), + ["template_for_organization_id", "locked_from_inside", "enable_anonymous"], + lock_result=False, + ) + lock_meeting = ( + instance.get("locked_from_inside") + if instance.get("locked_from_inside") is not None + else db_meeting.get("locked_from_inside") + ) + if lock_meeting: + if ( + set_as_template + if set_as_template is not None + else db_meeting.get("template_for_organization_id") + ): + raise ActionException( + "A meeting cannot be locked from the inside and a template at the same time." + ) + if ( + instance.get("enable_anonymous") + if instance.get("enable_anonymous") is not None + else db_meeting.get("enable_anonymous") + ): + raise ActionException( + "A meeting cannot be locked from the inside and have anonymous enabled at the same time." + ) diff --git a/openslides_backend/action/actions/meeting_user/create.py b/openslides_backend/action/actions/meeting_user/create.py index 79797226f..929cfc408 100644 --- a/openslides_backend/action/actions/meeting_user/create.py +++ b/openslides_backend/action/actions/meeting_user/create.py @@ -11,12 +11,19 @@ from ...util.default_schema import DefaultSchema from ...util.register import register_action from .history_mixin import MeetingUserHistoryMixin -from .mixin import CheckLockOutPermissionMixin, meeting_user_standard_fields +from .mixin import ( + CheckLockOutPermissionMixin, + MeetingUserGroupMixin, + meeting_user_standard_fields, +) @register_action("meeting_user.create", action_type=ActionType.BACKEND_INTERNAL) class MeetingUserCreate( - MeetingUserHistoryMixin, CreateAction, CheckLockOutPermissionMixin + MeetingUserHistoryMixin, + CreateAction, + MeetingUserGroupMixin, + CheckLockOutPermissionMixin, ): """ Action to create a meeting user. diff --git a/openslides_backend/action/actions/meeting_user/mixin.py b/openslides_backend/action/actions/meeting_user/mixin.py index bc64fdbb3..841a902ae 100644 --- a/openslides_backend/action/actions/meeting_user/mixin.py +++ b/openslides_backend/action/actions/meeting_user/mixin.py @@ -2,7 +2,6 @@ from openslides_backend.services.datastore.commands import GetManyRequest -from ....action.action import Action from ....action.mixins.meeting_user_helper import get_meeting_user from ....action.util.typing import ActionData, ActionResults from ....permissions.permissions import Permissions @@ -10,6 +9,7 @@ from ....shared.filters import And, Filter, FilterOperator, Or from ....shared.interfaces.write_request import WriteRequest from ....shared.patterns import fqid_from_collection_and_id +from ...action import Action from .history_mixin import MeetingUserHistoryMixin meeting_user_standard_fields = [ @@ -21,6 +21,21 @@ ] +class MeetingUserGroupMixin(Action): + def update_instance(self, instance: dict[str, Any]) -> dict[str, Any]: + if len(group_ids := instance.get("group_ids", [])) and self.datastore.exists( + "group", + And( + FilterOperator("anonymous_group_for_meeting_id", "!=", None), + Or(FilterOperator("id", "=", id_) for id_ in group_ids), + ), + ): + raise ActionException( + "Cannot add explicit users to a meetings anonymous group" + ) + return super().update_instance(instance) + + LockingStatusCheckResult = tuple[str, list[int] | None] # message to broken group ids diff --git a/openslides_backend/action/actions/meeting_user/set_data.py b/openslides_backend/action/actions/meeting_user/set_data.py index 13304c67e..f422d8f42 100644 --- a/openslides_backend/action/actions/meeting_user/set_data.py +++ b/openslides_backend/action/actions/meeting_user/set_data.py @@ -9,7 +9,7 @@ from ...util.default_schema import DefaultSchema from ...util.register import register_action from .helper_mixin import MeetingUserHelperMixin -from .mixin import MeetingUserMixin +from .mixin import MeetingUserGroupMixin, MeetingUserMixin @register_action("meeting_user.set_data", action_type=ActionType.BACKEND_INTERNAL) @@ -18,6 +18,7 @@ class MeetingUserSetData( ExtendHistoryMixin, MeetingUserHelperMixin, UpdateAction, + MeetingUserGroupMixin, ): """ Action to create, update or delete a meeting_user. diff --git a/openslides_backend/action/actions/meeting_user/update.py b/openslides_backend/action/actions/meeting_user/update.py index b269a3ef5..3d160b47d 100644 --- a/openslides_backend/action/actions/meeting_user/update.py +++ b/openslides_backend/action/actions/meeting_user/update.py @@ -9,15 +9,20 @@ from ...util.default_schema import DefaultSchema from ...util.register import register_action from .history_mixin import MeetingUserHistoryMixin -from .mixin import CheckLockOutPermissionMixin, meeting_user_standard_fields +from .mixin import ( + CheckLockOutPermissionMixin, + MeetingUserGroupMixin, + meeting_user_standard_fields, +) @register_action("meeting_user.update", action_type=ActionType.BACKEND_INTERNAL) class MeetingUserUpdate( MeetingUserHistoryMixin, UpdateAction, - ExtendHistoryMixin, + MeetingUserGroupMixin, CheckLockOutPermissionMixin, + ExtendHistoryMixin, ): """ Action to update a meeting_user. diff --git a/openslides_backend/action/actions/motion_comment_section/create.py b/openslides_backend/action/actions/motion_comment_section/create.py index 0d2ffa78f..b65827552 100644 --- a/openslides_backend/action/actions/motion_comment_section/create.py +++ b/openslides_backend/action/actions/motion_comment_section/create.py @@ -1,13 +1,18 @@ +from typing import Any + from ....models.models import MotionCommentSection from ....permissions.permissions import Permissions from ...generics.create import CreateAction +from ...mixins.forbid_anonymous_group_mixin import ForbidAnonymousGroupMixin from ...mixins.sequential_numbers_mixin import SequentialNumbersMixin from ...util.default_schema import DefaultSchema from ...util.register import register_action @register_action("motion_comment_section.create") -class MotionCommentSectionCreateAction(SequentialNumbersMixin, CreateAction): +class MotionCommentSectionCreateAction( + SequentialNumbersMixin, CreateAction, ForbidAnonymousGroupMixin +): """ Create Action with default weight. """ @@ -22,3 +27,7 @@ class MotionCommentSectionCreateAction(SequentialNumbersMixin, CreateAction): ], ) permission = Permissions.Motion.CAN_MANAGE + + def update_instance(self, instance: dict[str, Any]) -> dict[str, Any]: + self.check_anonymous_not_in_list_fields(instance, ["write_group_ids"]) + return super().update_instance(instance) diff --git a/openslides_backend/action/actions/motion_comment_section/update.py b/openslides_backend/action/actions/motion_comment_section/update.py index 53d5cb889..322d4843f 100644 --- a/openslides_backend/action/actions/motion_comment_section/update.py +++ b/openslides_backend/action/actions/motion_comment_section/update.py @@ -1,12 +1,15 @@ +from typing import Any + from ....models.models import MotionCommentSection from ....permissions.permissions import Permissions from ...generics.update import UpdateAction +from ...mixins.forbid_anonymous_group_mixin import ForbidAnonymousGroupMixin from ...util.default_schema import DefaultSchema from ...util.register import register_action @register_action("motion_comment_section.update") -class MotionCommentSectionUpdateAction(UpdateAction): +class MotionCommentSectionUpdateAction(UpdateAction, ForbidAnonymousGroupMixin): """ Action to update motion comment sections. """ @@ -21,3 +24,7 @@ class MotionCommentSectionUpdateAction(UpdateAction): ] ) permission = Permissions.Motion.CAN_MANAGE + + def update_instance(self, instance: dict[str, Any]) -> dict[str, Any]: + self.check_anonymous_not_in_list_fields(instance, ["write_group_ids"]) + return super().update_instance(instance) diff --git a/openslides_backend/action/actions/poll/create.py b/openslides_backend/action/actions/poll/create.py index f8114be95..7e7eeb4b4 100644 --- a/openslides_backend/action/actions/poll/create.py +++ b/openslides_backend/action/actions/poll/create.py @@ -7,6 +7,7 @@ from ....shared.patterns import collection_from_fqid, fqid_from_collection_and_id from ....shared.schema import decimal_schema, id_list_schema, optional_fqid_schema from ...generics.create import CreateAction +from ...mixins.forbid_anonymous_group_mixin import ForbidAnonymousGroupMixin from ...mixins.sequential_numbers_mixin import SequentialNumbersMixin from ...util.default_schema import DefaultSchema from ...util.register import register_action @@ -31,7 +32,11 @@ @register_action("poll.create") class PollCreateAction( - SequentialNumbersMixin, CreateAction, PollPermissionMixin, PollHistoryMixin + SequentialNumbersMixin, + CreateAction, + PollPermissionMixin, + PollHistoryMixin, + ForbidAnonymousGroupMixin, ): """ Action to create a poll. @@ -201,6 +206,7 @@ def update_instance(self, instance: dict[str, Any]) -> dict[str, Any]: instance.pop("options", None) instance.pop("publish_immediately", None) + self.check_anonymous_not_in_list_fields(instance, ["entitled_group_ids"]) return instance def parse_vote_value(self, data: dict[str, Any], field: str) -> Any: diff --git a/openslides_backend/action/actions/poll/update.py b/openslides_backend/action/actions/poll/update.py index 9ac93f6ff..00573b2ad 100644 --- a/openslides_backend/action/actions/poll/update.py +++ b/openslides_backend/action/actions/poll/update.py @@ -7,6 +7,7 @@ from ....shared.exceptions import ActionException from ....shared.patterns import fqid_from_collection_and_id from ...generics.update import UpdateAction +from ...mixins.forbid_anonymous_group_mixin import ForbidAnonymousGroupMixin from ...util.default_schema import DefaultSchema from ...util.register import register_action from .base import base_check_onehundred_percent_base @@ -15,7 +16,11 @@ @register_action("poll.update") class PollUpdateAction( - ExtendHistoryMixin, UpdateAction, PollPermissionMixin, PollHistoryMixin + ExtendHistoryMixin, + UpdateAction, + PollPermissionMixin, + PollHistoryMixin, + ForbidAnonymousGroupMixin, ): """ Action to update a poll. @@ -136,6 +141,7 @@ def update_instance(self, instance: dict[str, Any]) -> dict[str, Any]: instance[field] = "-2.000000" instance.pop("publish_immediately", None) + self.check_anonymous_not_in_list_fields(instance, ["entitled_group_ids"]) return instance def check_onehundred_percent_base(self, instance: dict[str, Any]) -> None: diff --git a/openslides_backend/action/actions/user/assign_meetings.py b/openslides_backend/action/actions/user/assign_meetings.py index 344890d35..9a61815f9 100644 --- a/openslides_backend/action/actions/user/assign_meetings.py +++ b/openslides_backend/action/actions/user/assign_meetings.py @@ -58,8 +58,11 @@ def update_instance(self, instance: dict[str, Any]) -> dict[str, Any]: filter_ = And( FilterOperator("name", "=", group_name), FilterOperator("meeting_id", "=", meeting_id), + FilterOperator("anonymous_group_for_meeting_id", "=", None), + ) + groups = self.datastore.filter( + "group", filter_, ["meeting_id", "meeting_user_ids"] ) - groups = self.datastore.filter("group", filter_, ["meeting_id", "user_ids"]) groups_meeting_ids.update( {group["meeting_id"] for group in groups.values()} ) diff --git a/openslides_backend/action/actions/user/participant_import.py b/openslides_backend/action/actions/user/participant_import.py index b88ecf206..a8b6a1a13 100644 --- a/openslides_backend/action/actions/user/participant_import.py +++ b/openslides_backend/action/actions/user/participant_import.py @@ -43,6 +43,11 @@ def update_models_to_create(self, model_name: str, field_name: str) -> None: model_name, And( FilterOperator("meeting_id", "=", self.meeting_id), + *( + [FilterOperator("anonymous_group_for_meeting_id", "=", None)] + if model_name == "group" + else [] + ), Or([FilterOperator("name", "=", name) for name in to_create]), ), ["name", "id"], diff --git a/openslides_backend/action/actions/user/participant_json_upload.py b/openslides_backend/action/actions/user/participant_json_upload.py index 3dc38c713..e3c9336e3 100644 --- a/openslides_backend/action/actions/user/participant_json_upload.py +++ b/openslides_backend/action/actions/user/participant_json_upload.py @@ -205,7 +205,12 @@ def setup_lookups(self, data: list[dict[str, Any]]) -> None: GetManyRequest( "group", meeting.get("group_ids", []), - ["name", "id", "default_group_for_meeting_id"], + [ + "name", + "id", + "default_group_for_meeting_id", + "anonymous_group_for_meeting_id", + ], ), GetManyRequest( "structure_level", @@ -214,6 +219,11 @@ def setup_lookups(self, data: list[dict[str, Any]]) -> None: ), ] ) + result["group"] = { + id_: group + for id_, group in result["group"].items() + if not group.get("anonymous_group_for_meeting_id") + } for collection in ("group", "structure_level"): self.lookups[collection] = self.create_lookup(result[collection].values()) for group in result["group"].values(): diff --git a/openslides_backend/action/mixins/forbid_anonymous_group_mixin.py b/openslides_backend/action/mixins/forbid_anonymous_group_mixin.py new file mode 100644 index 000000000..5e350b5e9 --- /dev/null +++ b/openslides_backend/action/mixins/forbid_anonymous_group_mixin.py @@ -0,0 +1,23 @@ +from typing import Any + +from ...shared.exceptions import ActionException +from ...shared.patterns import fqid_from_collection_and_id +from ..action import Action + + +class ForbidAnonymousGroupMixin(Action): + def check_anonymous_not_in_list_fields( + self, + instance: dict[str, Any], + group_list_field_names: list[str], + anonymous_group_id: int | None = None, + ) -> None: + if not anonymous_group_id: + anonymous_group_id = self.datastore.get( + fqid_from_collection_and_id("meeting", self.get_meeting_id(instance)), + ["anonymous_group_id"], + ).get("anonymous_group_id") + if anonymous_group_id: + for field in group_list_field_names: + if anonymous_group_id in instance.get(field, []): + raise ActionException(f"Anonymous group is not allowed in {field}.") diff --git a/openslides_backend/models/models.py b/openslides_backend/models/models.py index 41a5b7198..985b96e86 100644 --- a/openslides_backend/models/models.py +++ b/openslides_backend/models/models.py @@ -904,6 +904,9 @@ class Meeting(Model, MeetingModelMixin): to={"group": "default_group_for_meeting_id"}, required=True ) admin_group_id = fields.RelationField(to={"group": "admin_group_for_meeting_id"}) + anonymous_group_id = fields.RelationField( + to={"group": "anonymous_group_for_meeting_id"} + ) class StructureLevel(Model): @@ -988,6 +991,9 @@ class Group(Model): admin_group_for_meeting_id = fields.RelationField( to={"meeting": "admin_group_id"}, on_delete=fields.OnDelete.PROTECT ) + anonymous_group_for_meeting_id = fields.RelationField( + to={"meeting": "anonymous_group_id"}, on_delete=fields.OnDelete.PROTECT + ) mediafile_access_group_ids = fields.RelationListField( to={"mediafile": "access_group_ids"}, equal_fields="meeting_id" ) diff --git a/openslides_backend/permissions/permission_helper.py b/openslides_backend/permissions/permission_helper.py index 10bab894c..c8b17a439 100644 --- a/openslides_backend/permissions/permission_helper.py +++ b/openslides_backend/permissions/permission_helper.py @@ -5,10 +5,10 @@ from ..services.datastore.commands import GetManyRequest from ..services.datastore.interface import DatastoreService -from ..shared.exceptions import PermissionDenied +from ..shared.exceptions import ActionException, PermissionDenied from ..shared.patterns import fqid_from_collection_and_id from .management_levels import CommitteeManagementLevel, OrganizationManagementLevel -from .permissions import Permission, permission_parents +from .permissions import Permission, Permissions, permission_parents def has_perm( @@ -16,7 +16,7 @@ def has_perm( ) -> bool: meeting = datastore.get( fqid_from_collection_and_id("meeting", meeting_id), - ["default_group_id", "enable_anonymous", "locked_from_inside"], + ["anonymous_group_id", "enable_anonymous", "locked_from_inside"], lock_result=False, ) not_locked_from_editing = not meeting.get("locked_from_inside") @@ -49,11 +49,14 @@ def has_perm( if not group_ids: return False elif user_id == 0: - # anonymous users are in the default group + # anonymous users are in the anonymous group # check if anonymous is allowed if not meeting.get("enable_anonymous"): raise PermissionDenied(f"Anonymous is not enabled for meeting {meeting_id}") - group_ids = [meeting["default_group_id"]] + if anonymous_group_id := meeting.get("anonymous_group_id"): + group_ids = [anonymous_group_id] + else: + return False else: return False @@ -185,3 +188,29 @@ def is_admin(datastore: DatastoreService, user_id: int, meeting_id: int) -> bool group_ids = get_groups_from_meeting_user(datastore, meeting_id, user_id) return bool(group_ids) and meeting["admin_group_id"] in group_ids + + +anonymous_perms_whitelist: set[Permission] = { + Permissions.AgendaItem.CAN_SEE, + Permissions.AgendaItem.CAN_SEE_INTERNAL, + Permissions.AgendaItem.CAN_SEE_MODERATOR_NOTES, + Permissions.Assignment.CAN_SEE, + Permissions.ListOfSpeakers.CAN_SEE, + Permissions.Mediafile.CAN_SEE, + Permissions.Meeting.CAN_SEE_AUTOPILOT, + Permissions.Meeting.CAN_SEE_FRONTPAGE, + Permissions.Meeting.CAN_SEE_HISTORY, + Permissions.Meeting.CAN_SEE_LIVESTREAM, + Permissions.Motion.CAN_SEE, + Permissions.Motion.CAN_SEE_INTERNAL, + Permissions.Projector.CAN_SEE, + Permissions.User.CAN_SEE, + Permissions.User.CAN_SEE_SENSITIVE_DATA, +} + + +def check_if_perms_are_allowed_for_anonymous(permissions: list[Permission]) -> None: + if len(forbidden := set(permissions).difference(anonymous_perms_whitelist)): + raise ActionException( + f"The following permissions may not be set for the anonymous group: {forbidden}" + ) diff --git a/tests/system/action/base.py b/tests/system/action/base.py index 2f1a4ccb0..90a638d7d 100644 --- a/tests/system/action/base.py +++ b/tests/system/action/base.py @@ -216,8 +216,31 @@ def create_meeting(self, base: int = 1) -> None: } ) - def set_anonymous(self, enable: bool = True, meeting_id: int = 1) -> None: - self.set_models({f"meeting/{meeting_id}": {"enable_anonymous": enable}}) + def set_anonymous( + self, + enable: bool = True, + meeting_id: int = 1, + permissions: list[Permission] = [], + ) -> int: + """Also creates an anonymous group at the next-highest free group_id""" + next_group_id = self.datastore.reserve_id("group") + group_ids = self.get_model(f"meeting/{meeting_id}").get("group_ids", []) + self.set_models( + { + f"meeting/{meeting_id}": { + "enable_anonymous": enable, + "group_ids": [*group_ids, next_group_id], + "anonymous_group_id": next_group_id, + }, + f"group/{next_group_id}": { + "name": "Anonymous", + "meeting_id": meeting_id, + "anonymous_group_for_meeting_id": meeting_id, + "permissions": permissions, + }, + } + ) + return next_group_id def set_organization_management_level( self, level: OrganizationManagementLevel | None, user_id: int = 1 diff --git a/tests/system/action/chat_group/test_create.py b/tests/system/action/chat_group/test_create.py index 3d1de4b00..4d184b640 100644 --- a/tests/system/action/chat_group/test_create.py +++ b/tests/system/action/chat_group/test_create.py @@ -177,3 +177,58 @@ def test_create_same_name_in_two_meetings(self) -> None: ) self.assert_status_code(response, 200) self.assert_model_exists("chat_group/22", {"name": "test", "meeting_id": 2}) + + def test_create_anonymous_may_read(self) -> None: + self.set_models( + { + ONE_ORGANIZATION_FQID: {"enable_chat": True}, + "meeting/1": {"is_active_in_organization_id": 1}, + "group/1": {"meeting_id": 1}, + "group/2": {"meeting_id": 1}, + } + ) + anonymous_group = self.set_anonymous() + response = self.request( + "chat_group.create", + { + "name": "redekreis1", + "meeting_id": 1, + "read_group_ids": [anonymous_group], + "write_group_ids": [1, 2], + }, + ) + self.assert_status_code(response, 200) + self.assert_model_exists( + "chat_group/1", + { + "name": "redekreis1", + "meeting_id": 1, + "read_group_ids": [anonymous_group], + "write_group_ids": [1, 2], + }, + ) + + def test_create_anonymous_may_not_write(self) -> None: + self.set_models( + { + ONE_ORGANIZATION_FQID: {"enable_chat": True}, + "meeting/1": {"is_active_in_organization_id": 1}, + "group/1": {"meeting_id": 1}, + "group/2": {"meeting_id": 1}, + } + ) + anonymous_group = self.set_anonymous() + response = self.request( + "chat_group.create", + { + "name": "redekreis1", + "meeting_id": 1, + "read_group_ids": [1, 2], + "write_group_ids": [anonymous_group], + }, + ) + self.assert_status_code(response, 400) + self.assertIn( + "Anonymous group is not allowed in write_group_ids.", + response.json["message"], + ) diff --git a/tests/system/action/chat_group/test_update.py b/tests/system/action/chat_group/test_update.py index 6c6ea7a55..1ae364e3e 100644 --- a/tests/system/action/chat_group/test_update.py +++ b/tests/system/action/chat_group/test_update.py @@ -110,3 +110,37 @@ def test_update_not_unique_name(self) -> None: ) self.assert_status_code(response, 400) assert "The name of a chat group must be unique." == response.json["message"] + + def test_update_anonymous_may_read(self) -> None: + self.set_models(self.test_models) + anonymous_group = self.set_anonymous() + response = self.request( + "chat_group.update", + { + "id": 1, + "read_group_ids": [anonymous_group], + }, + ) + self.assert_status_code(response, 200) + self.assert_model_exists( + "chat_group/1", + { + "read_group_ids": [anonymous_group], + }, + ) + + def test_update_anonymous_may_not_write(self) -> None: + self.set_models(self.test_models) + anonymous_group = self.set_anonymous() + response = self.request( + "chat_group.update", + { + "id": 1, + "write_group_ids": [anonymous_group], + }, + ) + self.assert_status_code(response, 400) + self.assertIn( + "Anonymous group is not allowed in write_group_ids.", + response.json["message"], + ) diff --git a/tests/system/action/group/test_delete.py b/tests/system/action/group/test_delete.py index 4d23c96ce..e90790af8 100644 --- a/tests/system/action/group/test_delete.py +++ b/tests/system/action/group/test_delete.py @@ -47,6 +47,16 @@ def test_delete_admin_group(self) -> None: response = self.request("group.delete", {"id": 111}) self.assert_status_code(response, 400) + def test_delete_anonymous_group(self) -> None: + self.set_models( + { + "meeting/22": {"anonymous_group_id": 111}, + "group/111": {"anonymous_group_for_meeting_id": 22}, + } + ) + response = self.request("group.delete", {"id": 111}) + self.assert_status_code(response, 400) + def test_delete_with_users(self) -> None: self.set_models( { diff --git a/tests/system/action/group/test_update.py b/tests/system/action/group/test_update.py index 400fa45df..4a10ab7d0 100644 --- a/tests/system/action/group/test_update.py +++ b/tests/system/action/group/test_update.py @@ -109,3 +109,46 @@ def test_update_permission_locked_meeting(self) -> None: "group.update", {"id": 3, "name": "name_Xcdfgee"}, ) + + def test_update_permissions_on_anonymous_group_forbidden(self) -> None: + self.set_anonymous() + response = self.request( + "group.update", + { + "id": 4, + "permissions": [Permissions.User.CAN_MANAGE, Permissions.User.CAN_SEE], + }, + ) + self.assert_status_code(response, 400) + self.assertIn( + "The following permissions may not be set for the anonymous group: {'user.can_manage'}", + response.json["message"], + ) + + def test_update_permissions_on_anonymous_group_allowed(self) -> None: + self.set_anonymous() + response = self.request( + "group.update", + { + "id": 4, + "permissions": [Permissions.User.CAN_SEE], + }, + ) + self.assert_status_code(response, 200) + model = self.get_model("group/4") + assert model.get("permissions") == [Permissions.User.CAN_SEE] + + def test_update_name_on_anonymous_group(self) -> None: + self.set_anonymous() + response = self.request( + "group.update", + { + "id": 4, + "name": "Fight club", + }, + ) + self.assert_status_code(response, 400) + self.assertIn( + "Cannot change name of anonymous group.", + response.json["message"], + ) diff --git a/tests/system/action/meeting/test_update.py b/tests/system/action/meeting/test_update.py index e3058f14f..eb886696c 100644 --- a/tests/system/action/meeting/test_update.py +++ b/tests/system/action/meeting/test_update.py @@ -610,6 +610,7 @@ def test_update_list_of_speakers_closing_disables_point_of_order( ) def test_update_with_user(self) -> None: + """Also tests if the anonymous group is created""" self.set_models( { "committee/1": {"meeting_ids": [3]}, @@ -653,6 +654,10 @@ def test_update_with_user(self) -> None: ] ) self.assert_status_code(response, 200) + self.assert_model_exists( + "group/12", + {"meeting_id": 3, "name": "Anonymous", "anonymous_group_for_meeting_id": 3}, + ) def test_update_set_as_template_true(self) -> None: self.set_models(self.test_models) @@ -877,3 +882,115 @@ def test_update_cant_lock_template_4(self) -> None: "A meeting cannot be locked from the inside and a template at the same time.", response.json["message"], ) + + def test_update_cant_lock_public_meeting(self) -> None: + self.set_models(self.test_models) + response = self.request( + "meeting.update", + { + "id": 1, + "enable_anonymous": True, + "locked_from_inside": True, + "location": "Geneva", + }, + ) + self.assert_status_code(response, 400) + self.assertIn( + "A meeting cannot be locked from the inside and have anonymous enabled at the same time.", + response.json["message"], + ) + + def test_update_cant_lock_public_meeting_2(self) -> None: + self.set_models(self.test_models) + self.set_models( + { + "meeting/1": { + "enable_anonymous": True, + } + } + ) + response = self.request( + "meeting.update", + {"id": 1, "locked_from_inside": True, "location": "Geneva"}, + ) + self.assert_status_code(response, 400) + self.assertIn( + "A meeting cannot be locked from the inside and have anonymous enabled at the same time.", + response.json["message"], + ) + + def test_update_cant_lock_public_meeting_3(self) -> None: + self.create_meeting() + self.set_models(self.test_models) + self.set_models( + { + "meeting/1": { + "locked_from_inside": True, + "admin_group_id": 2, + } + } + ) + self.set_user_groups(1, [2]) + response = self.request( + "meeting.update", + {"id": 1, "enable_anonymous": True, "location": "Geneva"}, + ) + self.assert_status_code(response, 400) + self.assertIn( + "A meeting cannot be locked from the inside and have anonymous enabled at the same time.", + response.json["message"], + ) + + def test_update_set_anonymous_with_anonymous_group_already_existing(self) -> None: + self.set_models(self.test_models) + self.set_models( + { + "meeting/1": {"anonymous_group_id": 99, "group_ids": [1, 99]}, + "group/99": { + "anonymous_group_for_meeting_id": 1, + "name": "Anonymous", + "meeting_id": 1, + }, + } + ) + response = self.request( + "meeting.update", + { + "id": 1, + "enable_anonymous": True, + "location": "Geneva", + }, + ) + self.assert_status_code(response, 200) + self.assert_model_not_exists("group/100") + + def base_anonymous_group_in_poll_default_field_test(self, field: str) -> None: + self.create_meeting() + self.set_anonymous() + response = self.request( + "meeting.update", + { + "id": 1, + field: [4], + }, + ) + self.assert_status_code(response, 400) + self.assertIn( + f"Anonymous group is not allowed in {field}.", + response.json["message"], + ) + + def test_anonymous_in_assignment_poll_default_group_ids(self) -> None: + self.base_anonymous_group_in_poll_default_field_test( + "assignment_poll_default_group_ids" + ) + + def test_anonymous_in_motion_poll_default_group_ids(self) -> None: + self.base_anonymous_group_in_poll_default_field_test( + "motion_poll_default_group_ids" + ) + + def test_anonymous_in_topic_poll_default_group_ids(self) -> None: + self.base_anonymous_group_in_poll_default_field_test( + "topic_poll_default_group_ids" + ) diff --git a/tests/system/action/meeting_user/test_create.py b/tests/system/action/meeting_user/test_create.py index 25c053849..bb30e90b6 100644 --- a/tests/system/action/meeting_user/test_create.py +++ b/tests/system/action/meeting_user/test_create.py @@ -31,6 +31,29 @@ def test_create(self) -> None: self.assert_model_exists("meeting_user/1", test_dict) self.assert_model_exists("user/1", {"committee_ids": [1]}) + def test_create_anonymous_group_id(self) -> None: + self.create_meeting() + self.set_models( + { + "meeting/1": {"group_ids": [1, 2, 3, 4]}, + "group/4": {"anonymous_group_for_meeting_id": 1}, + } + ) + user_id = self.create_user("dummy") + response = self.request( + "meeting_user.create", + { + "user_id": user_id, + "meeting_id": 1, + "group_ids": [4], + }, + ) + self.assert_status_code(response, 400) + self.assertIn( + "Cannot add explicit users to a meetings anonymous group", + response.json["message"], + ) + def test_update_checks_locked_out_with_error(self) -> None: self.set_models( { diff --git a/tests/system/action/meeting_user/test_set_data.py b/tests/system/action/meeting_user/test_set_data.py index a46acfdc7..00dac7445 100644 --- a/tests/system/action/meeting_user/test_set_data.py +++ b/tests/system/action/meeting_user/test_set_data.py @@ -152,3 +152,48 @@ def test_prevent_zero_vote_weight(self) -> None: response = self.request("meeting_user.set_data", {"vote_weight": "0.000000"}) self.assert_status_code(response, 400) self.assert_model_exists("meeting_user/5", {"vote_weight": "1.000000"}) + + def test_set_data_anonymous_group_id(self) -> None: + self.create_meeting() + self.set_models( + { + "meeting/1": {"group_ids": [1, 2, 3, 4]}, + "group/4": {"anonymous_group_for_meeting_id": 1}, + } + ) + self.create_user("dummy", [1]) + response = self.request( + "meeting_user.set_data", + { + "id": 1, + "group_ids": [4], + }, + ) + self.assert_status_code(response, 400) + self.assertIn( + "Cannot add explicit users to a meetings anonymous group", + response.json["message"], + ) + + def test_set_data_anonymous_group_id_2(self) -> None: + self.create_meeting() + self.set_models( + { + "meeting/1": {"group_ids": [1, 2, 3, 4]}, + "group/4": {"anonymous_group_for_meeting_id": 1}, + } + ) + user_id = self.create_user("dummy") + response = self.request( + "meeting_user.set_data", + { + "user_id": user_id, + "meeting_id": 1, + "group_ids": [1, 4], + }, + ) + self.assert_status_code(response, 400) + self.assertIn( + "Cannot add explicit users to a meetings anonymous group", + response.json["message"], + ) diff --git a/tests/system/action/meeting_user/test_update.py b/tests/system/action/meeting_user/test_update.py index be28c15de..a360d0ba3 100644 --- a/tests/system/action/meeting_user/test_update.py +++ b/tests/system/action/meeting_user/test_update.py @@ -75,6 +75,28 @@ def test_update_merge_fields_correct(self) -> None: }, ) + def test_update_anonymous_group_id(self) -> None: + self.create_meeting() + self.set_models( + { + "meeting/1": {"group_ids": [1, 2, 3, 4]}, + "group/4": {"anonymous_group_for_meeting_id": 1}, + } + ) + self.create_user("dummy", [1]) + response = self.request( + "meeting_user.update", + { + "id": 1, + "group_ids": [4], + }, + ) + self.assert_status_code(response, 400) + self.assertIn( + "Cannot add explicit users to a meetings anonymous group", + response.json["message"], + ) + def test_update_checks_locked_out_with_error(self) -> None: self.set_models( { diff --git a/tests/system/action/motion_comment_section/test_create.py b/tests/system/action/motion_comment_section/test_create.py index dcdb6a751..b057d406d 100644 --- a/tests/system/action/motion_comment_section/test_create.py +++ b/tests/system/action/motion_comment_section/test_create.py @@ -93,3 +93,60 @@ def test_create_permissions_locked_meeting(self) -> None: "motion_comment_section.create", {"name": "test_Xcdfgee", "meeting_id": 1}, ) + + def test_create_anonymous_may_read(self) -> None: + self.set_models( + { + "meeting/222": { + "name": "name_SNLGsvIV", + "is_active_in_organization_id": 1, + }, + "group/23": {"name": "name_IIwngcUT", "meeting_id": 222}, + } + ) + anonymous_group = self.set_anonymous(meeting_id=222) + response = self.request( + "motion_comment_section.create", + { + "name": "test_Xcdfgee", + "meeting_id": 222, + "read_group_ids": [anonymous_group], + "write_group_ids": [23], + }, + ) + self.assert_status_code(response, 200) + self.assert_model_exists( + "motion_comment_section/1", + { + "name": "test_Xcdfgee", + "meeting_id": 222, + "read_group_ids": [anonymous_group], + "write_group_ids": [23], + }, + ) + + def test_create_anonymous_may_not_write(self) -> None: + self.set_models( + { + "meeting/222": { + "name": "name_SNLGsvIV", + "is_active_in_organization_id": 1, + }, + "group/23": {"name": "name_IIwngcUT", "meeting_id": 222}, + } + ) + anonymous_group = self.set_anonymous(meeting_id=222) + response = self.request( + "motion_comment_section.create", + { + "name": "test_Xcdfgee", + "meeting_id": 222, + "read_group_ids": [23], + "write_group_ids": [anonymous_group], + }, + ) + self.assert_status_code(response, 400) + self.assertIn( + "Anonymous group is not allowed in write_group_ids.", + response.json["message"], + ) diff --git a/tests/system/action/motion_comment_section/test_update.py b/tests/system/action/motion_comment_section/test_update.py index f7141186f..9e9cfdb67 100644 --- a/tests/system/action/motion_comment_section/test_update.py +++ b/tests/system/action/motion_comment_section/test_update.py @@ -106,3 +106,55 @@ def test_update_permissions_locked_meeting(self) -> None: "write_group_ids": [23], }, ) + + def test_update_anonymous_may_read(self) -> None: + self.create_meeting() + self.set_models( + { + "meeting/1": {"motion_comment_section_ids": [111]}, + "motion_comment_section/111": { + "name": "name_srtgb123", + "meeting_id": 1, + }, + } + ) + anonymous_group = self.set_anonymous() + response = self.request( + "motion_comment_section.update", + { + "id": 111, + "read_group_ids": [anonymous_group], + }, + ) + self.assert_status_code(response, 200) + self.assert_model_exists( + "motion_comment_section/111", + { + "read_group_ids": [anonymous_group], + }, + ) + + def test_update_anonymous_may_not_write(self) -> None: + self.create_meeting() + self.set_models( + { + "meeting/1": {"motion_comment_section_ids": [111]}, + "motion_comment_section/111": { + "name": "name_srtgb123", + "meeting_id": 1, + }, + } + ) + anonymous_group = self.set_anonymous() + response = self.request( + "motion_comment_section.update", + { + "id": 111, + "write_group_ids": [anonymous_group], + }, + ) + self.assert_status_code(response, 400) + self.assertIn( + "Anonymous group is not allowed in write_group_ids.", + response.json["message"], + ) diff --git a/tests/system/action/poll/test_create.py b/tests/system/action/poll/test_create.py index e7a800f27..eb3e7bea6 100644 --- a/tests/system/action/poll/test_create.py +++ b/tests/system/action/poll/test_create.py @@ -1078,3 +1078,24 @@ def test_create_poll_candidate_lists(self) -> None: "poll_candidate_list/2", {"option_id": 2, "meeting_id": 1, "poll_candidate_ids": [3, 4]}, ) + + def test_with_anonymous_in_entitled_group_ids(self) -> None: + self.create_meeting() + self.set_anonymous() + response = self.request( + "poll.create", + { + "meeting_id": 1, + "options": [{"text": "test"}], + "pollmethod": "YNA", + "title": "test", + "type": Poll.TYPE_NAMED, + "entitled_group_ids": [4], + "content_object_id": "assignment/1", + }, + ) + self.assert_status_code(response, 400) + self.assertIn( + "Anonymous group is not allowed in entitled_group_ids.", + response.json["message"], + ) diff --git a/tests/system/action/poll/test_update.py b/tests/system/action/poll/test_update.py index 353f21b51..940a967a4 100644 --- a/tests/system/action/poll/test_update.py +++ b/tests/system/action/poll/test_update.py @@ -212,6 +212,18 @@ def test_update_groups(self) -> None: poll = self.get_model("poll/1") self.assertEqual(poll.get("entitled_group_ids"), [2]) + def test_update_groups_with_anonymous(self) -> None: + group_id = self.set_anonymous() + response = self.request( + "poll.update", + {"entitled_group_ids": [group_id], "id": 1}, + ) + self.assert_status_code(response, 400) + self.assertIn( + "Anonymous group is not allowed in entitled_group_ids.", + response.json["message"], + ) + def test_update_title_started(self) -> None: self.update_model("poll/1", {"state": Poll.STATE_STARTED}) response = self.request( diff --git a/tests/system/action/test_permissions.py b/tests/system/action/test_permissions.py index 4c3f56e78..906a60fd0 100644 --- a/tests/system/action/test_permissions.py +++ b/tests/system/action/test_permissions.py @@ -51,8 +51,7 @@ def test_anonymous_no_permission(self) -> None: ) def test_anonymous_valid(self) -> None: - self.set_anonymous(True) - self.set_group_permissions(1, [Permissions.Motion.CAN_CREATE]) + self.set_anonymous(True, permissions=[Permissions.Motion.CAN_CREATE]) response = self.request( "fake_model_p.create", {"meeting_id": 1}, anonymous=True ) diff --git a/tests/system/action/user/test_assign_meetings.py b/tests/system/action/user/test_assign_meetings.py index dc4e5433b..25cc80065 100644 --- a/tests/system/action/user/test_assign_meetings.py +++ b/tests/system/action/user/test_assign_meetings.py @@ -107,6 +107,118 @@ def test_assign_meetings_correct(self) -> None: "meeting_user/7", {"meeting_id": 4, "user_id": 1, "group_ids": [43]} ) + def test_assign_meetings_ignore_meetings_anonymous_group(self) -> None: + """ + ...and don't ignore groups that are just named "Anonymous" + """ + self.set_models( + { + "group/11": { + "name": "Anonymous", + "meeting_id": 1, + "meeting_user_ids": [1], + }, + "group/22": { + "name": "nothing", + "meeting_id": 2, + "meeting_user_ids": [2], + }, + "group/31": { + "name": "Anonymous", + "meeting_id": 3, + "anonymous_group_for_meeting_id": 3, + }, + "group/32": { + "name": "standard", + "meeting_id": 3, + "default_group_for_meeting_id": 3, + }, + "group/43": {"name": "standard", "meeting_id": 4}, + "group/51": {"name": "Anonymous", "meeting_id": 5}, + "group/52": { + "name": "Anonymous", + "meeting_id": 5, + "anonymous_group_for_meeting_id": 5, + }, + "meeting/1": { + "name": "success(existing)", + "group_ids": [11], + "is_active_in_organization_id": 1, + "committee_id": 2, + "meeting_user_ids": [1], + }, + "meeting/2": { + "name": "nothing", + "group_ids": [22], + "is_active_in_organization_id": 1, + "committee_id": 2, + "meeting_user_ids": [2], + }, + "meeting/3": { + "name": "success(added)", + "group_ids": [30, 31], + "is_active_in_organization_id": 1, + "committee_id": 2, + "default_group_id": 32, + }, + "meeting/4": { + "name": "standard", + "group_ids": [43], + "is_active_in_organization_id": 1, + "default_group_id": 43, + "committee_id": 2, + }, + "meeting/5": { + "name": "success(added)", + "group_ids": [51, 52], + "is_active_in_organization_id": 1, + "committee_id": 2, + }, + "user/1": { + "meeting_user_ids": [1, 2, 5], + "meeting_ids": [1, 2, 5], + }, + "meeting_user/1": { + "meeting_id": 1, + "user_id": 1, + "group_ids": [11], + }, + "meeting_user/2": { + "meeting_id": 2, + "user_id": 1, + "group_ids": [22], + }, + "committee/2": {"meeting_ids": [1, 2, 3, 4, 5]}, + } + ) + response = self.request( + "user.assign_meetings", + { + "id": 1, + "meeting_ids": [1, 2, 3, 4, 5], + "group_name": "Anonymous", + }, + ) + self.assert_status_code(response, 200) + assert response.json["results"][0][0]["succeeded"] == [1, 5] + assert response.json["results"][0][0]["standard_group"] == [3, 4] + assert response.json["results"][0][0]["nothing"] == [2] + self.assert_model_exists( + "meeting_user/1", {"meeting_id": 1, "user_id": 1, "group_ids": [11]} + ) + self.assert_model_exists( + "meeting_user/2", {"meeting_id": 2, "user_id": 1, "group_ids": [22]} + ) + self.assert_model_exists( + "meeting_user/3", {"meeting_id": 5, "user_id": 1, "group_ids": [51]} + ) + self.assert_model_exists( + "meeting_user/4", {"meeting_id": 3, "user_id": 1, "group_ids": [32]} + ) + self.assert_model_exists( + "meeting_user/5", {"meeting_id": 4, "user_id": 1, "group_ids": [43]} + ) + def test_assign_meetings_multiple_committees(self) -> None: self.set_models( { diff --git a/tests/system/action/user/test_create.py b/tests/system/action/user/test_create.py index 33106535a..eea35e93f 100644 --- a/tests/system/action/user/test_create.py +++ b/tests/system/action/user/test_create.py @@ -1537,6 +1537,28 @@ def test_create_saml_id_but_duplicate_error2(self) -> None: "A user with the username 123saml already exists.", response.json["message"] ) + def test_create_anonymous_group_id(self) -> None: + self.create_meeting() + self.set_models( + { + "meeting/1": {"group_ids": [1, 2, 3, 4]}, + "group/4": {"anonymous_group_for_meeting_id": 1}, + } + ) + response = self.request( + "user.create", + { + "username": "test_Xcdfgee", + "meeting_id": 1, + "group_ids": [4], + }, + ) + self.assert_status_code(response, 400) + self.assertIn( + "Cannot add explicit users to a meetings anonymous group", + response.json["message"], + ) + def test_create_permission_as_locked_out(self) -> None: self.create_meeting() self.user_id = self.create_user("user") diff --git a/tests/system/action/user/test_participant_import.py b/tests/system/action/user/test_participant_import.py index b42a97f0a..1efb0e462 100644 --- a/tests/system/action/user/test_participant_import.py +++ b/tests/system/action/user/test_participant_import.py @@ -542,16 +542,10 @@ def test_json_upload_update_multiple_users_okay(self) -> None: self.json_upload_multiple_users() response = self.request("participant.import", {"id": 1, "import": True}) self.assert_status_code(response, 200) - group = self.assert_model_exists("group/8") - if group["name"] == "unknown": - self.assert_model_exists("group/9", {"name": "group4"}) - unknown_id = 8 - group4_id = 9 - else: - assert group["name"] == "group4" - self.assert_model_exists("group/9", {"name": "unknown"}) - unknown_id = 9 - group4_id = 8 + created_groups = { + self.assert_model_exists(f"group/{id_}")["name"]: id_ for id_ in [9, 10, 11] + } + assert sorted(list(created_groups.keys())) == ["Anonymous", "group4", "unknown"] self.assert_model_exists( "user/2", { @@ -578,7 +572,7 @@ def test_json_upload_update_multiple_users_okay(self) -> None: "meeting_user/38", { "user_id": 2, - "group_ids": [3, group4_id], + "group_ids": [3, created_groups["group4"]], "meeting_id": 1, "structure_level_ids": [level_up["id"]], }, @@ -624,7 +618,7 @@ def test_json_upload_update_multiple_users_okay(self) -> None: "meeting_user/39", { "user_id": 4, - "group_ids": [group4_id], + "group_ids": [created_groups["group4"]], "meeting_id": 1, "vote_weight": None, }, @@ -666,7 +660,7 @@ def test_json_upload_update_multiple_users_okay(self) -> None: "meeting_user/36", { "user_id": 6, - "group_ids": [group4_id], + "group_ids": [created_groups["group4"]], "meeting_id": 1, }, ) @@ -686,7 +680,13 @@ def test_json_upload_update_multiple_users_okay(self) -> None: "meeting_user/37", { "user_id": 7, - "group_ids": [2, group4_id, unknown_id, 7], + "group_ids": [ + 2, + created_groups["group4"], + created_groups["Anonymous"], + created_groups["unknown"], + 7, + ], "meeting_id": 1, }, ) @@ -695,13 +695,10 @@ def test_json_upload_one_structure_level_newly_created(self) -> None: self.json_upload_multiple_users() self.request("structure_level.create", {"meeting_id": 1, "name": "no. 5"}) response = self.request("participant.import", {"id": 1, "import": True}) - group = self.assert_model_exists("group/8") - if group["name"] == "unknown": - self.assert_model_exists("group/9", {"name": "group4"}) - group4_id = 9 - else: - assert group["name"] == "group4" - group4_id = 8 + created_groups = { + self.assert_model_exists(f"group/{id_}")["name"]: id_ for id_ in [9, 10, 11] + } + assert sorted(list(created_groups.keys())) == ["Anonymous", "group4", "unknown"] self.assert_status_code(response, 200) assert (result := response.json["results"][0][0])["state"] == ImportState.DONE row = result["rows"][0] @@ -716,7 +713,7 @@ def test_json_upload_one_structure_level_newly_created(self) -> None: "default_password": {"info": ImportState.WARNING, "value": ""}, "groups": [ {"id": 3, "info": "done", "value": "group3"}, - {"id": group4_id, "info": "new", "value": "group4"}, + {"id": created_groups["group4"], "info": "new", "value": "group4"}, ], "structure_level": [{"info": "new", "value": "level up", "id": 2}], } @@ -744,7 +741,9 @@ def test_json_upload_one_structure_level_newly_created(self) -> None: "username": {"id": 4, "info": ImportState.DONE, "value": "user4"}, "last_name": {"value": "Luther King", "info": ImportState.DONE}, "first_name": {"value": "Martin", "info": ImportState.DONE}, - "groups": [{"id": group4_id, "info": "new", "value": "group4"}], + "groups": [ + {"id": created_groups["group4"], "info": "new", "value": "group4"} + ], } row = result["rows"][3] @@ -776,7 +775,7 @@ def test_json_upload_one_structure_level_newly_created(self) -> None: "default_password": {"info": ImportState.WARNING, "value": ""}, "is_present": {"info": "done", "value": True}, "groups": [ - {"id": group4_id, "info": "new", "value": "group4"}, + {"id": created_groups["group4"], "info": "new", "value": "group4"}, ], } @@ -905,6 +904,7 @@ def test_json_upload_update_multiple_users_all_error(self) -> None: assert row["data"]["groups"] == [ {"id": 2, "info": "warning", "value": "group2"}, {"info": "new", "value": "group4"}, + {"info": "new", "value": "Anonymous"}, {"info": "new", "value": "unknown"}, {"id": 7, "info": "warning", "value": "group7M1"}, ] diff --git a/tests/system/action/user/test_participant_json_upload.py b/tests/system/action/user/test_participant_json_upload.py index 90f372440..263566176 100644 --- a/tests/system/action/user/test_participant_json_upload.py +++ b/tests/system/action/user/test_participant_json_upload.py @@ -1480,7 +1480,16 @@ def json_upload_multiple_users(self) -> None: "meeting_id": 1, "name": "group7M1", }, - "meeting/1": {"meeting_user_ids": [31], "group_ids": [1, 2, 3, 7]}, + "group/8": { + "meeting_id": 1, + "name": "Anonymous", + "anonymous_group_for_meeting_id": 1, + }, + "meeting/1": { + "meeting_user_ids": [31], + "group_ids": [1, 2, 3, 7, 8], + "anonymous_group_id": 8, + }, "meeting/4": {"meeting_user_ids": [34]}, } ) @@ -1515,7 +1524,13 @@ def json_upload_multiple_users(self) -> None: { "first_name": "Joan", "last_name": "Baez7", - "groups": ["group2", "group4", "unknown", "group7M1"], + "groups": [ + "group2", + "group4", + "Anonymous", + "unknown", + "group7M1", + ], }, ], }, @@ -1609,6 +1624,7 @@ def json_upload_multiple_users(self) -> None: "groups": [ {"id": 2, "info": "done", "value": "group2"}, {"info": "new", "value": "group4"}, + {"info": "new", "value": "Anonymous"}, {"info": "new", "value": "unknown"}, {"id": 7, "info": "done", "value": "group7M1"}, ], diff --git a/tests/system/action/user/test_update.py b/tests/system/action/user/test_update.py index cba8a0487..a9489f480 100644 --- a/tests/system/action/user/test_update.py +++ b/tests/system/action/user/test_update.py @@ -2659,6 +2659,29 @@ def test_update_with_internal_fields_error(self) -> None: ]: self.assertIn(field, message) + def test_update_anonymous_group_id(self) -> None: + self.create_meeting() + self.set_models( + { + "meeting/1": {"group_ids": [1, 2, 3, 4]}, + "group/4": {"anonymous_group_for_meeting_id": 1}, + } + ) + user_id = self.create_user("dummy", [1]) + response = self.request( + "user.update", + { + "id": user_id, + "meeting_id": 1, + "group_ids": [1, 4], + }, + ) + self.assert_status_code(response, 400) + self.assertIn( + "Cannot add explicit users to a meetings anonymous group", + response.json["message"], + ) + def create_data_for_locked_out_test(self) -> dict[str, tuple[int, int | None]]: """ Creates two meetings and a bunch of users with different roles.