Skip to content

Commit df3ec69

Browse files
committed
♻️(backend) sync accesses if updated role == max_ancestors_role
When an access role is updated and the new role is equal to the access max_ancestors_role, we want to resync accesses. For this, the current access is deleted instead of updating it. We don't want to have 2 explicits accesses sharing the same role.
1 parent d2ac2a8 commit df3ec69

File tree

2 files changed

+87
-24
lines changed

2 files changed

+87
-24
lines changed

src/backend/core/api/viewsets.py

Lines changed: 31 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1112,7 +1112,6 @@ class ItemAccessViewSet(
11121112
drf.mixins.CreateModelMixin,
11131113
drf.mixins.DestroyModelMixin,
11141114
drf.mixins.RetrieveModelMixin,
1115-
drf.mixins.UpdateModelMixin,
11161115
viewsets.GenericViewSet,
11171116
):
11181117
"""
@@ -1256,15 +1255,24 @@ def list(self, request, *args, **kwargs):
12561255

12571256
return drf.response.Response(serialized_data)
12581257

1259-
def perform_update(self, serializer):
1260-
"""Check that we don't change the role if it leads to losing the last owner."""
1261-
instance = serializer.instance
1258+
def update(self, request, *args, **kwargs):
1259+
"""
1260+
We not use the update mixin to apply a specific behavior we can't implement using
1261+
perform_update method.
1262+
1263+
If the role is updated and is the same role as the max ancestors role,
1264+
we don't want to have two consecutive explicit accesses with the same role.
1265+
We have to delete the current access, this item will have an inherited access
1266+
with the correct role.
1267+
"""
1268+
partial = kwargs.pop("partial", False)
1269+
instance = self.get_object()
1270+
serializer = self.get_serializer(instance, data=request.data, partial=partial)
1271+
serializer.is_valid(raise_exception=True)
1272+
role = serializer.validated_data.get("role")
12621273

