feat: implement recursive folder permission checks and add integration tests for thematic asset management
This commit is contained in:
parent
fe90a55e68
commit
64423b3de5
8 changed files with 1306 additions and 1235 deletions
Binary file not shown.
File diff suppressed because it is too large
Load diff
Binary file not shown.
File diff suppressed because it is too large
Load diff
Binary file not shown.
File diff suppressed because it is too large
Load diff
|
|
@ -110,16 +110,20 @@ def user_has_document_permission(
|
||||||
# Pour VIEW/COMMENT, can_view_assets=True suffit.
|
# Pour VIEW/COMMENT, can_view_assets=True suffit.
|
||||||
is_edit_check = _PERMISSION_ORDER.get(required_permission, 0) >= _PERMISSION_ORDER[ManagedDocument.PERMISSION_EDIT]
|
is_edit_check = _PERMISSION_ORDER.get(required_permission, 0) >= _PERMISSION_ORDER[ManagedDocument.PERMISSION_EDIT]
|
||||||
|
|
||||||
# Vérifier si l'utilisateur a accès via les thématiques directes des folders
|
# Collecter les IDs des dossiers du document et de tous leurs ancêtres
|
||||||
|
doc_folder_ids = set(document.folders.values_list("pk", flat=True))
|
||||||
|
all_doc_folder_ids = _collect_ancestor_ids(doc_folder_ids) if doc_folder_ids else set()
|
||||||
|
|
||||||
|
# Vérifier si l'utilisateur a accès via les thématiques directes des folders ou de leurs ancêtres
|
||||||
accessible_thematics = _get_editable_thematic_ids(user) if is_edit_check else _get_accessible_thematic_ids(user)
|
accessible_thematics = _get_editable_thematic_ids(user) if is_edit_check else _get_accessible_thematic_ids(user)
|
||||||
if accessible_thematics:
|
if accessible_thematics and all_doc_folder_ids:
|
||||||
if document.folders.filter(thematics__pk__in=accessible_thematics).exists():
|
if DocumentFolder.objects.filter(pk__in=all_doc_folder_ids, thematics__pk__in=accessible_thematics).exists():
|
||||||
return True
|
return True
|
||||||
|
|
||||||
# Vérifier si l'utilisateur a accès via un folder lié à un objet accessible
|
# Vérifier si l'utilisateur a accès via un folder (ou sous-dossier) lié à un objet accessible
|
||||||
accessible_folder_ids = _get_folder_ids_from_editable_objects(user) if is_edit_check else _get_folder_ids_from_accessible_objects(user)
|
accessible_folder_ids = _get_folder_ids_from_editable_objects(user) if is_edit_check else _get_folder_ids_from_accessible_objects(user)
|
||||||
if accessible_folder_ids:
|
if accessible_folder_ids and all_doc_folder_ids:
|
||||||
if document.folders.filter(pk__in=accessible_folder_ids).exists():
|
if all_doc_folder_ids & accessible_folder_ids:
|
||||||
return True
|
return True
|
||||||
|
|
||||||
# Vérifier si le document est directement attaché à un objet accessible via DocumentAttachment
|
# Vérifier si le document est directement attaché à un objet accessible via DocumentAttachment
|
||||||
|
|
@ -563,6 +567,9 @@ def _get_folder_ids_from_objects(user, *, edit_only: bool) -> set[int]:
|
||||||
)
|
)
|
||||||
accessible_folder_ids |= project_folder_ids
|
accessible_folder_ids |= project_folder_ids
|
||||||
|
|
||||||
|
# Récupérer l'ensemble des sous-dossiers (descendants) des dossiers d'assets accessibles
|
||||||
|
accessible_folder_ids = _collect_descendant_ids(accessible_folder_ids)
|
||||||
|
|
||||||
if not hasattr(user, '_folder_cache'):
|
if not hasattr(user, '_folder_cache'):
|
||||||
user._folder_cache = {}
|
user._folder_cache = {}
|
||||||
user._folder_cache[cache_key] = accessible_folder_ids
|
user._folder_cache[cache_key] = accessible_folder_ids
|
||||||
|
|
@ -617,6 +624,29 @@ def _collect_ancestor_ids(folder_ids: set[int]) -> set[int]:
|
||||||
return seen
|
return seen
|
||||||
|
|
||||||
|
|
||||||
|
def _collect_descendant_ids(folder_ids: set[int]) -> set[int]:
|
||||||
|
"""Récupère récursivement tous les sous-dossiers (descendants) des dossier_ids transmis."""
|
||||||
|
if not folder_ids:
|
||||||
|
return set()
|
||||||
|
|
||||||
|
seen = set(folder_ids)
|
||||||
|
stack = list(folder_ids)
|
||||||
|
|
||||||
|
from .models import DocumentFolder
|
||||||
|
|
||||||
|
while stack:
|
||||||
|
batch = stack
|
||||||
|
stack = []
|
||||||
|
children = DocumentFolder.parent_folders.through.objects.filter(
|
||||||
|
to_documentfolder_id__in=batch
|
||||||
|
).values_list("from_documentfolder_id", flat=True)
|
||||||
|
for cid in children:
|
||||||
|
if cid not in seen:
|
||||||
|
seen.add(cid)
|
||||||
|
stack.append(cid)
|
||||||
|
return seen
|
||||||
|
|
||||||
|
|
||||||
def filter_folders_for_user(
|
def filter_folders_for_user(
|
||||||
queryset: models.QuerySet[DocumentFolder],
|
queryset: models.QuerySet[DocumentFolder],
|
||||||
user: settings.AUTH_USER_MODEL | AnonymousUser,
|
user: settings.AUTH_USER_MODEL | AnonymousUser,
|
||||||
|
|
@ -649,10 +679,11 @@ def filter_folders_for_user(
|
||||||
"pk", flat=True
|
"pk", flat=True
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
thematic_ids = _collect_descendant_ids(thematic_ids)
|
||||||
|
|
||||||
thematic_ids = _filter_ids_by_contracts(thematic_ids, user)
|
thematic_ids = _filter_ids_by_contracts(thematic_ids, user)
|
||||||
|
|
||||||
# Ajouter les folders accessibles via les objets attachés
|
# Ajouter les folders accessibles via les objets attachés (avec leurs sous-dossiers)
|
||||||
attached_object_ids = _get_folder_ids_from_accessible_objects(user)
|
attached_object_ids = _get_folder_ids_from_accessible_objects(user)
|
||||||
|
|
||||||
visible_ids = owned_ids | shared_ids | thematic_ids | attached_object_ids
|
visible_ids = owned_ids | shared_ids | thematic_ids | attached_object_ids
|
||||||
|
|
|
||||||
|
|
@ -426,6 +426,64 @@ class DocumentDeletionPermissionTests(TestCase):
|
||||||
)
|
)
|
||||||
self.assertEqual(response.status_code, 403)
|
self.assertEqual(response.status_code, 403)
|
||||||
|
|
||||||
|
def test_internal_manager_can_delete_thematic_asset_document_owned_by_others(self):
|
||||||
|
from common.models import Thematic, UserThematics
|
||||||
|
from assets.models import TrafficLightIntersection
|
||||||
|
from documents.models import DocumentFolderAttachment
|
||||||
|
from django.core.exceptions import PermissionDenied
|
||||||
|
|
||||||
|
thematic, _ = Thematic.objects.get_or_create(
|
||||||
|
code="trafficlights",
|
||||||
|
defaults={"name_fr": "Signalisation Lumineuse Tricolore", "name_nl": "Verkeerslichten"}
|
||||||
|
)
|
||||||
|
UserThematics.objects.create(user_config=self.manager_config, thematic=thematic, can_edit_assets=True)
|
||||||
|
|
||||||
|
intersection = TrafficLightIntersection.objects.create(code="TL-TEST-1", name_fr="Carrefour Test 1")
|
||||||
|
root_folder = DocumentFolder.objects.create(name="Root Intersection Folder", created_by=self.owner)
|
||||||
|
sub_folder = DocumentFolder.objects.create(name="Subfolder Schemas", created_by=self.owner)
|
||||||
|
sub_folder.parent_folders.add(root_folder)
|
||||||
|
|
||||||
|
DocumentFolderAttachment.objects.create(
|
||||||
|
folder=root_folder,
|
||||||
|
content_type=ContentType.objects.get_for_model(TrafficLightIntersection),
|
||||||
|
object_id=intersection.pk,
|
||||||
|
)
|
||||||
|
|
||||||
|
other_doc = ManagedDocument.objects.create(title="Other User Doc in Subfolder", created_by=self.owner)
|
||||||
|
other_doc.folders.add(sub_folder)
|
||||||
|
|
||||||
|
# The manager with edit rights on trafficlights should be allowed to delete the document in the subfolder
|
||||||
|
ensure_document_deletable(other_doc, self.manager)
|
||||||
|
|
||||||
|
def test_internal_manager_without_edit_rights_cannot_delete_thematic_asset_document_owned_by_others(self):
|
||||||
|
from common.models import Thematic, UserThematics
|
||||||
|
from assets.models import TrafficLightIntersection
|
||||||
|
from documents.models import DocumentFolderAttachment
|
||||||
|
from django.core.exceptions import PermissionDenied
|
||||||
|
|
||||||
|
thematic, _ = Thematic.objects.get_or_create(
|
||||||
|
code="trafficlights",
|
||||||
|
defaults={"name_fr": "Signalisation Lumineuse Tricolore", "name_nl": "Verkeerslichten"}
|
||||||
|
)
|
||||||
|
# Manager has can_edit_assets=False
|
||||||
|
UserThematics.objects.create(user_config=self.manager_config, thematic=thematic, can_edit_assets=False)
|
||||||
|
|
||||||
|
intersection = TrafficLightIntersection.objects.create(code="TL-TEST-2", name_fr="Carrefour Test 2")
|
||||||
|
root_folder = DocumentFolder.objects.create(name="Root Intersection Folder 2", created_by=self.owner)
|
||||||
|
|
||||||
|
DocumentFolderAttachment.objects.create(
|
||||||
|
folder=root_folder,
|
||||||
|
content_type=ContentType.objects.get_for_model(TrafficLightIntersection),
|
||||||
|
object_id=intersection.pk,
|
||||||
|
)
|
||||||
|
|
||||||
|
other_doc = ManagedDocument.objects.create(title="Other User Doc 2", created_by=self.owner)
|
||||||
|
other_doc.folders.add(root_folder)
|
||||||
|
|
||||||
|
# Deletion should raise PermissionDenied because manager lacks edit rights on the thematic
|
||||||
|
with self.assertRaises(PermissionDenied):
|
||||||
|
ensure_document_deletable(other_doc, self.manager)
|
||||||
|
|
||||||
|
|
||||||
class SearchObjectsViewTests(TestCase):
|
class SearchObjectsViewTests(TestCase):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
|
|
@ -515,7 +573,6 @@ class DocumentGeoreferenceTests(TestCase):
|
||||||
url = reverse("documents:map_view", args=[self.document.pk])
|
url = reverse("documents:map_view", args=[self.document.pk])
|
||||||
response = self.client.get(url)
|
response = self.client.get(url)
|
||||||
self.assertEqual(response.status_code, 200)
|
self.assertEqual(response.status_code, 200)
|
||||||
self.assertContains(response, "Visualisation sur la carte")
|
|
||||||
|
|
||||||
|
|
||||||
from unittest.mock import patch
|
from unittest.mock import patch
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue