Skip to content

Commit

Permalink
Allow meeting admin user to update a non admin user that shares all h…
Browse files Browse the repository at this point in the history
…is meetings with requesting user. (#2576)

* Allow meeting admin user to update a non admin user that shares all his meetings with requesting admin user.
* Use user.can_update and user.can_manage.
* Implement get_user_editable presenter with payload field names to support all payload field groups.

---------

Co-authored-by: Elblinator <[email protected]>
Co-authored-by: luisa-beerboom <[email protected]>
  • Loading branch information
3 people authored Nov 25, 2024
1 parent deac9e1 commit c13b26f
Show file tree
Hide file tree
Showing 27 changed files with 1,671 additions and 112 deletions.
1 change: 1 addition & 0 deletions docs/Presenters-Overview.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ Available presenters:
- [get_forwarding_meetings](presenters/get_forwarding_meetings.md)
- [get_meetings](presenters/get_meetings.md)
- [get_users](presenters/get_users.md)
- [get_user_editable](presenters/get_user_editable.md)
- [get_user_related_models](presenters/get_user_related_models.md)
- [get_user_scope](presenters/get_user_scope.md)
- [search_deleted_models](presenters/search_deleted_models.md)
Expand Down
1 change: 0 additions & 1 deletion docs/actions/user.set_password.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@
set_as_default: boolean; // default false, if not given
}
```

## Action
Sets the password of the user given by `id` to `password`. If `set_as_default` is true, the `default_password` is also updated.

Expand Down
31 changes: 31 additions & 0 deletions docs/presenters/get_user_editable.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
## Payload

```js
{
user_ids: Id[], // required
fields: string[] // required
}
```

## Returns

```js
{
user_id: Id: {
field: str: (
editable: boolean, // true if user can be updated or deleted,
message?: string // error message if an exception was caught
),
...
},
...
}
```

## Logic

It iterates over the given `user_ids` and calculates whether a user can be updated depending on the given payload fields, permissions in shared committees and meetings, OML and the user-scope. The user scope is defined [here](https://github.com/OpenSlides/OpenSlides/wiki/Users#user-scopes). The payload field permissions are described [here](https://github.com/OpenSlides/openslides-backend/blob/main/docs/actions/user.update.md) and [here](https://github.com/OpenSlides/openslides-backend/blob/main/docs/actions/user.create.md).

## Permissions

There are no special permissions necessary.
4 changes: 2 additions & 2 deletions docs/presenters/get_user_related_models.md
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
## Payload

```js
```
{
user_ids: Id[] // required
}
```

## Returns

```js
```
{
[user_id: Id]: {
organization_management_level: OML-String,
Expand Down
11 changes: 6 additions & 5 deletions docs/presenters/get_user_scope.md
Original file line number Diff line number Diff line change
@@ -1,21 +1,22 @@
## Payload

```js
```
{
user_ids: Id[] // required
}
```

## Returns

```js
```
{
user_id: Id: {
collection: String, # one of "meeting", "committee" or "organization"
id: Id,
user_oml: String, # one of "superadmin", "can_manage_organization", "can_manage_users", ""
committee_ids: int[] // Ids of all committees the user is part of
}
user_oml: String, # one of "superadmin", "can_manage_organization", "can_manage_users", ""
committee_ids: Id[] // Ids of all committees the user is part of
},
...
}
```

Expand Down
5 changes: 4 additions & 1 deletion openslides_backend/action/actions/user/create.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@
from typing import Any

from openslides_backend.permissions.permissions import Permissions
from openslides_backend.shared.mixins.user_create_update_permissions_mixin import (
CreateUpdatePermissionsMixin,
)

from ....models.models import User
from ....shared.exceptions import ActionException
Expand All @@ -15,13 +18,13 @@
from ...util.register import register_action
from ...util.typing import ActionResultElement
from ..meeting_user.mixin import CheckLockOutPermissionMixin
from .create_update_permissions_mixin import CreateUpdatePermissionsMixin
from .password_mixins import SetPasswordMixin
from .user_mixins import LimitOfUserMixin, UserMixin, UsernameMixin, check_gender_exists


@register_action("user.create")
class UserCreate(
UserMixin,
EmailCheckMixin,
CreateAction,
CreateUpdatePermissionsMixin,
Expand Down
9 changes: 5 additions & 4 deletions openslides_backend/action/actions/user/participant_common.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,14 @@
)
from openslides_backend.permissions.permissions import Permissions
from openslides_backend.shared.exceptions import MissingPermission
from openslides_backend.shared.mixins.user_create_update_permissions_mixin import (
CreateUpdatePermissionsFailingFields,
PermissionVarStore,
)
from openslides_backend.shared.patterns import fqid_from_collection_and_id

from ....shared.filters import And, FilterOperator, Or
from ..meeting_user.mixin import CheckLockOutPermissionMixin
from ..user.create_update_permissions_mixin import (
CreateUpdatePermissionsFailingFields,
PermissionVarStore,
)


class ParticipantCommon(BaseImportJsonUploadAction, CheckLockOutPermissionMixin):
Expand Down Expand Up @@ -51,6 +51,7 @@ def check_permissions(self, instance: dict[str, Any]) -> None:
)

self.permission_check = CreateUpdatePermissionsFailingFields(
self.user_id,
permstore,
self.services,
self.datastore,
Expand Down
5 changes: 4 additions & 1 deletion openslides_backend/action/actions/user/update.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@
from typing import Any

from openslides_backend.permissions.permissions import Permissions
from openslides_backend.shared.mixins.user_create_update_permissions_mixin import (
CreateUpdatePermissionsMixin,
)

from ....action.action import original_instances
from ....action.util.typing import ActionData
Expand All @@ -17,7 +20,6 @@
from ...util.register import register_action
from ..meeting_user.mixin import CheckLockOutPermissionMixin
from .conditional_speaker_cascade_mixin import ConditionalSpeakerCascadeMixin
from .create_update_permissions_mixin import CreateUpdatePermissionsMixin
from .user_mixins import (
AdminIntegrityCheckMixin,
LimitOfUserMixin,
Expand All @@ -29,6 +31,7 @@

@register_action("user.update")
class UserUpdate(
UserMixin,
EmailCheckMixin,
CreateUpdatePermissionsMixin,
UpdateAction,
Expand Down
4 changes: 4 additions & 0 deletions openslides_backend/action/actions/user/user_mixins.py
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,10 @@ class UserMixin(CheckForArchivedMeetingMixin):
"locked_out": {"type": "boolean"},
}

def check_permissions(self, instance: dict[str, Any]) -> None:
self.assert_not_anonymous()
super().check_permissions(instance)

def validate_instance(self, instance: dict[str, Any]) -> None:
super().validate_instance(instance)
if "meeting_id" not in instance and any(
Expand Down
2 changes: 1 addition & 1 deletion openslides_backend/action/mixins/import_mixins.py
Original file line number Diff line number Diff line change
Expand Up @@ -317,7 +317,7 @@ def flatten_copied_object_fields(
) -> list[ImportRow]:
"""The self.rows will be deepcopied, flattened and returned, without
changes on the self.rows.
This is necessary for using the data in the executution of actions.
This is necessary for using the data in the execution of actions.
The requests response should be given with the unchanged self.rows.
Parameter:
hook_method:
Expand Down
1 change: 1 addition & 0 deletions openslides_backend/presenter/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
get_forwarding_meetings,
get_history_information,
get_mediafile_context,
get_user_editable,
get_user_related_models,
get_user_scope,
get_users,
Expand Down
1 change: 1 addition & 0 deletions openslides_backend/presenter/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ class BasePresenter(BaseServiceProvider):
Base class for presenters.
"""

internal: bool = False
data: Any
schema: Callable[[Any], None] | None = None

Expand Down
85 changes: 85 additions & 0 deletions openslides_backend/presenter/get_user_editable.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
from collections import defaultdict
from typing import Any

import fastjsonschema

from openslides_backend.permissions.permissions import Permissions
from openslides_backend.shared.exceptions import (
ActionException,
MissingPermission,
PermissionDenied,
PresenterException,
)
from openslides_backend.shared.mixins.user_create_update_permissions_mixin import (
CreateUpdatePermissionsMixin,
)
from openslides_backend.shared.schema import id_list_schema, str_list_schema

from ..shared.schema import schema_version
from .base import BasePresenter
from .presenter import register_presenter

get_user_editable_schema = fastjsonschema.compile(
{
"$schema": schema_version,
"type": "object",
"title": "get_user_editable",
"description": "get user editable",
"properties": {
"user_ids": id_list_schema,
"fields": str_list_schema,
},
"required": ["user_ids", "fields"],
"additionalProperties": False,
}
)


@register_presenter("get_user_editable")
class GetUserEditable(CreateUpdatePermissionsMixin, BasePresenter):
"""
Checks for each given user whether the given fields are editable by calling user on a per payload group basis.
"""

schema = get_user_editable_schema
name = "get_user_editable"
permission = Permissions.User.CAN_MANAGE

def get_result(self) -> Any:
if not self.data["fields"]:
raise PresenterException(
"Need at least one field name to check editability."
)
reversed_field_rights = {
field: group
for group, fields in self.field_rights.items()
for field in fields
}
one_field_per_group = {
group_fields[0]
for field_name in self.data["fields"]
for group_fields in self.field_rights.values()
if field_name in group_fields
}
result: defaultdict[str, dict[str, tuple[bool, str]]] = defaultdict(dict)
for user_id in self.data["user_ids"]:
result[str(user_id)] = {}
groups_editable = {}
for field_name in one_field_per_group:
try:
self.check_permissions({"id": user_id, field_name: None})
groups_editable[reversed_field_rights[field_name]] = (True, "")
except (PermissionDenied, MissingPermission, ActionException) as e:
groups_editable[reversed_field_rights[field_name]] = (
False,
e.message,
)
result[str(user_id)].update(
{
data_field_name: groups_editable[
reversed_field_rights[data_field_name]
]
for data_field_name in self.data["fields"]
}
)
return result
5 changes: 4 additions & 1 deletion openslides_backend/presenter/get_user_scope.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,10 @@ def get_result(self) -> Any:
result: dict[str, Any] = {}
user_ids = self.data["user_ids"]
for user_id in user_ids:
scope, scope_id, user_oml, committee_ids = self.get_user_scope(user_id)
scope, scope_id, user_oml, committee_meeting_ids = self.get_user_scope(
user_id
)
committee_ids = [ci for ci in committee_meeting_ids.keys()]
result[str(user_id)] = {
"collection": scope,
"id": scope_id,
Expand Down
25 changes: 19 additions & 6 deletions openslides_backend/shared/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -131,16 +131,29 @@ def __init__(self, action_name: str) -> None:
class MissingPermission(PermissionDenied):
def __init__(
self,
permissions: AnyPermission | dict[AnyPermission, int],
permissions: AnyPermission | dict[AnyPermission, int | set[int]],
) -> None:
if isinstance(permissions, dict):
self.message = (
"Missing permission" + ("s" if len(permissions) > 1 else "") + ": "
)
to_remove = []
for permission, id_or_ids in permissions.items():
if isinstance(id_or_ids, set) and not id_or_ids:
to_remove.append(permission)
for permission in to_remove:
del permissions[permission]
self.message = "Missing permission" + self._plural_s(permissions) + ": "
self.message += " or ".join(
f"{permission.get_verbose_type()} {permission} in {permission.get_base_model()} {id}"
for permission, id in permissions.items()
f"{permission.get_verbose_type()} {permission} in {permission.get_base_model()}{self._plural_s(id_or_ids)} {id_or_ids}"
for permission, id_or_ids in permissions.items()
)
else:
self.message = f"Missing {permissions.get_verbose_type()}: {permissions}"
super().__init__(self.message)

def _plural_s(self, permission_or_id_or_ids: dict | int | set[int]) -> str:
if (
isinstance(permission_or_id_or_ids, set)
or (isinstance(permission_or_id_or_ids, dict))
) and len(permission_or_id_or_ids) > 1:
return "s"
else:
return ""
Loading

0 comments on commit c13b26f

Please sign in to comment.