12631274
# Check if the role is being updated and the new role is not "owner"
1264-
if (
1265-
"role" in self.request.data
1266-
and self.request.data["role"] != models.RoleChoices.OWNER
1267-
):
1275+
if role and role != models.RoleChoices.OWNER:
12681276
# Check if the access being updated is the last owner access for the resource
12691277
if (
12701278
self.item.is_root
@@ -1275,10 +1283,25 @@ def perform_update(self, serializer):
12751283
message = "Cannot change the role to a non-owner role for the last owner access."
12761284
raise drf.exceptions.PermissionDenied({"detail": message})
12771285

1286+
if role and instance.max_ancestors_role == role:
1287+
# The submitted role is the same as the max ancestors role,
1288+
# We don't want to have two consecutive explicit accesses with the same role.
1289+
# We have to delete the current access, this item will have an inherited access
1290+
# with the correct role.
1291+
instance.delete()
1292+
return drf.response.Response(status=drf.status.HTTP_204_NO_CONTENT)
1293+
12781294
access = serializer.save()
12791295

12801296
self._syncronize_descendants_accesses(access)
12811297

1298+
return drf.response.Response(serializer.data)
1299+
1300+
def partial_update(self, request, *args, **kwargs):
1301+
"""Partial update the item access."""
1302+
kwargs["partial"] = True
1303+
return self.update(request, *args, **kwargs)
1304+
12821305
def perform_create(self, serializer):
12831306
"""
12841307
Actually create the new item access:

src/backend/core/tests/items/test_api_item_accesses.py

Lines changed: 56 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ def test_api_item_accesses_list_anonymous():
3838

3939
def test_api_item_accesses_list_authenticated_unrelated():
4040
"""
41-
Authenticated users should not be allowed to list item accesses for a item
41+
Authenticated users should not be allowed to list item accesses for an item
4242
to which they are not related.
4343
"""
4444
user = factories.UserFactory()
@@ -528,7 +528,7 @@ def test_api_item_accesses_list_authenticated_related_same_team(
528528

529529
def test_api_item_accesses_retrieve_anonymous():
530530
"""
531-
Anonymous users should not be allowed to retrieve a item access.
531+
Anonymous users should not be allowed to retrieve an item access.
532532
"""
533533
access = factories.UserItemAccessFactory()
534534

@@ -646,8 +646,11 @@ def test_api_item_accesses_retrieve_authenticated_related(
646646
}
647647

648648

649+
## Update --
650+
651+
649652
def test_api_item_accesses_update_anonymous():
650-
"""Anonymous users should not be allowed to update a item access."""
653+
"""Anonymous users should not be allowed to update an item access."""
651654
access = factories.UserItemAccessFactory()
652655
old_values = serializers.ItemAccessSerializer(instance=access).data
653656

@@ -673,7 +676,7 @@ def test_api_item_accesses_update_anonymous():
673676

674677
def test_api_item_accesses_update_authenticated_unrelated():
675678
"""
676-
Authenticated users should not be allowed to update a item access for a item to which
679+
Authenticated users should not be allowed to update an item access for an item to which
677680
they are not related.
678681
"""
679682
user = factories.UserFactory()
@@ -708,7 +711,7 @@ def test_api_item_accesses_update_authenticated_unrelated():
708711
def test_api_item_accesses_update_authenticated_reader_or_editor(
709712
via, role, mock_user_teams
710713
):
711-
"""Readers or editors of a item should not be allowed to update its accesses."""
714+
"""Readers or editors of an item should not be allowed to update its accesses."""
712715
user = factories.UserFactory()
713716

714717
client = APIClient()
@@ -806,7 +809,7 @@ def test_api_item_accesses_update_administrator_except_owner(
806809
@pytest.mark.parametrize("via", VIA)
807810
def test_api_item_accesses_update_administrator_from_owner(via, mock_user_teams):
808811
"""
809-
A user who is an administrator in a item, should not be allowed to update
812+
A user who is an administrator in an item, should not be allowed to update
810813
the user access of an "owner" for this item.
811814
"""
812815
user = factories.UserFactory()
@@ -850,7 +853,7 @@ def test_api_item_accesses_update_administrator_to_owner(
850853
mock_user_teams,
851854
):
852855
"""
853-
A user who is an administrator in a item, should not be allowed to update
856+
A user who is an administrator in an item, should not be allowed to update
854857
the user access of another user to grant item ownership.
855858
"""
856859
user = factories.UserFactory()
@@ -911,7 +914,7 @@ def test_api_item_accesses_update_owner(
911914
mock_user_teams,
912915
):
913916
"""
914-
A user who is an owner in a item should be allowed to update
917+
A user who is an owner in an item should be allowed to update
915918
a user access for this item whatever the role.
916919
"""
917920
user = factories.UserFactory()
@@ -969,7 +972,7 @@ def test_api_item_accesses_update_owner_self(
969972
mock_user_teams,
970973
):
971974
"""
972-
A user who is owner of a item should be allowed to update
975+
A user who is owner of an item should be allowed to update
973976
their own user access provided there are other owners in the item.
974977
"""
975978
user = factories.UserFactory()
@@ -1215,11 +1218,48 @@ def test_api_item_accesses_update_authenticated_owner_syncronize_descendants_acc
12151218
)
12161219

12171220

1221+
def test_api_item_accesses_update_to_same_role_as_max_ancestors_role():
1222+
"""
1223+
If the role is updated and is the same role as the max ancestors role,
1224+
The current access is deleted and a 204 status code is returned.
1225+
"""
1226+
user = factories.UserFactory()
1227+
1228+
client = APIClient()
1229+
client.force_login(user)
1230+
1231+
root = factories.ItemFactory(type=models.ItemTypeChoices.FOLDER)
1232+
parent = factories.ItemFactory(parent=root, type=models.ItemTypeChoices.FOLDER)
1233+
item = factories.ItemFactory(parent=parent, type=models.ItemTypeChoices.FOLDER)
1234+
1235+
# Create an explicit owner access for the current user on the root item
1236+
factories.UserItemAccessFactory(item=root, user=user, role="owner")
1237+
1238+
other_user = factories.UserFactory()
1239+
factories.UserItemAccessFactory(item=root, user=other_user, role="editor")
1240+
target_access = factories.UserItemAccessFactory(
1241+
item=item, user=other_user, role="administrator"
1242+
)
1243+
1244+
assert item.get_role(other_user) == "administrator"
1245+
assert models.ItemAccess.objects.filter(item=item, user=other_user).exists()
1246+
1247+
response = client.put(
1248+
f"/api/v1.0/items/{item.id!s}/accesses/{target_access.id!s}/",
1249+
data={"role": "editor"},
1250+
)
1251+
1252+
assert response.status_code == 204
1253+
1254+
assert item.get_role(other_user) == "editor"
1255+
assert not models.ItemAccess.objects.filter(item=item, user=other_user).exists()
1256+
1257+
12181258
# Delete
12191259

12201260

12211261
def test_api_item_accesses_delete_anonymous():
1222-
"""Anonymous users should not be allowed to destroy a item access."""
1262+
"""Anonymous users should not be allowed to destroy an item access."""
12231263
user = factories.UserFactory()
12241264
item = user.get_main_workspace()
12251265
access = models.ItemAccess.objects.get(user=user, role="owner", item=item)
@@ -1234,7 +1274,7 @@ def test_api_item_accesses_delete_anonymous():
12341274

12351275
def test_api_item_accesses_delete_authenticated():
12361276
"""
1237-
Authenticated users should not be allowed to delete a item access for a
1277+
Authenticated users should not be allowed to delete an item access for a
12381278
item to which they are not related.
12391279
"""
12401280
user = factories.UserFactory()
@@ -1258,7 +1298,7 @@ def test_api_item_accesses_delete_authenticated():
12581298
@pytest.mark.parametrize("via", VIA)
12591299
def test_api_item_accesses_delete_reader_or_editor(via, role, mock_user_teams):
12601300
"""
1261-
Authenticated users should not be allowed to delete a item access for a
1301+
Authenticated users should not be allowed to delete an item access for a
12621302
item in which they are a simple reader or editor.
12631303
"""
12641304
user = factories.UserFactory()
@@ -1292,7 +1332,7 @@ def test_api_item_accesses_delete_administrators_except_owners(
12921332
mock_user_teams,
12931333
):
12941334
"""
1295-
Users who are administrators in a item should be allowed to delete an access
1335+
Users who are administrators in an item should be allowed to delete an access
12961336
from the item provided it is not ownership.
12971337
"""
12981338
user = factories.UserFactory()
@@ -1325,7 +1365,7 @@ def test_api_item_accesses_delete_administrators_except_owners(
13251365
@pytest.mark.parametrize("via", VIA)
13261366
def test_api_item_accesses_delete_administrator_on_owners(via, mock_user_teams):
13271367
"""
1328-
Users who are administrators in a item should not be allowed to delete an ownership
1368+
Users who are administrators in an item should not be allowed to delete an ownership
13291369
access from the item.
13301370
"""
13311371
user = factories.UserFactory()
@@ -1360,7 +1400,7 @@ def test_api_item_accesses_delete_owners(
13601400
):
13611401
"""
13621402
Users should be able to delete the item access of another user
1363-
for a item of which they are owner.
1403+
for an item of which they are owner.
13641404
"""
13651405
user = factories.UserFactory()
13661406

@@ -1390,7 +1430,7 @@ def test_api_item_accesses_delete_owners(
13901430
@pytest.mark.parametrize("via", VIA)
13911431
def test_api_item_accesses_delete_owners_last_owner(via, mock_user_teams):
13921432
"""
1393-
It should not be possible to delete the last owner access from a item
1433+
It should not be possible to delete the last owner access from an item
13941434
"""
13951435
user = factories.UserFactory()
13961436

0 commit comments

Comments
 (0)