(backend) we want to display ancestors accesses on a document share

The document accesses a user have on a document's ancestors also apply
to this document. The frontend needs to list them as "inherited" so we
need to add them to the list.
Adding a "document_id" field on the output will allow the frontend to
differentiate between inherited and direct accesses on a document.
This commit is contained in:
Samuel Paccoud - DINUM
2025-04-12 13:43:30 +02:00
committed by Anthony LC
parent df2b953e53
commit fae024229e
5 changed files with 113 additions and 82 deletions

View File

@@ -10,6 +10,7 @@ and this project adheres to
### Added ### Added
- ✨(backend) include ancestors accesses on document accesses list view # 846
- ✨(backend) add ancestors links definitions to document abilities #846 - ✨(backend) add ancestors links definitions to document abilities #846
- ✨(frontend) add customization for translations #857 - ✨(frontend) add customization for translations #857
- ✨(frontend) Duplicate a doc #1078 - ✨(frontend) Duplicate a doc #1078

View File

@@ -97,7 +97,7 @@ class BaseAccessSerializer(serializers.ModelSerializer):
if not self.Meta.model.objects.filter( # pylint: disable=no-member if not self.Meta.model.objects.filter( # pylint: disable=no-member
Q(user=user) | Q(team__in=user.teams), Q(user=user) | Q(team__in=user.teams),
role__in=[models.RoleChoices.OWNER, models.RoleChoices.ADMIN], role__in=models.PRIVILEGED_ROLES,
**{self.Meta.resource_field_name: resource_id}, # pylint: disable=no-member **{self.Meta.resource_field_name: resource_id}, # pylint: disable=no-member
).exists(): ).exists():
raise exceptions.PermissionDenied( raise exceptions.PermissionDenied(
@@ -124,6 +124,10 @@ class BaseAccessSerializer(serializers.ModelSerializer):
class DocumentAccessSerializer(BaseAccessSerializer): class DocumentAccessSerializer(BaseAccessSerializer):
"""Serialize document accesses.""" """Serialize document accesses."""
document_id = serializers.PrimaryKeyRelatedField(
read_only=True,
source="document",
)
user_id = serializers.PrimaryKeyRelatedField( user_id = serializers.PrimaryKeyRelatedField(
queryset=models.User.objects.all(), queryset=models.User.objects.all(),
write_only=True, write_only=True,
@@ -136,11 +140,11 @@ class DocumentAccessSerializer(BaseAccessSerializer):
class Meta: class Meta:
model = models.DocumentAccess model = models.DocumentAccess
resource_field_name = "document" resource_field_name = "document"
fields = ["id", "user", "user_id", "team", "role", "abilities"] fields = ["id", "document_id", "user", "user_id", "team", "role", "abilities"]
read_only_fields = ["id", "abilities"] read_only_fields = ["id", "document_id", "abilities"]
class DocumentAccessLightSerializer(DocumentAccessSerializer): class DocumentAccessLightSerializer(BaseAccessSerializer):
"""Serialize document accesses with limited fields.""" """Serialize document accesses with limited fields."""
user = UserLightSerializer(read_only=True) user = UserLightSerializer(read_only=True)

View File

@@ -8,7 +8,6 @@ from urllib.parse import unquote, urlencode, urlparse
from django.conf import settings from django.conf import settings
from django.contrib.postgres.aggregates import ArrayAgg from django.contrib.postgres.aggregates import ArrayAgg
from django.contrib.postgres.fields import ArrayField
from django.contrib.postgres.search import TrigramSimilarity from django.contrib.postgres.search import TrigramSimilarity
from django.core.cache import cache from django.core.cache import cache
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
@@ -1511,12 +1510,10 @@ class DocumentAccessViewSet(
queryset = models.DocumentAccess.objects.select_related("user").all() queryset = models.DocumentAccess.objects.select_related("user").all()
resource_field_name = "document" resource_field_name = "document"
serializer_class = serializers.DocumentAccessSerializer serializer_class = serializers.DocumentAccessSerializer
is_current_user_owner_or_admin = False
def list(self, request, *args, **kwargs): def list(self, request, *args, **kwargs):
"""Return accesses for the current document with filters and annotations.""" """Return accesses for the current document with filters and annotations."""
user = self.request.user user = self.request.user
queryset = self.filter_queryset(self.get_queryset())
try: try:
document = models.Document.objects.get(pk=self.kwargs["resource_id"]) document = models.Document.objects.get(pk=self.kwargs["resource_id"])
@@ -1527,22 +1524,35 @@ class DocumentAccessViewSet(
if not roles: if not roles:
return drf.response.Response([]) return drf.response.Response([])
is_owner_or_admin = bool(roles.intersection(set(models.PRIVILEGED_ROLES))) ancestors = (
self.is_current_user_owner_or_admin = is_owner_or_admin (document.get_ancestors() | models.Document.objects.filter(pk=document.pk))
if not is_owner_or_admin: .filter(ancestors_deleted_at__isnull=True)
.order_by("path")
)
highest_readable = ancestors.readable_per_se(user).only("depth").first()
if highest_readable is None:
return drf.response.Response([])
queryset = self.get_queryset()
queryset = queryset.filter(
document__in=ancestors.filter(depth__gte=highest_readable.depth)
)
is_privileged = bool(roles.intersection(set(models.PRIVILEGED_ROLES)))
if is_privileged:
serializer_class = serializers.DocumentAccessSerializer
else:
# Return only the document's privileged accesses # Return only the document's privileged accesses
queryset = queryset.filter(role__in=models.PRIVILEGED_ROLES) queryset = queryset.filter(role__in=models.PRIVILEGED_ROLES)
serializer_class = serializers.DocumentAccessLightSerializer
queryset = queryset.distinct() queryset = queryset.distinct()
serializer = self.get_serializer(queryset, many=True) serializer = serializer_class(
queryset, many=True, context=self.get_serializer_context()
)
return drf.response.Response(serializer.data) return drf.response.Response(serializer.data)
def get_serializer_class(self):
if self.action == "list" and not self.is_current_user_owner_or_admin:
return serializers.DocumentAccessLightSerializer
return super().get_serializer_class()
def perform_create(self, serializer): def perform_create(self, serializer):
"""Add a new access to the document and send an email to the new added user.""" """Add a new access to the document and send an email to the new added user."""
access = serializer.save() access = serializer.save()

View File

@@ -76,22 +76,30 @@ def test_api_document_accesses_list_authenticated_related_non_privileged(
via, role, mock_user_teams via, role, mock_user_teams
): ):
""" """
Authenticated users should be able to list document accesses for a document Authenticated users with no privileged role should only be able to list document
to which they are directly related, whatever their role in the document. accesses associated with privileged roles for a document, including from ancestors.
""" """
user = factories.UserFactory() user = factories.UserFactory()
client = APIClient() client = APIClient()
client.force_login(user) client.force_login(user)
owner = factories.UserFactory() # Create documents structured as a tree
accesses = [] unreadable_ancestor = factories.DocumentFactory(link_reach="restricted")
# make all documents below the grand parent readable without a specific access for the user
document_access = factories.UserDocumentAccessFactory( grand_parent = factories.DocumentFactory(
user=owner, role=models.RoleChoices.OWNER parent=unreadable_ancestor, link_reach="authenticated"
) )
accesses.append(document_access) parent = factories.DocumentFactory(parent=grand_parent)
document = document_access.document document = factories.DocumentFactory(parent=parent)
child = factories.DocumentFactory(parent=document)
# Create accesses related to each document
factories.UserDocumentAccessFactory(document=unreadable_ancestor)
grand_parent_access = factories.UserDocumentAccessFactory(document=grand_parent)
parent_access = factories.UserDocumentAccessFactory(document=parent)
document_access = factories.UserDocumentAccessFactory(document=document)
factories.UserDocumentAccessFactory(document=child)
if via == USER: if via == USER:
models.DocumentAccess.objects.create( models.DocumentAccess.objects.create(
document=document, document=document,
@@ -108,8 +116,6 @@ def test_api_document_accesses_list_authenticated_related_non_privileged(
access1 = factories.TeamDocumentAccessFactory(document=document) access1 = factories.TeamDocumentAccessFactory(document=document)
access2 = factories.UserDocumentAccessFactory(document=document) access2 = factories.UserDocumentAccessFactory(document=document)
accesses.append(access1)
accesses.append(access2)
# Accesses for other documents to which the user is related should not be listed either # Accesses for other documents to which the user is related should not be listed either
other_access = factories.UserDocumentAccessFactory(user=user) other_access = factories.UserDocumentAccessFactory(user=user)
@@ -119,13 +125,16 @@ def test_api_document_accesses_list_authenticated_related_non_privileged(
f"/api/v1.0/documents/{document.id!s}/accesses/", f"/api/v1.0/documents/{document.id!s}/accesses/",
) )
# Return only privileged roles
privileged_accesses = [
access for access in accesses if access.role in models.PRIVILEGED_ROLES
]
assert response.status_code == 200 assert response.status_code == 200
content = response.json() content = response.json()
# Make sure only privileged roles are returned
accesses = [grand_parent_access, parent_access, document_access, access1, access2]
privileged_accesses = [
acc for acc in accesses if acc.role in models.PRIVILEGED_ROLES
]
assert len(content) == len(privileged_accesses) assert len(content) == len(privileged_accesses)
assert sorted(content, key=lambda x: x["id"]) == sorted( assert sorted(content, key=lambda x: x["id"]) == sorted(
[ [
{ {
@@ -147,33 +156,39 @@ def test_api_document_accesses_list_authenticated_related_non_privileged(
key=lambda x: x["id"], key=lambda x: x["id"],
) )
for access in content:
assert access["role"] in models.PRIVILEGED_ROLES
@pytest.mark.parametrize("via", VIA) @pytest.mark.parametrize("via", VIA)
@pytest.mark.parametrize("role", models.PRIVILEGED_ROLES) @pytest.mark.parametrize(
def test_api_document_accesses_list_authenticated_related_privileged_roles( "role", [role for role in models.RoleChoices if role in models.PRIVILEGED_ROLES]
)
def test_api_document_accesses_list_authenticated_related_privileged(
via, role, mock_user_teams via, role, mock_user_teams
): ):
""" """
Authenticated users should be able to list document accesses for a document Authenticated users with a privileged role should be able to list all
to which they are directly related, whatever their role in the document. document accesses whatever the role, including from ancestors.
""" """
user = factories.UserFactory() user = factories.UserFactory()
client = APIClient() client = APIClient()
client.force_login(user) client.force_login(user)
owner = factories.UserFactory() # Create documents structured as a tree
accesses = [] unreadable_ancestor = factories.DocumentFactory(link_reach="restricted")
# make all documents below the grand parent readable without a specific access for the user
document_access = factories.UserDocumentAccessFactory( grand_parent = factories.DocumentFactory(
user=owner, role=models.RoleChoices.OWNER parent=unreadable_ancestor, link_reach="authenticated"
) )
accesses.append(document_access) parent = factories.DocumentFactory(parent=grand_parent)
document = document_access.document document = factories.DocumentFactory(parent=parent)
user_access = None child = factories.DocumentFactory(parent=document)
# Create accesses related to each document
factories.UserDocumentAccessFactory(document=unreadable_ancestor)
grand_parent_access = factories.UserDocumentAccessFactory(document=grand_parent)
parent_access = factories.UserDocumentAccessFactory(document=parent)
document_access = factories.UserDocumentAccessFactory(document=document)
factories.UserDocumentAccessFactory(document=child)
if via == USER: if via == USER:
user_access = models.DocumentAccess.objects.create( user_access = models.DocumentAccess.objects.create(
document=document, document=document,
@@ -187,11 +202,11 @@ def test_api_document_accesses_list_authenticated_related_privileged_roles(
team="lasuite", team="lasuite",
role=role, role=role,
) )
else:
raise RuntimeError()
access1 = factories.TeamDocumentAccessFactory(document=document) access1 = factories.TeamDocumentAccessFactory(document=document)
access2 = factories.UserDocumentAccessFactory(document=document) access2 = factories.UserDocumentAccessFactory(document=document)
accesses.append(access1)
accesses.append(access2)
# Accesses for other documents to which the user is related should not be listed either # Accesses for other documents to which the user is related should not be listed either
other_access = factories.UserDocumentAccessFactory(user=user) other_access = factories.UserDocumentAccessFactory(user=user)
@@ -201,42 +216,39 @@ def test_api_document_accesses_list_authenticated_related_privileged_roles(
f"/api/v1.0/documents/{document.id!s}/accesses/", f"/api/v1.0/documents/{document.id!s}/accesses/",
) )
access2_user = serializers.UserSerializer(instance=access2.user).data
base_user = serializers.UserSerializer(instance=user).data
assert response.status_code == 200 assert response.status_code == 200
content = response.json() content = response.json()
assert len(content) == 4
# Make sure all expected accesses are returned
accesses = [
user_access,
grand_parent_access,
parent_access,
document_access,
access1,
access2,
]
assert len(content) == 6
assert sorted(content, key=lambda x: x["id"]) == sorted( assert sorted(content, key=lambda x: x["id"]) == sorted(
[ [
{ {
"id": str(user_access.id), "id": str(access.id),
"user": base_user if via == "user" else None, "document_id": str(access.document_id),
"team": "lasuite" if via == "team" else "", "user": {
"role": user_access.role, "id": str(access.user.id),
"abilities": user_access.get_abilities(user), "email": access.user.email,
}, "language": access.user.language,
{ "full_name": access.user.full_name,
"id": str(access1.id), "short_name": access.user.short_name,
"user": None, }
"team": access1.team, if access.user
"role": access1.role, else None,
"abilities": access1.get_abilities(user), "team": access.team,
}, "role": access.role,
{ "abilities": access.get_abilities(user),
"id": str(access2.id), }
"user": access2_user, for access in accesses
"team": "",
"role": access2.role,
"abilities": access2.get_abilities(user),
},
{
"id": str(document_access.id),
"user": serializers.UserSerializer(instance=owner).data,
"team": "",
"role": models.RoleChoices.OWNER,
"abilities": document_access.get_abilities(user),
},
], ],
key=lambda x: x["id"], key=lambda x: x["id"],
) )
@@ -331,6 +343,7 @@ def test_api_document_accesses_retrieve_authenticated_related(
assert response.status_code == 200 assert response.status_code == 200
assert response.json() == { assert response.json() == {
"id": str(access.id), "id": str(access.id),
"document_id": str(access.document_id),
"user": access_user, "user": access_user,
"team": "", "team": "",
"role": access.role, "role": access.role,

View File

@@ -165,6 +165,7 @@ def test_api_document_accesses_create_authenticated_administrator(via, mock_user
other_user = serializers.UserSerializer(instance=other_user).data other_user = serializers.UserSerializer(instance=other_user).data
assert response.json() == { assert response.json() == {
"abilities": new_document_access.get_abilities(user), "abilities": new_document_access.get_abilities(user),
"document_id": str(new_document_access.document_id),
"id": str(new_document_access.id), "id": str(new_document_access.id),
"team": "", "team": "",
"role": role, "role": role,
@@ -222,6 +223,7 @@ def test_api_document_accesses_create_authenticated_owner(via, mock_user_teams):
new_document_access = models.DocumentAccess.objects.filter(user=other_user).get() new_document_access = models.DocumentAccess.objects.filter(user=other_user).get()
other_user = serializers.UserSerializer(instance=other_user).data other_user = serializers.UserSerializer(instance=other_user).data
assert response.json() == { assert response.json() == {
"document_id": str(new_document_access.document_id),
"id": str(new_document_access.id), "id": str(new_document_access.id),
"user": other_user, "user": other_user,
"team": "", "team": "",
@@ -286,6 +288,7 @@ def test_api_document_accesses_create_email_in_receivers_language(via, mock_user
).get() ).get()
other_user_data = serializers.UserSerializer(instance=other_user).data other_user_data = serializers.UserSerializer(instance=other_user).data
assert response.json() == { assert response.json() == {
"document_id": str(new_document_access.document_id),
"id": str(new_document_access.id), "id": str(new_document_access.id),
"user": other_user_data, "user": other_user_data,
"team": "", "team": "",