✨(models/api) add document model and API
We do this by making copies of existing Template and TemplateAccess models and API. A little refactoring is done to try to limit duplicate code.
This commit is contained in:
committed by
Anthony LC
parent
0024cc5814
commit
3e0739cd0a
@@ -25,16 +25,11 @@ class UserSerializer(serializers.ModelSerializer):
|
|||||||
read_only_fields = ["id", "is_device", "is_staff"]
|
read_only_fields = ["id", "is_device", "is_staff"]
|
||||||
|
|
||||||
|
|
||||||
class TemplateAccessSerializer(serializers.ModelSerializer):
|
class BaseAccessSerializer(serializers.ModelSerializer):
|
||||||
"""Serialize template accesses."""
|
"""Serialize template accesses."""
|
||||||
|
|
||||||
abilities = serializers.SerializerMethodField(read_only=True)
|
abilities = serializers.SerializerMethodField(read_only=True)
|
||||||
|
|
||||||
class Meta:
|
|
||||||
model = models.TemplateAccess
|
|
||||||
fields = ["id", "user", "team", "role", "abilities"]
|
|
||||||
read_only_fields = ["id", "abilities"]
|
|
||||||
|
|
||||||
def update(self, instance, validated_data):
|
def update(self, instance, validated_data):
|
||||||
"""Make "user" field is readonly but only on update."""
|
"""Make "user" field is readonly but only on update."""
|
||||||
validated_data.pop("user", None)
|
validated_data.pop("user", None)
|
||||||
@@ -71,55 +66,88 @@ class TemplateAccessSerializer(serializers.ModelSerializer):
|
|||||||
else:
|
else:
|
||||||
teams = user.get_teams()
|
teams = user.get_teams()
|
||||||
try:
|
try:
|
||||||
template_id = self.context["template_id"]
|
resource_id = self.context["resource_id"]
|
||||||
except KeyError as exc:
|
except KeyError as exc:
|
||||||
raise exceptions.ValidationError(
|
raise exceptions.ValidationError(
|
||||||
"You must set a template ID in kwargs to create a new template access."
|
"You must set a resource ID in kwargs to create a new access."
|
||||||
) from exc
|
) from exc
|
||||||
|
|
||||||
if not models.TemplateAccess.objects.filter(
|
if not self.Meta.model.objects.filter( # pylint: disable=no-member
|
||||||
Q(user=user) | Q(team__in=teams),
|
Q(user=user) | Q(team__in=teams),
|
||||||
template=template_id,
|
|
||||||
role__in=[models.RoleChoices.OWNER, models.RoleChoices.ADMIN],
|
role__in=[models.RoleChoices.OWNER, models.RoleChoices.ADMIN],
|
||||||
).exists():
|
).exists():
|
||||||
raise exceptions.PermissionDenied(
|
raise exceptions.PermissionDenied(
|
||||||
"You are not allowed to manage accesses for this template."
|
"You are not allowed to manage accesses for this resource."
|
||||||
)
|
)
|
||||||
|
|
||||||
if (
|
if (
|
||||||
role == models.RoleChoices.OWNER
|
role == models.RoleChoices.OWNER
|
||||||
and not models.TemplateAccess.objects.filter(
|
and not self.Meta.model.objects.filter( # pylint: disable=no-member
|
||||||
Q(user=user) | Q(team__in=teams),
|
Q(user=user) | Q(team__in=teams),
|
||||||
template=template_id,
|
|
||||||
role=models.RoleChoices.OWNER,
|
role=models.RoleChoices.OWNER,
|
||||||
|
**{self.Meta.resource_field_name: resource_id}, # pylint: disable=no-member
|
||||||
).exists()
|
).exists()
|
||||||
):
|
):
|
||||||
raise exceptions.PermissionDenied(
|
raise exceptions.PermissionDenied(
|
||||||
"Only owners of a template can assign other users as owners."
|
"Only owners of a resource can assign other users as owners."
|
||||||
)
|
)
|
||||||
|
|
||||||
attrs["template_id"] = self.context["template_id"]
|
# pylint: disable=no-member
|
||||||
|
attrs[f"{self.Meta.resource_field_name}_id"] = self.context["resource_id"]
|
||||||
return attrs
|
return attrs
|
||||||
|
|
||||||
|
|
||||||
class TemplateSerializer(serializers.ModelSerializer):
|
class DocumentAccessSerializer(BaseAccessSerializer):
|
||||||
"""Serialize templates."""
|
"""Serialize document accesses."""
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = models.DocumentAccess
|
||||||
|
resource_field_name = "document"
|
||||||
|
fields = ["id", "user", "team", "role", "abilities"]
|
||||||
|
read_only_fields = ["id", "abilities"]
|
||||||
|
|
||||||
|
|
||||||
|
class TemplateAccessSerializer(BaseAccessSerializer):
|
||||||
|
"""Serialize template accesses."""
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = models.TemplateAccess
|
||||||
|
resource_field_name = "template"
|
||||||
|
fields = ["id", "user", "team", "role", "abilities"]
|
||||||
|
read_only_fields = ["id", "abilities"]
|
||||||
|
|
||||||
|
|
||||||
|
class BaseResourceSerializer(serializers.ModelSerializer):
|
||||||
|
"""Serialize documents."""
|
||||||
|
|
||||||
abilities = serializers.SerializerMethodField(read_only=True)
|
abilities = serializers.SerializerMethodField(read_only=True)
|
||||||
accesses = TemplateAccessSerializer(many=True, read_only=True)
|
accesses = TemplateAccessSerializer(many=True, read_only=True)
|
||||||
|
|
||||||
|
def get_abilities(self, document) -> dict:
|
||||||
|
"""Return abilities of the logged-in user on the instance."""
|
||||||
|
request = self.context.get("request")
|
||||||
|
if request:
|
||||||
|
return document.get_abilities(request.user)
|
||||||
|
return {}
|
||||||
|
|
||||||
|
|
||||||
|
class DocumentSerializer(BaseResourceSerializer):
|
||||||
|
"""Serialize documents."""
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = models.Document
|
||||||
|
fields = ["id", "title", "accesses", "abilities"]
|
||||||
|
read_only_fields = ["id", "accesses", "abilities"]
|
||||||
|
|
||||||
|
|
||||||
|
class TemplateSerializer(BaseResourceSerializer):
|
||||||
|
"""Serialize templates."""
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = models.Template
|
model = models.Template
|
||||||
fields = ["id", "title", "accesses", "abilities"]
|
fields = ["id", "title", "accesses", "abilities"]
|
||||||
read_only_fields = ["id", "accesses", "abilities"]
|
read_only_fields = ["id", "accesses", "abilities"]
|
||||||
|
|
||||||
def get_abilities(self, template) -> dict:
|
|
||||||
"""Return abilities of the logged-in user on the instance."""
|
|
||||||
request = self.context.get("request")
|
|
||||||
if request:
|
|
||||||
return template.get_abilities(request.user)
|
|
||||||
return {}
|
|
||||||
|
|
||||||
|
|
||||||
# pylint: disable=abstract-method
|
# pylint: disable=abstract-method
|
||||||
class DocumentGenerationSerializer(serializers.Serializer):
|
class DocumentGenerationSerializer(serializers.Serializer):
|
||||||
|
|||||||
@@ -135,7 +135,201 @@ class UserViewSet(
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class ResourceViewsetMixin:
|
||||||
|
"""Mixin with methods common to all resource viewsets that are managed with accesses."""
|
||||||
|
|
||||||
|
def get_queryset(self):
|
||||||
|
"""Custom queryset to get user related resources."""
|
||||||
|
queryset = super().get_queryset()
|
||||||
|
if not self.request.user.is_authenticated:
|
||||||
|
return queryset.filter(is_public=True)
|
||||||
|
|
||||||
|
user = self.request.user
|
||||||
|
teams = user.get_teams()
|
||||||
|
|
||||||
|
user_roles_query = (
|
||||||
|
self.access_model_class.objects.filter(
|
||||||
|
Q(user=user) | Q(team__in=teams),
|
||||||
|
**{self.resource_field_name: OuterRef("pk")},
|
||||||
|
)
|
||||||
|
.values(self.resource_field_name)
|
||||||
|
.annotate(roles_array=ArrayAgg("role"))
|
||||||
|
.values("roles_array")
|
||||||
|
)
|
||||||
|
return (
|
||||||
|
queryset.filter(
|
||||||
|
Q(accesses__user=user) | Q(accesses__team__in=teams) | Q(is_public=True)
|
||||||
|
)
|
||||||
|
.annotate(user_roles=Subquery(user_roles_query))
|
||||||
|
.distinct()
|
||||||
|
)
|
||||||
|
|
||||||
|
def perform_create(self, serializer):
|
||||||
|
"""Set the current user as owner of the newly created object."""
|
||||||
|
obj = serializer.save()
|
||||||
|
self.access_model_class.objects.create(
|
||||||
|
user=self.request.user,
|
||||||
|
role=models.RoleChoices.OWNER,
|
||||||
|
**{self.resource_field_name: obj},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class ResourceAccessViewsetMixin:
|
||||||
|
"""Mixin with methods common to all access viewsets."""
|
||||||
|
|
||||||
|
def get_permissions(self):
|
||||||
|
"""User only needs to be authenticated to list resource accesses"""
|
||||||
|
if self.action == "list":
|
||||||
|
permission_classes = [permissions.IsAuthenticated]
|
||||||
|
else:
|
||||||
|
return super().get_permissions()
|
||||||
|
|
||||||
|
return [permission() for permission in permission_classes]
|
||||||
|
|
||||||
|
def get_serializer_context(self):
|
||||||
|
"""Extra context provided to the serializer class."""
|
||||||
|
context = super().get_serializer_context()
|
||||||
|
context["resource_id"] = self.kwargs["resource_id"]
|
||||||
|
return context
|
||||||
|
|
||||||
|
def get_queryset(self):
|
||||||
|
"""Return the queryset according to the action."""
|
||||||
|
queryset = super().get_queryset()
|
||||||
|
queryset = queryset.filter(
|
||||||
|
**{self.resource_field_name: self.kwargs["resource_id"]}
|
||||||
|
)
|
||||||
|
|
||||||
|
if self.action == "list":
|
||||||
|
user = self.request.user
|
||||||
|
teams = user.get_teams()
|
||||||
|
|
||||||
|
user_roles_query = (
|
||||||
|
queryset.filter(
|
||||||
|
Q(user=user) | Q(team__in=teams),
|
||||||
|
**{self.resource_field_name: self.kwargs["resource_id"]},
|
||||||
|
)
|
||||||
|
.values(self.resource_field_name)
|
||||||
|
.annotate(roles_array=ArrayAgg("role"))
|
||||||
|
.values("roles_array")
|
||||||
|
)
|
||||||
|
|
||||||
|
# Limit to resource access instances related to a resource THAT also has
|
||||||
|
# a resource access
|
||||||
|
# instance for the logged-in user (we don't want to list only the resource
|
||||||
|
# access instances pointing to the logged-in user)
|
||||||
|
queryset = (
|
||||||
|
queryset.filter(
|
||||||
|
Q(**{f"{self.resource_field_name}__accesses__user": user})
|
||||||
|
| Q(**{f"{self.resource_field_name}__accesses__team__in": teams}),
|
||||||
|
**{self.resource_field_name: self.kwargs["resource_id"]},
|
||||||
|
)
|
||||||
|
.annotate(user_roles=Subquery(user_roles_query))
|
||||||
|
.distinct()
|
||||||
|
)
|
||||||
|
return queryset
|
||||||
|
|
||||||
|
def destroy(self, request, *args, **kwargs):
|
||||||
|
"""Forbid deleting the last owner access"""
|
||||||
|
instance = self.get_object()
|
||||||
|
resource = getattr(instance, self.resource_field_name)
|
||||||
|
|
||||||
|
# Check if the access being deleted is the last owner access for the resource
|
||||||
|
if (
|
||||||
|
instance.role == "owner"
|
||||||
|
and resource.accesses.filter(role="owner").count() == 1
|
||||||
|
):
|
||||||
|
return drf_response.Response(
|
||||||
|
{"detail": "Cannot delete the last owner access for the resource."},
|
||||||
|
status=403,
|
||||||
|
)
|
||||||
|
|
||||||
|
return super().destroy(request, *args, **kwargs)
|
||||||
|
|
||||||
|
def perform_update(self, serializer):
|
||||||
|
"""Check that we don't change the role if it leads to losing the last owner."""
|
||||||
|
instance = serializer.instance
|
||||||
|
|
||||||
|
# Check if the role is being updated and the new role is not "owner"
|
||||||
|
if (
|
||||||
|
"role" in self.request.data
|
||||||
|
and self.request.data["role"] != models.RoleChoices.OWNER
|
||||||
|
):
|
||||||
|
resource = getattr(instance, self.resource_field_name)
|
||||||
|
# Check if the access being updated is the last owner access for the resource
|
||||||
|
if (
|
||||||
|
instance.role == models.RoleChoices.OWNER
|
||||||
|
and resource.accesses.filter(role=models.RoleChoices.OWNER).count() == 1
|
||||||
|
):
|
||||||
|
message = "Cannot change the role to a non-owner role for the last owner access."
|
||||||
|
raise exceptions.PermissionDenied({"detail": message})
|
||||||
|
|
||||||
|
serializer.save()
|
||||||
|
|
||||||
|
|
||||||
|
class DocumentViewSet(
|
||||||
|
ResourceViewsetMixin,
|
||||||
|
mixins.CreateModelMixin,
|
||||||
|
mixins.DestroyModelMixin,
|
||||||
|
mixins.ListModelMixin,
|
||||||
|
mixins.RetrieveModelMixin,
|
||||||
|
mixins.UpdateModelMixin,
|
||||||
|
viewsets.GenericViewSet,
|
||||||
|
):
|
||||||
|
"""Document ViewSet"""
|
||||||
|
|
||||||
|
permission_classes = [
|
||||||
|
permissions.IsAuthenticatedOrSafe,
|
||||||
|
permissions.AccessPermission,
|
||||||
|
]
|
||||||
|
serializer_class = serializers.DocumentSerializer
|
||||||
|
access_model_class = models.DocumentAccess
|
||||||
|
resource_field_name = "document"
|
||||||
|
queryset = models.Document.objects.all()
|
||||||
|
|
||||||
|
|
||||||
|
class DocumentAccessViewSet(
|
||||||
|
ResourceAccessViewsetMixin,
|
||||||
|
mixins.CreateModelMixin,
|
||||||
|
mixins.DestroyModelMixin,
|
||||||
|
mixins.ListModelMixin,
|
||||||
|
mixins.RetrieveModelMixin,
|
||||||
|
mixins.UpdateModelMixin,
|
||||||
|
viewsets.GenericViewSet,
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
API ViewSet for all interactions with document accesses.
|
||||||
|
|
||||||
|
GET /api/v1.0/documents/<resource_id>/accesses/:<document_access_id>
|
||||||
|
Return list of all document accesses related to the logged-in user or one
|
||||||
|
document access if an id is provided.
|
||||||
|
|
||||||
|
POST /api/v1.0/documents/<resource_id>/accesses/ with expected data:
|
||||||
|
- user: str
|
||||||
|
- role: str [owner|admin|member]
|
||||||
|
Return newly created document access
|
||||||
|
|
||||||
|
PUT /api/v1.0/documents/<resource_id>/accesses/<document_access_id>/ with expected data:
|
||||||
|
- role: str [owner|admin|member]
|
||||||
|
Return updated document access
|
||||||
|
|
||||||
|
PATCH /api/v1.0/documents/<resource_id>/accesses/<document_access_id>/ with expected data:
|
||||||
|
- role: str [owner|admin|member]
|
||||||
|
Return partially updated document access
|
||||||
|
|
||||||
|
DELETE /api/v1.0/documents/<resource_id>/accesses/<document_access_id>/
|
||||||
|
Delete targeted document access
|
||||||
|
"""
|
||||||
|
|
||||||
|
lookup_field = "pk"
|
||||||
|
pagination_class = Pagination
|
||||||
|
permission_classes = [permissions.IsAuthenticated, permissions.AccessPermission]
|
||||||
|
queryset = models.DocumentAccess.objects.select_related("user").all()
|
||||||
|
resource_field_name = "document"
|
||||||
|
serializer_class = serializers.DocumentAccessSerializer
|
||||||
|
|
||||||
|
|
||||||
class TemplateViewSet(
|
class TemplateViewSet(
|
||||||
|
ResourceViewsetMixin,
|
||||||
mixins.CreateModelMixin,
|
mixins.CreateModelMixin,
|
||||||
mixins.DestroyModelMixin,
|
mixins.DestroyModelMixin,
|
||||||
mixins.ListModelMixin,
|
mixins.ListModelMixin,
|
||||||
@@ -150,41 +344,10 @@ class TemplateViewSet(
|
|||||||
permissions.AccessPermission,
|
permissions.AccessPermission,
|
||||||
]
|
]
|
||||||
serializer_class = serializers.TemplateSerializer
|
serializer_class = serializers.TemplateSerializer
|
||||||
|
access_model_class = models.TemplateAccess
|
||||||
|
resource_field_name = "template"
|
||||||
queryset = models.Template.objects.all()
|
queryset = models.Template.objects.all()
|
||||||
|
|
||||||
def get_queryset(self):
|
|
||||||
"""Custom queryset to get user related templates."""
|
|
||||||
if not self.request.user.is_authenticated:
|
|
||||||
return models.Template.objects.filter(is_public=True)
|
|
||||||
|
|
||||||
user = self.request.user
|
|
||||||
teams = user.get_teams()
|
|
||||||
|
|
||||||
user_roles_query = (
|
|
||||||
models.TemplateAccess.objects.filter(
|
|
||||||
Q(user=user) | Q(team__in=teams), template=OuterRef("pk")
|
|
||||||
)
|
|
||||||
.values("template")
|
|
||||||
.annotate(roles_array=ArrayAgg("role"))
|
|
||||||
.values("roles_array")
|
|
||||||
)
|
|
||||||
return (
|
|
||||||
models.Template.objects.filter(
|
|
||||||
Q(accesses__user=user) | Q(accesses__team__in=teams) | Q(is_public=True)
|
|
||||||
)
|
|
||||||
.annotate(user_roles=Subquery(user_roles_query))
|
|
||||||
.distinct()
|
|
||||||
)
|
|
||||||
|
|
||||||
def perform_create(self, serializer):
|
|
||||||
"""Set the current user as owner of the newly created template."""
|
|
||||||
template = serializer.save()
|
|
||||||
models.TemplateAccess.objects.create(
|
|
||||||
template=template,
|
|
||||||
user=self.request.user,
|
|
||||||
role=models.RoleChoices.OWNER,
|
|
||||||
)
|
|
||||||
|
|
||||||
@decorators.action(
|
@decorators.action(
|
||||||
detail=True,
|
detail=True,
|
||||||
methods=["post"],
|
methods=["post"],
|
||||||
@@ -214,6 +377,7 @@ class TemplateViewSet(
|
|||||||
|
|
||||||
|
|
||||||
class TemplateAccessViewSet(
|
class TemplateAccessViewSet(
|
||||||
|
ResourceAccessViewsetMixin,
|
||||||
mixins.CreateModelMixin,
|
mixins.CreateModelMixin,
|
||||||
mixins.DestroyModelMixin,
|
mixins.DestroyModelMixin,
|
||||||
mixins.ListModelMixin,
|
mixins.ListModelMixin,
|
||||||
@@ -248,91 +412,6 @@ class TemplateAccessViewSet(
|
|||||||
lookup_field = "pk"
|
lookup_field = "pk"
|
||||||
pagination_class = Pagination
|
pagination_class = Pagination
|
||||||
permission_classes = [permissions.IsAuthenticated, permissions.AccessPermission]
|
permission_classes = [permissions.IsAuthenticated, permissions.AccessPermission]
|
||||||
queryset = models.TemplateAccess.objects.all().select_related("user")
|
queryset = models.TemplateAccess.objects.select_related("user").all()
|
||||||
|
resource_field_name = "template"
|
||||||
serializer_class = serializers.TemplateAccessSerializer
|
serializer_class = serializers.TemplateAccessSerializer
|
||||||
|
|
||||||
def get_permissions(self):
|
|
||||||
"""User only needs to be authenticated to list template accesses"""
|
|
||||||
if self.action == "list":
|
|
||||||
permission_classes = [permissions.IsAuthenticated]
|
|
||||||
else:
|
|
||||||
return super().get_permissions()
|
|
||||||
|
|
||||||
return [permission() for permission in permission_classes]
|
|
||||||
|
|
||||||
def get_serializer_context(self):
|
|
||||||
"""Extra context provided to the serializer class."""
|
|
||||||
context = super().get_serializer_context()
|
|
||||||
context["template_id"] = self.kwargs["template_id"]
|
|
||||||
return context
|
|
||||||
|
|
||||||
def get_queryset(self):
|
|
||||||
"""Return the queryset according to the action."""
|
|
||||||
queryset = super().get_queryset()
|
|
||||||
queryset = queryset.filter(template=self.kwargs["template_id"])
|
|
||||||
|
|
||||||
if self.action == "list":
|
|
||||||
user = self.request.user
|
|
||||||
teams = user.get_teams()
|
|
||||||
|
|
||||||
user_roles_query = (
|
|
||||||
models.TemplateAccess.objects.filter(
|
|
||||||
Q(user=user) | Q(team__in=teams),
|
|
||||||
template=self.kwargs["template_id"],
|
|
||||||
)
|
|
||||||
.values("template")
|
|
||||||
.annotate(roles_array=ArrayAgg("role"))
|
|
||||||
.values("roles_array")
|
|
||||||
)
|
|
||||||
|
|
||||||
# Limit to template access instances related to a template THAT also has
|
|
||||||
# a template access
|
|
||||||
# instance for the logged-in user (we don't want to list only the template
|
|
||||||
# access instances pointing to the logged-in user)
|
|
||||||
queryset = (
|
|
||||||
queryset.filter(
|
|
||||||
Q(template__accesses__user=user)
|
|
||||||
| Q(template__accesses__team__in=teams),
|
|
||||||
template=self.kwargs["template_id"],
|
|
||||||
)
|
|
||||||
.annotate(user_roles=Subquery(user_roles_query))
|
|
||||||
.distinct()
|
|
||||||
)
|
|
||||||
return queryset
|
|
||||||
|
|
||||||
def destroy(self, request, *args, **kwargs):
|
|
||||||
"""Forbid deleting the last owner access"""
|
|
||||||
instance = self.get_object()
|
|
||||||
template = instance.template
|
|
||||||
|
|
||||||
# Check if the access being deleted is the last owner access for the template
|
|
||||||
if (
|
|
||||||
instance.role == "owner"
|
|
||||||
and template.accesses.filter(role="owner").count() == 1
|
|
||||||
):
|
|
||||||
return drf_response.Response(
|
|
||||||
{"detail": "Cannot delete the last owner access for the template."},
|
|
||||||
status=403,
|
|
||||||
)
|
|
||||||
|
|
||||||
return super().destroy(request, *args, **kwargs)
|
|
||||||
|
|
||||||
def perform_update(self, serializer):
|
|
||||||
"""Check that we don't change the role if it leads to losing the last owner."""
|
|
||||||
instance = serializer.instance
|
|
||||||
|
|
||||||
# Check if the role is being updated and the new role is not "owner"
|
|
||||||
if (
|
|
||||||
"role" in self.request.data
|
|
||||||
and self.request.data["role"] != models.RoleChoices.OWNER
|
|
||||||
):
|
|
||||||
template = instance.template
|
|
||||||
# Check if the access being updated is the last owner access for the template
|
|
||||||
if (
|
|
||||||
instance.role == models.RoleChoices.OWNER
|
|
||||||
and template.accesses.filter(role=models.RoleChoices.OWNER).count() == 1
|
|
||||||
):
|
|
||||||
message = "Cannot change the role to a non-owner role for the last owner access."
|
|
||||||
raise exceptions.PermissionDenied({"detail": message})
|
|
||||||
|
|
||||||
serializer.save()
|
|
||||||
|
|||||||
@@ -25,12 +25,57 @@ class UserFactory(factory.django.DjangoModelFactory):
|
|||||||
password = make_password("password")
|
password = make_password("password")
|
||||||
|
|
||||||
|
|
||||||
|
class DocumentFactory(factory.django.DjangoModelFactory):
|
||||||
|
"""A factory to create documents"""
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = models.Document
|
||||||
|
django_get_or_create = ("title",)
|
||||||
|
skip_postgeneration_save = True
|
||||||
|
|
||||||
|
title = factory.Sequence(lambda n: f"document{n}")
|
||||||
|
is_public = factory.Faker("boolean")
|
||||||
|
|
||||||
|
@factory.post_generation
|
||||||
|
def users(self, create, extracted, **kwargs):
|
||||||
|
"""Add users to document from a given list of users with or without roles."""
|
||||||
|
if create and extracted:
|
||||||
|
for item in extracted:
|
||||||
|
if isinstance(item, models.User):
|
||||||
|
UserDocumentAccessFactory(document=self, user=item)
|
||||||
|
else:
|
||||||
|
UserDocumentAccessFactory(document=self, user=item[0], role=item[1])
|
||||||
|
|
||||||
|
|
||||||
|
class UserDocumentAccessFactory(factory.django.DjangoModelFactory):
|
||||||
|
"""Create fake document user accesses for testing."""
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = models.DocumentAccess
|
||||||
|
|
||||||
|
document = factory.SubFactory(DocumentFactory)
|
||||||
|
user = factory.SubFactory(UserFactory)
|
||||||
|
role = factory.fuzzy.FuzzyChoice([r[0] for r in models.RoleChoices.choices])
|
||||||
|
|
||||||
|
|
||||||
|
class TeamDocumentAccessFactory(factory.django.DjangoModelFactory):
|
||||||
|
"""Create fake document team accesses for testing."""
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = models.DocumentAccess
|
||||||
|
|
||||||
|
document = factory.SubFactory(DocumentFactory)
|
||||||
|
team = factory.Sequence(lambda n: f"team{n}")
|
||||||
|
role = factory.fuzzy.FuzzyChoice([r[0] for r in models.RoleChoices.choices])
|
||||||
|
|
||||||
|
|
||||||
class TemplateFactory(factory.django.DjangoModelFactory):
|
class TemplateFactory(factory.django.DjangoModelFactory):
|
||||||
"""A factory to create templates"""
|
"""A factory to create templates"""
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = models.Template
|
model = models.Template
|
||||||
django_get_or_create = ("title",)
|
django_get_or_create = ("title",)
|
||||||
|
skip_postgeneration_save = True
|
||||||
|
|
||||||
title = factory.Sequence(lambda n: f"template{n}")
|
title = factory.Sequence(lambda n: f"template{n}")
|
||||||
is_public = factory.Faker("boolean")
|
is_public = factory.Faker("boolean")
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
# Generated by Django 5.0.2 on 2024-02-24 17:39
|
# Generated by Django 5.0.2 on 2024-03-31 18:49
|
||||||
|
|
||||||
import django.contrib.auth.models
|
import django.contrib.auth.models
|
||||||
import django.core.validators
|
import django.core.validators
|
||||||
@@ -18,6 +18,22 @@ class Migration(migrations.Migration):
|
|||||||
]
|
]
|
||||||
|
|
||||||
operations = [
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='Document',
|
||||||
|
fields=[
|
||||||
|
('id', models.UUIDField(default=uuid.uuid4, editable=False, help_text='primary key for the record as UUID', primary_key=True, serialize=False, verbose_name='id')),
|
||||||
|
('created_at', models.DateTimeField(auto_now_add=True, help_text='date and time at which a record was created', verbose_name='created on')),
|
||||||
|
('updated_at', models.DateTimeField(auto_now=True, help_text='date and time at which a record was last updated', verbose_name='updated on')),
|
||||||
|
('title', models.CharField(max_length=255, verbose_name='title')),
|
||||||
|
('is_public', models.BooleanField(default=False, help_text='Whether this document is public for anyone to use.', verbose_name='public')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'verbose_name': 'Document',
|
||||||
|
'verbose_name_plural': 'Documents',
|
||||||
|
'db_table': 'impress_document',
|
||||||
|
'ordering': ('title',),
|
||||||
|
},
|
||||||
|
),
|
||||||
migrations.CreateModel(
|
migrations.CreateModel(
|
||||||
name='Template',
|
name='Template',
|
||||||
fields=[
|
fields=[
|
||||||
@@ -66,6 +82,24 @@ class Migration(migrations.Migration):
|
|||||||
('objects', django.contrib.auth.models.UserManager()),
|
('objects', django.contrib.auth.models.UserManager()),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='DocumentAccess',
|
||||||
|
fields=[
|
||||||
|
('id', models.UUIDField(default=uuid.uuid4, editable=False, help_text='primary key for the record as UUID', primary_key=True, serialize=False, verbose_name='id')),
|
||||||
|
('created_at', models.DateTimeField(auto_now_add=True, help_text='date and time at which a record was created', verbose_name='created on')),
|
||||||
|
('updated_at', models.DateTimeField(auto_now=True, help_text='date and time at which a record was last updated', verbose_name='updated on')),
|
||||||
|
('team', models.CharField(blank=True, max_length=100)),
|
||||||
|
('role', models.CharField(choices=[('member', 'Member'), ('administrator', 'Administrator'), ('owner', 'Owner')], default='member', max_length=20)),
|
||||||
|
('document', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='accesses', to='core.document')),
|
||||||
|
('user', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'verbose_name': 'Document/user relation',
|
||||||
|
'verbose_name_plural': 'Document/user relations',
|
||||||
|
'db_table': 'impress_document_access',
|
||||||
|
'ordering': ('-created_at',),
|
||||||
|
},
|
||||||
|
),
|
||||||
migrations.CreateModel(
|
migrations.CreateModel(
|
||||||
name='TemplateAccess',
|
name='TemplateAccess',
|
||||||
fields=[
|
fields=[
|
||||||
@@ -75,14 +109,27 @@ class Migration(migrations.Migration):
|
|||||||
('team', models.CharField(blank=True, max_length=100)),
|
('team', models.CharField(blank=True, max_length=100)),
|
||||||
('role', models.CharField(choices=[('member', 'Member'), ('administrator', 'Administrator'), ('owner', 'Owner')], default='member', max_length=20)),
|
('role', models.CharField(choices=[('member', 'Member'), ('administrator', 'Administrator'), ('owner', 'Owner')], default='member', max_length=20)),
|
||||||
('template', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='accesses', to='core.template')),
|
('template', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='accesses', to='core.template')),
|
||||||
('user', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='accesses', to=settings.AUTH_USER_MODEL)),
|
('user', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
|
||||||
],
|
],
|
||||||
options={
|
options={
|
||||||
'verbose_name': 'Template/user relation',
|
'verbose_name': 'Template/user relation',
|
||||||
'verbose_name_plural': 'Template/user relations',
|
'verbose_name_plural': 'Template/user relations',
|
||||||
'db_table': 'impress_template_access',
|
'db_table': 'impress_template_access',
|
||||||
|
'ordering': ('-created_at',),
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
migrations.AddConstraint(
|
||||||
|
model_name='documentaccess',
|
||||||
|
constraint=models.UniqueConstraint(condition=models.Q(('user__isnull', False)), fields=('user', 'document'), name='unique_document_user', violation_error_message='This user is already in this document.'),
|
||||||
|
),
|
||||||
|
migrations.AddConstraint(
|
||||||
|
model_name='documentaccess',
|
||||||
|
constraint=models.UniqueConstraint(condition=models.Q(('team__gt', '')), fields=('team', 'document'), name='unique_document_team', violation_error_message='This team is already in this document.'),
|
||||||
|
),
|
||||||
|
migrations.AddConstraint(
|
||||||
|
model_name='documentaccess',
|
||||||
|
constraint=models.CheckConstraint(check=models.Q(models.Q(('team', ''), ('user__isnull', False)), models.Q(('team__gt', ''), ('user__isnull', True)), _connector='OR'), name='check_document_access_either_user_or_team', violation_error_message='Either user or team must be set, not both.'),
|
||||||
|
),
|
||||||
migrations.AddConstraint(
|
migrations.AddConstraint(
|
||||||
model_name='templateaccess',
|
model_name='templateaccess',
|
||||||
constraint=models.UniqueConstraint(condition=models.Q(('user__isnull', False)), fields=('user', 'template'), name='unique_template_user', violation_error_message='This user is already in this template.'),
|
constraint=models.UniqueConstraint(condition=models.Q(('user__isnull', False)), fields=('user', 'template'), name='unique_template_user', violation_error_message='This user is already in this template.'),
|
||||||
@@ -93,6 +140,6 @@ class Migration(migrations.Migration):
|
|||||||
),
|
),
|
||||||
migrations.AddConstraint(
|
migrations.AddConstraint(
|
||||||
model_name='templateaccess',
|
model_name='templateaccess',
|
||||||
constraint=models.CheckConstraint(check=models.Q(models.Q(('team', ''), ('user__isnull', False)), models.Q(('team__gt', ''), ('user__isnull', True)), _connector='OR'), name='check_either_user_or_team', violation_error_message='Either user or team must be set, not both.'),
|
constraint=models.CheckConstraint(check=models.Q(models.Q(('team', ''), ('user__isnull', False)), models.Q(('team__gt', ''), ('user__isnull', True)), _connector='OR'), name='check_template_access_either_user_or_team', violation_error_message='Either user or team must be set, not both.'),
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -22,6 +22,23 @@ from weasyprint import CSS, HTML
|
|||||||
from weasyprint.text.fonts import FontConfiguration
|
from weasyprint.text.fonts import FontConfiguration
|
||||||
|
|
||||||
|
|
||||||
|
def get_resource_roles(resource, user):
|
||||||
|
"""Compute the roles a user has on a resource."""
|
||||||
|
roles = []
|
||||||
|
if user.is_authenticated:
|
||||||
|
try:
|
||||||
|
roles = resource.user_roles or []
|
||||||
|
except AttributeError:
|
||||||
|
teams = user.get_teams()
|
||||||
|
try:
|
||||||
|
roles = resource.accesses.filter(
|
||||||
|
models.Q(user=user) | models.Q(team__in=teams),
|
||||||
|
).values_list("role", flat=True)
|
||||||
|
except (models.ObjectDoesNotExist, IndexError):
|
||||||
|
roles = []
|
||||||
|
return roles
|
||||||
|
|
||||||
|
|
||||||
class RoleChoices(models.TextChoices):
|
class RoleChoices(models.TextChoices):
|
||||||
"""Defines the possible roles a user can have in a template."""
|
"""Defines the possible roles a user can have in a template."""
|
||||||
|
|
||||||
@@ -156,6 +173,154 @@ class User(AbstractBaseUser, BaseModel, auth_models.PermissionsMixin):
|
|||||||
return []
|
return []
|
||||||
|
|
||||||
|
|
||||||
|
class BaseAccess(BaseModel):
|
||||||
|
"""Base model for accesses to handle resources."""
|
||||||
|
|
||||||
|
user = models.ForeignKey(
|
||||||
|
User,
|
||||||
|
on_delete=models.CASCADE,
|
||||||
|
null=True,
|
||||||
|
blank=True,
|
||||||
|
)
|
||||||
|
team = models.CharField(max_length=100, blank=True)
|
||||||
|
role = models.CharField(
|
||||||
|
max_length=20, choices=RoleChoices.choices, default=RoleChoices.MEMBER
|
||||||
|
)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
abstract = True
|
||||||
|
|
||||||
|
def _get_abilities(self, resource, user):
|
||||||
|
"""
|
||||||
|
Compute and return abilities for a given user taking into account
|
||||||
|
the current state of the object.
|
||||||
|
"""
|
||||||
|
roles = []
|
||||||
|
if user.is_authenticated:
|
||||||
|
teams = user.get_teams()
|
||||||
|
try:
|
||||||
|
roles = self.user_roles or []
|
||||||
|
except AttributeError:
|
||||||
|
try:
|
||||||
|
roles = resource.accesses.filter(
|
||||||
|
models.Q(user=user) | models.Q(team__in=teams),
|
||||||
|
).values_list("role", flat=True)
|
||||||
|
except (self._meta.model.DoesNotExist, IndexError):
|
||||||
|
roles = []
|
||||||
|
|
||||||
|
is_owner_or_admin = bool(
|
||||||
|
set(roles).intersection({RoleChoices.OWNER, RoleChoices.ADMIN})
|
||||||
|
)
|
||||||
|
if self.role == RoleChoices.OWNER:
|
||||||
|
can_delete = (
|
||||||
|
RoleChoices.OWNER in roles
|
||||||
|
and resource.accesses.filter(role=RoleChoices.OWNER).count() > 1
|
||||||
|
)
|
||||||
|
set_role_to = [RoleChoices.ADMIN, RoleChoices.MEMBER] if can_delete else []
|
||||||
|
else:
|
||||||
|
can_delete = is_owner_or_admin
|
||||||
|
set_role_to = []
|
||||||
|
if RoleChoices.OWNER in roles:
|
||||||
|
set_role_to.append(RoleChoices.OWNER)
|
||||||
|
if is_owner_or_admin:
|
||||||
|
set_role_to.extend([RoleChoices.ADMIN, RoleChoices.MEMBER])
|
||||||
|
|
||||||
|
# Remove the current role as we don't want to propose it as an option
|
||||||
|
try:
|
||||||
|
set_role_to.remove(self.role)
|
||||||
|
except ValueError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
return {
|
||||||
|
"destroy": can_delete,
|
||||||
|
"update": bool(set_role_to),
|
||||||
|
"retrieve": bool(roles),
|
||||||
|
"set_role_to": set_role_to,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class Document(BaseModel):
|
||||||
|
"""Pad document carrying the content."""
|
||||||
|
|
||||||
|
title = models.CharField(_("title"), max_length=255)
|
||||||
|
is_public = models.BooleanField(
|
||||||
|
_("public"),
|
||||||
|
default=False,
|
||||||
|
help_text=_("Whether this document is public for anyone to use."),
|
||||||
|
)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
db_table = "impress_document"
|
||||||
|
ordering = ("title",)
|
||||||
|
verbose_name = _("Document")
|
||||||
|
verbose_name_plural = _("Documents")
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return self.title
|
||||||
|
|
||||||
|
def get_abilities(self, user):
|
||||||
|
"""
|
||||||
|
Compute and return abilities for a given user on the document.
|
||||||
|
"""
|
||||||
|
roles = get_resource_roles(self, user)
|
||||||
|
is_owner_or_admin = bool(
|
||||||
|
set(roles).intersection({RoleChoices.OWNER, RoleChoices.ADMIN})
|
||||||
|
)
|
||||||
|
can_get = self.is_public or bool(roles)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"destroy": RoleChoices.OWNER in roles,
|
||||||
|
"manage_accesses": is_owner_or_admin,
|
||||||
|
"update": is_owner_or_admin,
|
||||||
|
"retrieve": can_get,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class DocumentAccess(BaseAccess):
|
||||||
|
"""Relation model to give access to a document for a user or a team with a role."""
|
||||||
|
|
||||||
|
document = models.ForeignKey(
|
||||||
|
Document,
|
||||||
|
on_delete=models.CASCADE,
|
||||||
|
related_name="accesses",
|
||||||
|
)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
db_table = "impress_document_access"
|
||||||
|
ordering = ("-created_at",)
|
||||||
|
verbose_name = _("Document/user relation")
|
||||||
|
verbose_name_plural = _("Document/user relations")
|
||||||
|
constraints = [
|
||||||
|
models.UniqueConstraint(
|
||||||
|
fields=["user", "document"],
|
||||||
|
condition=models.Q(user__isnull=False), # Exclude null users
|
||||||
|
name="unique_document_user",
|
||||||
|
violation_error_message=_("This user is already in this document."),
|
||||||
|
),
|
||||||
|
models.UniqueConstraint(
|
||||||
|
fields=["team", "document"],
|
||||||
|
condition=models.Q(team__gt=""), # Exclude empty string teams
|
||||||
|
name="unique_document_team",
|
||||||
|
violation_error_message=_("This team is already in this document."),
|
||||||
|
),
|
||||||
|
models.CheckConstraint(
|
||||||
|
check=models.Q(user__isnull=False, team="")
|
||||||
|
| models.Q(user__isnull=True, team__gt=""),
|
||||||
|
name="check_document_access_either_user_or_team",
|
||||||
|
violation_error_message=_("Either user or team must be set, not both."),
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return f"{self.user!s} is {self.role:s} in document {self.document!s}"
|
||||||
|
|
||||||
|
def get_abilities(self, user):
|
||||||
|
"""
|
||||||
|
Compute and return abilities for a given user on the document access.
|
||||||
|
"""
|
||||||
|
return self._get_abilities(self.document, user)
|
||||||
|
|
||||||
|
|
||||||
class Template(BaseModel):
|
class Template(BaseModel):
|
||||||
"""HTML and CSS code used for formatting the print around the MarkDown body."""
|
"""HTML and CSS code used for formatting the print around the MarkDown body."""
|
||||||
|
|
||||||
@@ -178,6 +343,24 @@ class Template(BaseModel):
|
|||||||
def __str__(self):
|
def __str__(self):
|
||||||
return self.title
|
return self.title
|
||||||
|
|
||||||
|
def get_abilities(self, user):
|
||||||
|
"""
|
||||||
|
Compute and return abilities for a given user on the template.
|
||||||
|
"""
|
||||||
|
roles = get_resource_roles(self, user)
|
||||||
|
is_owner_or_admin = bool(
|
||||||
|
set(roles).intersection({RoleChoices.OWNER, RoleChoices.ADMIN})
|
||||||
|
)
|
||||||
|
can_get = self.is_public or bool(roles)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"destroy": RoleChoices.OWNER in roles,
|
||||||
|
"generate_document": can_get,
|
||||||
|
"manage_accesses": is_owner_or_admin,
|
||||||
|
"update": is_owner_or_admin,
|
||||||
|
"retrieve": can_get,
|
||||||
|
}
|
||||||
|
|
||||||
def generate_document(self, body):
|
def generate_document(self, body):
|
||||||
"""
|
"""
|
||||||
Generate and return a PDF document for this template around the
|
Generate and return a PDF document for this template around the
|
||||||
@@ -201,38 +384,8 @@ class Template(BaseModel):
|
|||||||
)
|
)
|
||||||
return document_html.write_pdf(stylesheets=[css], zoom=1)
|
return document_html.write_pdf(stylesheets=[css], zoom=1)
|
||||||
|
|
||||||
def get_abilities(self, user):
|
|
||||||
"""
|
|
||||||
Compute and return abilities for a given user on the template.
|
|
||||||
"""
|
|
||||||
# Compute user role
|
|
||||||
roles = []
|
|
||||||
if user.is_authenticated:
|
|
||||||
try:
|
|
||||||
roles = self.user_roles or []
|
|
||||||
except AttributeError:
|
|
||||||
teams = user.get_teams()
|
|
||||||
try:
|
|
||||||
roles = self.accesses.filter(
|
|
||||||
models.Q(user=user) | models.Q(team__in=teams)
|
|
||||||
).values_list("role", flat=True)
|
|
||||||
except (TemplateAccess.DoesNotExist, IndexError):
|
|
||||||
roles = []
|
|
||||||
is_owner_or_admin = bool(
|
|
||||||
set(roles).intersection({RoleChoices.OWNER, RoleChoices.ADMIN})
|
|
||||||
)
|
|
||||||
can_get = self.is_public or bool(roles)
|
|
||||||
|
|
||||||
return {
|
class TemplateAccess(BaseAccess):
|
||||||
"destroy": RoleChoices.OWNER in roles,
|
|
||||||
"generate_document": can_get,
|
|
||||||
"manage_accesses": is_owner_or_admin,
|
|
||||||
"update": is_owner_or_admin,
|
|
||||||
"retrieve": can_get,
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
class TemplateAccess(BaseModel):
|
|
||||||
"""Relation model to give access to a template for a user or a team with a role."""
|
"""Relation model to give access to a template for a user or a team with a role."""
|
||||||
|
|
||||||
template = models.ForeignKey(
|
template = models.ForeignKey(
|
||||||
@@ -240,20 +393,10 @@ class TemplateAccess(BaseModel):
|
|||||||
on_delete=models.CASCADE,
|
on_delete=models.CASCADE,
|
||||||
related_name="accesses",
|
related_name="accesses",
|
||||||
)
|
)
|
||||||
user = models.ForeignKey(
|
|
||||||
User,
|
|
||||||
on_delete=models.CASCADE,
|
|
||||||
related_name="accesses",
|
|
||||||
null=True,
|
|
||||||
blank=True,
|
|
||||||
)
|
|
||||||
team = models.CharField(max_length=100, blank=True)
|
|
||||||
role = models.CharField(
|
|
||||||
max_length=20, choices=RoleChoices.choices, default=RoleChoices.MEMBER
|
|
||||||
)
|
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
db_table = "impress_template_access"
|
db_table = "impress_template_access"
|
||||||
|
ordering = ("-created_at",)
|
||||||
verbose_name = _("Template/user relation")
|
verbose_name = _("Template/user relation")
|
||||||
verbose_name_plural = _("Template/user relations")
|
verbose_name_plural = _("Template/user relations")
|
||||||
constraints = [
|
constraints = [
|
||||||
@@ -272,7 +415,7 @@ class TemplateAccess(BaseModel):
|
|||||||
models.CheckConstraint(
|
models.CheckConstraint(
|
||||||
check=models.Q(user__isnull=False, team="")
|
check=models.Q(user__isnull=False, team="")
|
||||||
| models.Q(user__isnull=True, team__gt=""),
|
| models.Q(user__isnull=True, team__gt=""),
|
||||||
name="check_either_user_or_team",
|
name="check_template_access_either_user_or_team",
|
||||||
violation_error_message=_("Either user or team must be set, not both."),
|
violation_error_message=_("Either user or team must be set, not both."),
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
@@ -282,51 +425,6 @@ class TemplateAccess(BaseModel):
|
|||||||
|
|
||||||
def get_abilities(self, user):
|
def get_abilities(self, user):
|
||||||
"""
|
"""
|
||||||
Compute and return abilities for a given user taking into account
|
Compute and return abilities for a given user on the template access.
|
||||||
the current state of the object.
|
|
||||||
"""
|
"""
|
||||||
is_template_owner_or_admin = False
|
return self._get_abilities(self.template, user)
|
||||||
|
|
||||||
roles = []
|
|
||||||
if user.is_authenticated:
|
|
||||||
teams = user.get_teams()
|
|
||||||
try:
|
|
||||||
roles = self.user_roles or []
|
|
||||||
except AttributeError:
|
|
||||||
try:
|
|
||||||
roles = self._meta.model.objects.filter(
|
|
||||||
models.Q(user=user) | models.Q(team__in=teams),
|
|
||||||
template=self.template_id,
|
|
||||||
).values_list("role", flat=True)
|
|
||||||
except (self._meta.model.DoesNotExist, IndexError):
|
|
||||||
roles = []
|
|
||||||
|
|
||||||
is_template_owner_or_admin = bool(
|
|
||||||
set(roles).intersection({RoleChoices.OWNER, RoleChoices.ADMIN})
|
|
||||||
)
|
|
||||||
if self.role == RoleChoices.OWNER:
|
|
||||||
can_delete = (
|
|
||||||
RoleChoices.OWNER in roles
|
|
||||||
and self.template.accesses.filter(role=RoleChoices.OWNER).count() > 1
|
|
||||||
)
|
|
||||||
set_role_to = [RoleChoices.ADMIN, RoleChoices.MEMBER] if can_delete else []
|
|
||||||
else:
|
|
||||||
can_delete = is_template_owner_or_admin
|
|
||||||
set_role_to = []
|
|
||||||
if RoleChoices.OWNER in roles:
|
|
||||||
set_role_to.append(RoleChoices.OWNER)
|
|
||||||
if is_template_owner_or_admin:
|
|
||||||
set_role_to.extend([RoleChoices.ADMIN, RoleChoices.MEMBER])
|
|
||||||
|
|
||||||
# Remove the current role as we don't want to propose it as an option
|
|
||||||
try:
|
|
||||||
set_role_to.remove(self.role)
|
|
||||||
except ValueError:
|
|
||||||
pass
|
|
||||||
|
|
||||||
return {
|
|
||||||
"destroy": can_delete,
|
|
||||||
"update": bool(set_role_to),
|
|
||||||
"retrieve": bool(roles),
|
|
||||||
"set_role_to": set_role_to,
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -0,0 +1,47 @@
|
|||||||
|
"""
|
||||||
|
Tests for Documents API endpoint in impress's core app: create
|
||||||
|
"""
|
||||||
|
import pytest
|
||||||
|
from rest_framework.test import APIClient
|
||||||
|
|
||||||
|
from core import factories
|
||||||
|
from core.models import Document
|
||||||
|
|
||||||
|
pytestmark = pytest.mark.django_db
|
||||||
|
|
||||||
|
|
||||||
|
def test_api_documents_create_anonymous():
|
||||||
|
"""Anonymous users should not be allowed to create documents."""
|
||||||
|
response = APIClient().post(
|
||||||
|
"/api/v1.0/documents/",
|
||||||
|
{
|
||||||
|
"title": "my document",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
assert response.status_code == 401
|
||||||
|
assert not Document.objects.exists()
|
||||||
|
|
||||||
|
|
||||||
|
def test_api_documents_create_authenticated():
|
||||||
|
"""
|
||||||
|
Authenticated users should be able to create documents and should automatically be declared
|
||||||
|
as the owner of the newly created document.
|
||||||
|
"""
|
||||||
|
user = factories.UserFactory()
|
||||||
|
|
||||||
|
client = APIClient()
|
||||||
|
client.force_login(user)
|
||||||
|
|
||||||
|
response = client.post(
|
||||||
|
"/api/v1.0/documents/",
|
||||||
|
{
|
||||||
|
"title": "my document",
|
||||||
|
},
|
||||||
|
format="json",
|
||||||
|
)
|
||||||
|
|
||||||
|
assert response.status_code == 201
|
||||||
|
document = Document.objects.get()
|
||||||
|
assert document.title == "my document"
|
||||||
|
assert document.accesses.filter(role="owner", user=user).exists()
|
||||||
106
src/backend/core/tests/documents/test_api_documents_delete.py
Normal file
106
src/backend/core/tests/documents/test_api_documents_delete.py
Normal file
@@ -0,0 +1,106 @@
|
|||||||
|
"""
|
||||||
|
Tests for Documents API endpoint in impress's core app: delete
|
||||||
|
"""
|
||||||
|
import random
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
from rest_framework.test import APIClient
|
||||||
|
|
||||||
|
from core import factories, models
|
||||||
|
from core.tests.conftest import TEAM, USER, VIA
|
||||||
|
|
||||||
|
pytestmark = pytest.mark.django_db
|
||||||
|
|
||||||
|
|
||||||
|
def test_api_documents_delete_anonymous():
|
||||||
|
"""Anonymous users should not be allowed to destroy a document."""
|
||||||
|
document = factories.DocumentFactory()
|
||||||
|
|
||||||
|
response = APIClient().delete(
|
||||||
|
f"/api/v1.0/documents/{document.id!s}/",
|
||||||
|
)
|
||||||
|
|
||||||
|
assert response.status_code == 401
|
||||||
|
assert models.Document.objects.count() == 1
|
||||||
|
|
||||||
|
|
||||||
|
def test_api_documents_delete_authenticated_unrelated():
|
||||||
|
"""
|
||||||
|
Authenticated users should not be allowed to delete a document to which they are not
|
||||||
|
related.
|
||||||
|
"""
|
||||||
|
user = factories.UserFactory()
|
||||||
|
|
||||||
|
client = APIClient()
|
||||||
|
client.force_login(user)
|
||||||
|
|
||||||
|
is_public = random.choice([True, False])
|
||||||
|
document = factories.DocumentFactory(is_public=is_public)
|
||||||
|
|
||||||
|
response = client.delete(
|
||||||
|
f"/api/v1.0/documents/{document.id!s}/",
|
||||||
|
)
|
||||||
|
|
||||||
|
assert response.status_code == 403 if is_public else 404
|
||||||
|
assert models.Document.objects.count() == 1
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("role", ["member", "administrator"])
|
||||||
|
@pytest.mark.parametrize("via", VIA)
|
||||||
|
def test_api_documents_delete_authenticated_member_or_administrator(
|
||||||
|
via, role, mock_user_get_teams
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Authenticated users should not be allowed to delete a document for which they are
|
||||||
|
only a member or administrator.
|
||||||
|
"""
|
||||||
|
user = factories.UserFactory()
|
||||||
|
|
||||||
|
client = APIClient()
|
||||||
|
client.force_login(user)
|
||||||
|
|
||||||
|
document = factories.DocumentFactory()
|
||||||
|
if via == USER:
|
||||||
|
factories.UserDocumentAccessFactory(document=document, user=user, role=role)
|
||||||
|
elif via == TEAM:
|
||||||
|
mock_user_get_teams.return_value = ["lasuite", "unknown"]
|
||||||
|
factories.TeamDocumentAccessFactory(
|
||||||
|
document=document, team="lasuite", role=role
|
||||||
|
)
|
||||||
|
|
||||||
|
response = client.delete(
|
||||||
|
f"/api/v1.0/documents/{document.id}/",
|
||||||
|
)
|
||||||
|
|
||||||
|
assert response.status_code == 403
|
||||||
|
assert response.json() == {
|
||||||
|
"detail": "You do not have permission to perform this action."
|
||||||
|
}
|
||||||
|
assert models.Document.objects.count() == 1
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("via", VIA)
|
||||||
|
def test_api_documents_delete_authenticated_owner(via, mock_user_get_teams):
|
||||||
|
"""
|
||||||
|
Authenticated users should be able to delete a document they own.
|
||||||
|
"""
|
||||||
|
user = factories.UserFactory()
|
||||||
|
|
||||||
|
client = APIClient()
|
||||||
|
client.force_login(user)
|
||||||
|
|
||||||
|
document = factories.DocumentFactory()
|
||||||
|
if via == USER:
|
||||||
|
factories.UserDocumentAccessFactory(document=document, user=user, role="owner")
|
||||||
|
elif via == TEAM:
|
||||||
|
mock_user_get_teams.return_value = ["lasuite", "unknown"]
|
||||||
|
factories.TeamDocumentAccessFactory(
|
||||||
|
document=document, team="lasuite", role="owner"
|
||||||
|
)
|
||||||
|
|
||||||
|
response = client.delete(
|
||||||
|
f"/api/v1.0/documents/{document.id}/",
|
||||||
|
)
|
||||||
|
|
||||||
|
assert response.status_code == 204
|
||||||
|
assert models.Document.objects.exists() is False
|
||||||
166
src/backend/core/tests/documents/test_api_documents_list.py
Normal file
166
src/backend/core/tests/documents/test_api_documents_list.py
Normal file
@@ -0,0 +1,166 @@
|
|||||||
|
"""
|
||||||
|
Tests for Documents API endpoint in impress's core app: list
|
||||||
|
"""
|
||||||
|
from unittest import mock
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
from rest_framework.pagination import PageNumberPagination
|
||||||
|
from rest_framework.status import HTTP_200_OK
|
||||||
|
from rest_framework.test import APIClient
|
||||||
|
|
||||||
|
from core import factories
|
||||||
|
|
||||||
|
pytestmark = pytest.mark.django_db
|
||||||
|
|
||||||
|
|
||||||
|
def test_api_documents_list_anonymous():
|
||||||
|
"""Anonymous users should only be able to list public documents."""
|
||||||
|
factories.DocumentFactory.create_batch(2, is_public=False)
|
||||||
|
documents = factories.DocumentFactory.create_batch(2, is_public=True)
|
||||||
|
expected_ids = {str(document.id) for document in documents}
|
||||||
|
|
||||||
|
response = APIClient().get("/api/v1.0/documents/")
|
||||||
|
|
||||||
|
assert response.status_code == HTTP_200_OK
|
||||||
|
results = response.json()["results"]
|
||||||
|
assert len(results) == 2
|
||||||
|
results_id = {result["id"] for result in results}
|
||||||
|
assert expected_ids == results_id
|
||||||
|
|
||||||
|
|
||||||
|
def test_api_documents_list_authenticated_direct():
|
||||||
|
"""
|
||||||
|
Authenticated users should be able to list documents they are a direct
|
||||||
|
owner/administrator/member of.
|
||||||
|
"""
|
||||||
|
user = factories.UserFactory()
|
||||||
|
|
||||||
|
client = APIClient()
|
||||||
|
client.force_login(user)
|
||||||
|
|
||||||
|
related_documents = [
|
||||||
|
access.document
|
||||||
|
for access in factories.UserDocumentAccessFactory.create_batch(5, user=user)
|
||||||
|
]
|
||||||
|
public_documents = factories.DocumentFactory.create_batch(2, is_public=True)
|
||||||
|
factories.DocumentFactory.create_batch(2, is_public=False)
|
||||||
|
|
||||||
|
expected_ids = {
|
||||||
|
str(document.id) for document in related_documents + public_documents
|
||||||
|
}
|
||||||
|
|
||||||
|
response = client.get(
|
||||||
|
"/api/v1.0/documents/",
|
||||||
|
)
|
||||||
|
|
||||||
|
assert response.status_code == HTTP_200_OK
|
||||||
|
results = response.json()["results"]
|
||||||
|
assert len(results) == 7
|
||||||
|
results_id = {result["id"] for result in results}
|
||||||
|
assert expected_ids == results_id
|
||||||
|
|
||||||
|
|
||||||
|
def test_api_documents_list_authenticated_via_team(mock_user_get_teams):
|
||||||
|
"""
|
||||||
|
Authenticated users should be able to list documents they are a
|
||||||
|
owner/administrator/member of via a team.
|
||||||
|
"""
|
||||||
|
user = factories.UserFactory()
|
||||||
|
|
||||||
|
client = APIClient()
|
||||||
|
client.force_login(user)
|
||||||
|
|
||||||
|
mock_user_get_teams.return_value = ["team1", "team2", "unknown"]
|
||||||
|
|
||||||
|
documents_team1 = [
|
||||||
|
access.document
|
||||||
|
for access in factories.TeamDocumentAccessFactory.create_batch(2, team="team1")
|
||||||
|
]
|
||||||
|
documents_team2 = [
|
||||||
|
access.document
|
||||||
|
for access in factories.TeamDocumentAccessFactory.create_batch(3, team="team2")
|
||||||
|
]
|
||||||
|
public_documents = factories.DocumentFactory.create_batch(2, is_public=True)
|
||||||
|
factories.DocumentFactory.create_batch(2, is_public=False)
|
||||||
|
|
||||||
|
expected_ids = {
|
||||||
|
str(document.id)
|
||||||
|
for document in documents_team1 + documents_team2 + public_documents
|
||||||
|
}
|
||||||
|
|
||||||
|
response = client.get("/api/v1.0/documents/")
|
||||||
|
|
||||||
|
assert response.status_code == HTTP_200_OK
|
||||||
|
results = response.json()["results"]
|
||||||
|
assert len(results) == 7
|
||||||
|
results_id = {result["id"] for result in results}
|
||||||
|
assert expected_ids == results_id
|
||||||
|
|
||||||
|
|
||||||
|
@mock.patch.object(PageNumberPagination, "get_page_size", return_value=2)
|
||||||
|
def test_api_documents_list_pagination(
|
||||||
|
_mock_page_size,
|
||||||
|
):
|
||||||
|
"""Pagination should work as expected."""
|
||||||
|
user = factories.UserFactory()
|
||||||
|
|
||||||
|
client = APIClient()
|
||||||
|
client.force_login(user)
|
||||||
|
|
||||||
|
document_ids = [
|
||||||
|
str(access.document_id)
|
||||||
|
for access in factories.UserDocumentAccessFactory.create_batch(3, user=user)
|
||||||
|
]
|
||||||
|
|
||||||
|
# Get page 1
|
||||||
|
response = client.get(
|
||||||
|
"/api/v1.0/documents/",
|
||||||
|
)
|
||||||
|
|
||||||
|
assert response.status_code == HTTP_200_OK
|
||||||
|
content = response.json()
|
||||||
|
|
||||||
|
assert content["count"] == 3
|
||||||
|
assert content["next"] == "http://testserver/api/v1.0/documents/?page=2"
|
||||||
|
assert content["previous"] is None
|
||||||
|
|
||||||
|
assert len(content["results"]) == 2
|
||||||
|
for item in content["results"]:
|
||||||
|
document_ids.remove(item["id"])
|
||||||
|
|
||||||
|
# Get page 2
|
||||||
|
response = client.get(
|
||||||
|
"/api/v1.0/documents/?page=2",
|
||||||
|
)
|
||||||
|
|
||||||
|
assert response.status_code == HTTP_200_OK
|
||||||
|
content = response.json()
|
||||||
|
|
||||||
|
assert content["count"] == 3
|
||||||
|
assert content["next"] is None
|
||||||
|
assert content["previous"] == "http://testserver/api/v1.0/documents/"
|
||||||
|
|
||||||
|
assert len(content["results"]) == 1
|
||||||
|
document_ids.remove(content["results"][0]["id"])
|
||||||
|
assert document_ids == []
|
||||||
|
|
||||||
|
|
||||||
|
def test_api_documents_list_authenticated_distinct():
|
||||||
|
"""A document with several related users should only be listed once."""
|
||||||
|
user = factories.UserFactory()
|
||||||
|
|
||||||
|
client = APIClient()
|
||||||
|
client.force_login(user)
|
||||||
|
|
||||||
|
other_user = factories.UserFactory()
|
||||||
|
|
||||||
|
document = factories.DocumentFactory(users=[user, other_user], is_public=True)
|
||||||
|
|
||||||
|
response = client.get(
|
||||||
|
"/api/v1.0/documents/",
|
||||||
|
)
|
||||||
|
|
||||||
|
assert response.status_code == HTTP_200_OK
|
||||||
|
content = response.json()
|
||||||
|
assert len(content["results"]) == 1
|
||||||
|
assert content["results"][0]["id"] == str(document.id)
|
||||||
441
src/backend/core/tests/documents/test_api_documents_retrieve.py
Normal file
441
src/backend/core/tests/documents/test_api_documents_retrieve.py
Normal file
@@ -0,0 +1,441 @@
|
|||||||
|
"""
|
||||||
|
Tests for Documents API endpoint in impress's core app: retrieve
|
||||||
|
"""
|
||||||
|
import pytest
|
||||||
|
from rest_framework.test import APIClient
|
||||||
|
|
||||||
|
from core import factories
|
||||||
|
|
||||||
|
pytestmark = pytest.mark.django_db
|
||||||
|
|
||||||
|
|
||||||
|
def test_api_documents_retrieve_anonymous_public():
|
||||||
|
"""Anonymous users should be allowed to retrieve public documents."""
|
||||||
|
document = factories.DocumentFactory(is_public=True)
|
||||||
|
|
||||||
|
response = APIClient().get(f"/api/v1.0/documents/{document.id!s}/")
|
||||||
|
|
||||||
|
assert response.status_code == 200
|
||||||
|
assert response.json() == {
|
||||||
|
"id": str(document.id),
|
||||||
|
"abilities": {
|
||||||
|
"destroy": False,
|
||||||
|
"manage_accesses": False,
|
||||||
|
"retrieve": True,
|
||||||
|
"update": False,
|
||||||
|
},
|
||||||
|
"accesses": [],
|
||||||
|
"title": document.title,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def test_api_documents_retrieve_anonymous_not_public():
|
||||||
|
"""Anonymous users should not be able to retrieve a document that is not public."""
|
||||||
|
document = factories.DocumentFactory(is_public=False)
|
||||||
|
|
||||||
|
response = APIClient().get(f"/api/v1.0/documents/{document.id!s}/")
|
||||||
|
|
||||||
|
assert response.status_code == 404
|
||||||
|
assert response.json() == {"detail": "Not found."}
|
||||||
|
|
||||||
|
|
||||||
|
def test_api_documents_retrieve_authenticated_unrelated_public():
|
||||||
|
"""
|
||||||
|
Authenticated users should be able to retrieve a public document to which they are
|
||||||
|
not related.
|
||||||
|
"""
|
||||||
|
user = factories.UserFactory()
|
||||||
|
|
||||||
|
client = APIClient()
|
||||||
|
client.force_login(user)
|
||||||
|
|
||||||
|
document = factories.DocumentFactory(is_public=True)
|
||||||
|
|
||||||
|
response = client.get(
|
||||||
|
f"/api/v1.0/documents/{document.id!s}/",
|
||||||
|
)
|
||||||
|
assert response.status_code == 200
|
||||||
|
assert response.json() == {
|
||||||
|
"id": str(document.id),
|
||||||
|
"abilities": {
|
||||||
|
"destroy": False,
|
||||||
|
"manage_accesses": False,
|
||||||
|
"retrieve": True,
|
||||||
|
"update": False,
|
||||||
|
},
|
||||||
|
"accesses": [],
|
||||||
|
"title": document.title,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def test_api_documents_retrieve_authenticated_unrelated_not_public():
|
||||||
|
"""
|
||||||
|
Authenticated users should not be allowed to retrieve a document that is not public and
|
||||||
|
to which they are not related.
|
||||||
|
"""
|
||||||
|
user = factories.UserFactory()
|
||||||
|
|
||||||
|
client = APIClient()
|
||||||
|
client.force_login(user)
|
||||||
|
|
||||||
|
document = factories.DocumentFactory(is_public=False)
|
||||||
|
|
||||||
|
response = client.get(
|
||||||
|
f"/api/v1.0/documents/{document.id!s}/",
|
||||||
|
)
|
||||||
|
assert response.status_code == 404
|
||||||
|
assert response.json() == {"detail": "Not found."}
|
||||||
|
|
||||||
|
|
||||||
|
def test_api_documents_retrieve_authenticated_related_direct():
|
||||||
|
"""
|
||||||
|
Authenticated users should be allowed to retrieve a document to which they
|
||||||
|
are directly related whatever the role.
|
||||||
|
"""
|
||||||
|
user = factories.UserFactory()
|
||||||
|
|
||||||
|
client = APIClient()
|
||||||
|
client.force_login(user)
|
||||||
|
|
||||||
|
document = factories.DocumentFactory()
|
||||||
|
access1 = factories.UserDocumentAccessFactory(document=document, user=user)
|
||||||
|
access2 = factories.UserDocumentAccessFactory(document=document)
|
||||||
|
|
||||||
|
response = client.get(
|
||||||
|
f"/api/v1.0/documents/{document.id!s}/",
|
||||||
|
)
|
||||||
|
assert response.status_code == 200
|
||||||
|
content = response.json()
|
||||||
|
assert sorted(content.pop("accesses"), key=lambda x: x["user"]) == sorted(
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"id": str(access1.id),
|
||||||
|
"user": str(user.id),
|
||||||
|
"team": "",
|
||||||
|
"role": access1.role,
|
||||||
|
"abilities": access1.get_abilities(user),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": str(access2.id),
|
||||||
|
"user": str(access2.user.id),
|
||||||
|
"team": "",
|
||||||
|
"role": access2.role,
|
||||||
|
"abilities": access2.get_abilities(user),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
key=lambda x: x["user"],
|
||||||
|
)
|
||||||
|
assert response.json() == {
|
||||||
|
"id": str(document.id),
|
||||||
|
"title": document.title,
|
||||||
|
"abilities": document.get_abilities(user),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def test_api_documents_retrieve_authenticated_related_team_none(mock_user_get_teams):
|
||||||
|
"""
|
||||||
|
Authenticated users should not be able to retrieve a document related to teams in
|
||||||
|
which the user is not.
|
||||||
|
"""
|
||||||
|
mock_user_get_teams.return_value = []
|
||||||
|
|
||||||
|
user = factories.UserFactory()
|
||||||
|
|
||||||
|
client = APIClient()
|
||||||
|
client.force_login(user)
|
||||||
|
|
||||||
|
document = factories.DocumentFactory(is_public=False)
|
||||||
|
|
||||||
|
factories.TeamDocumentAccessFactory(
|
||||||
|
document=document, team="members", role="member"
|
||||||
|
)
|
||||||
|
factories.TeamDocumentAccessFactory(
|
||||||
|
document=document, team="administrators", role="administrator"
|
||||||
|
)
|
||||||
|
factories.TeamDocumentAccessFactory(document=document, team="owners", role="owner")
|
||||||
|
factories.TeamDocumentAccessFactory(document=document)
|
||||||
|
factories.TeamDocumentAccessFactory()
|
||||||
|
|
||||||
|
response = client.get(f"/api/v1.0/documents/{document.id!s}/")
|
||||||
|
assert response.status_code == 404
|
||||||
|
assert response.json() == {"detail": "Not found."}
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"teams",
|
||||||
|
[
|
||||||
|
["members"],
|
||||||
|
["unknown", "members"],
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_api_documents_retrieve_authenticated_related_team_members(
|
||||||
|
teams, mock_user_get_teams
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Authenticated users should be allowed to retrieve a document to which they
|
||||||
|
are related via a team whatever the role and see all its accesses.
|
||||||
|
"""
|
||||||
|
mock_user_get_teams.return_value = teams
|
||||||
|
|
||||||
|
user = factories.UserFactory()
|
||||||
|
|
||||||
|
client = APIClient()
|
||||||
|
client.force_login(user)
|
||||||
|
|
||||||
|
document = factories.DocumentFactory(is_public=False)
|
||||||
|
|
||||||
|
access_member = factories.TeamDocumentAccessFactory(
|
||||||
|
document=document, team="members", role="member"
|
||||||
|
)
|
||||||
|
access_administrator = factories.TeamDocumentAccessFactory(
|
||||||
|
document=document, team="administrators", role="administrator"
|
||||||
|
)
|
||||||
|
access_owner = factories.TeamDocumentAccessFactory(
|
||||||
|
document=document, team="owners", role="owner"
|
||||||
|
)
|
||||||
|
other_access = factories.TeamDocumentAccessFactory(document=document)
|
||||||
|
factories.TeamDocumentAccessFactory()
|
||||||
|
|
||||||
|
response = client.get(f"/api/v1.0/documents/{document.id!s}/")
|
||||||
|
assert response.status_code == 200
|
||||||
|
content = response.json()
|
||||||
|
expected_abilities = {
|
||||||
|
"destroy": False,
|
||||||
|
"retrieve": True,
|
||||||
|
"set_role_to": [],
|
||||||
|
"update": False,
|
||||||
|
}
|
||||||
|
assert sorted(content.pop("accesses"), key=lambda x: x["id"]) == sorted(
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"id": str(access_member.id),
|
||||||
|
"user": None,
|
||||||
|
"team": "members",
|
||||||
|
"role": access_member.role,
|
||||||
|
"abilities": expected_abilities,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": str(access_administrator.id),
|
||||||
|
"user": None,
|
||||||
|
"team": "administrators",
|
||||||
|
"role": access_administrator.role,
|
||||||
|
"abilities": expected_abilities,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": str(access_owner.id),
|
||||||
|
"user": None,
|
||||||
|
"team": "owners",
|
||||||
|
"role": access_owner.role,
|
||||||
|
"abilities": expected_abilities,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": str(other_access.id),
|
||||||
|
"user": None,
|
||||||
|
"team": other_access.team,
|
||||||
|
"role": other_access.role,
|
||||||
|
"abilities": expected_abilities,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
key=lambda x: x["id"],
|
||||||
|
)
|
||||||
|
assert response.json() == {
|
||||||
|
"id": str(document.id),
|
||||||
|
"title": document.title,
|
||||||
|
"abilities": document.get_abilities(user),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"teams",
|
||||||
|
[
|
||||||
|
["administrators"],
|
||||||
|
["members", "administrators"],
|
||||||
|
["unknown", "administrators"],
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_api_documents_retrieve_authenticated_related_team_administrators(
|
||||||
|
teams, mock_user_get_teams
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Authenticated users should be allowed to retrieve a document to which they
|
||||||
|
are related via a team whatever the role and see all its accesses.
|
||||||
|
"""
|
||||||
|
mock_user_get_teams.return_value = teams
|
||||||
|
|
||||||
|
user = factories.UserFactory()
|
||||||
|
|
||||||
|
client = APIClient()
|
||||||
|
client.force_login(user)
|
||||||
|
|
||||||
|
document = factories.DocumentFactory(is_public=False)
|
||||||
|
|
||||||
|
access_member = factories.TeamDocumentAccessFactory(
|
||||||
|
document=document, team="members", role="member"
|
||||||
|
)
|
||||||
|
access_administrator = factories.TeamDocumentAccessFactory(
|
||||||
|
document=document, team="administrators", role="administrator"
|
||||||
|
)
|
||||||
|
access_owner = factories.TeamDocumentAccessFactory(
|
||||||
|
document=document, team="owners", role="owner"
|
||||||
|
)
|
||||||
|
other_access = factories.TeamDocumentAccessFactory(document=document)
|
||||||
|
factories.TeamDocumentAccessFactory()
|
||||||
|
|
||||||
|
response = client.get(f"/api/v1.0/documents/{document.id!s}/")
|
||||||
|
|
||||||
|
# pylint: disable=R0801
|
||||||
|
assert response.status_code == 200
|
||||||
|
content = response.json()
|
||||||
|
assert sorted(content.pop("accesses"), key=lambda x: x["id"]) == sorted(
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"id": str(access_member.id),
|
||||||
|
"user": None,
|
||||||
|
"team": "members",
|
||||||
|
"role": "member",
|
||||||
|
"abilities": {
|
||||||
|
"destroy": True,
|
||||||
|
"retrieve": True,
|
||||||
|
"set_role_to": ["administrator"],
|
||||||
|
"update": True,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": str(access_administrator.id),
|
||||||
|
"user": None,
|
||||||
|
"team": "administrators",
|
||||||
|
"role": "administrator",
|
||||||
|
"abilities": {
|
||||||
|
"destroy": True,
|
||||||
|
"retrieve": True,
|
||||||
|
"set_role_to": ["member"],
|
||||||
|
"update": True,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": str(access_owner.id),
|
||||||
|
"user": None,
|
||||||
|
"team": "owners",
|
||||||
|
"role": "owner",
|
||||||
|
"abilities": {
|
||||||
|
"destroy": False,
|
||||||
|
"retrieve": True,
|
||||||
|
"set_role_to": [],
|
||||||
|
"update": False,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": str(other_access.id),
|
||||||
|
"user": None,
|
||||||
|
"team": other_access.team,
|
||||||
|
"role": other_access.role,
|
||||||
|
"abilities": other_access.get_abilities(user),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
key=lambda x: x["id"],
|
||||||
|
)
|
||||||
|
assert response.json() == {
|
||||||
|
"id": str(document.id),
|
||||||
|
"title": document.title,
|
||||||
|
"abilities": document.get_abilities(user),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"teams",
|
||||||
|
[
|
||||||
|
["owners"],
|
||||||
|
["owners", "administrators"],
|
||||||
|
["members", "administrators", "owners"],
|
||||||
|
["unknown", "owners"],
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_api_documents_retrieve_authenticated_related_team_owners(
|
||||||
|
teams, mock_user_get_teams
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Authenticated users should be allowed to retrieve a document to which they
|
||||||
|
are related via a team whatever the role and see all its accesses.
|
||||||
|
"""
|
||||||
|
mock_user_get_teams.return_value = teams
|
||||||
|
|
||||||
|
user = factories.UserFactory()
|
||||||
|
|
||||||
|
client = APIClient()
|
||||||
|
client.force_login(user)
|
||||||
|
|
||||||
|
document = factories.DocumentFactory(is_public=False)
|
||||||
|
|
||||||
|
access_member = factories.TeamDocumentAccessFactory(
|
||||||
|
document=document, team="members", role="member"
|
||||||
|
)
|
||||||
|
access_administrator = factories.TeamDocumentAccessFactory(
|
||||||
|
document=document, team="administrators", role="administrator"
|
||||||
|
)
|
||||||
|
access_owner = factories.TeamDocumentAccessFactory(
|
||||||
|
document=document, team="owners", role="owner"
|
||||||
|
)
|
||||||
|
other_access = factories.TeamDocumentAccessFactory(document=document)
|
||||||
|
factories.TeamDocumentAccessFactory()
|
||||||
|
|
||||||
|
response = client.get(f"/api/v1.0/documents/{document.id!s}/")
|
||||||
|
|
||||||
|
# pylint: disable=R0801
|
||||||
|
assert response.status_code == 200
|
||||||
|
content = response.json()
|
||||||
|
assert sorted(content.pop("accesses"), key=lambda x: x["id"]) == sorted(
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"id": str(access_member.id),
|
||||||
|
"user": None,
|
||||||
|
"team": "members",
|
||||||
|
"role": "member",
|
||||||
|
"abilities": {
|
||||||
|
"destroy": True,
|
||||||
|
"retrieve": True,
|
||||||
|
"set_role_to": ["owner", "administrator"],
|
||||||
|
"update": True,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": str(access_administrator.id),
|
||||||
|
"user": None,
|
||||||
|
"team": "administrators",
|
||||||
|
"role": "administrator",
|
||||||
|
"abilities": {
|
||||||
|
"destroy": True,
|
||||||
|
"retrieve": True,
|
||||||
|
"set_role_to": ["owner", "member"],
|
||||||
|
"update": True,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": str(access_owner.id),
|
||||||
|
"user": None,
|
||||||
|
"team": "owners",
|
||||||
|
"role": "owner",
|
||||||
|
"abilities": {
|
||||||
|
# editable only if there is another owner role than the user's team...
|
||||||
|
"destroy": other_access.role == "owner",
|
||||||
|
"retrieve": True,
|
||||||
|
"set_role_to": ["administrator", "member"]
|
||||||
|
if other_access.role == "owner"
|
||||||
|
else [],
|
||||||
|
"update": other_access.role == "owner",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": str(other_access.id),
|
||||||
|
"user": None,
|
||||||
|
"team": other_access.team,
|
||||||
|
"role": other_access.role,
|
||||||
|
"abilities": other_access.get_abilities(user),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
key=lambda x: x["id"],
|
||||||
|
)
|
||||||
|
assert response.json() == {
|
||||||
|
"id": str(document.id),
|
||||||
|
"title": document.title,
|
||||||
|
"abilities": document.get_abilities(user),
|
||||||
|
}
|
||||||
230
src/backend/core/tests/documents/test_api_documents_update.py
Normal file
230
src/backend/core/tests/documents/test_api_documents_update.py
Normal file
@@ -0,0 +1,230 @@
|
|||||||
|
"""
|
||||||
|
Tests for Documents API endpoint in impress's core app: update
|
||||||
|
"""
|
||||||
|
import random
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
from rest_framework.test import APIClient
|
||||||
|
|
||||||
|
from core import factories
|
||||||
|
from core.api import serializers
|
||||||
|
from core.tests.conftest import TEAM, USER, VIA
|
||||||
|
|
||||||
|
pytestmark = pytest.mark.django_db
|
||||||
|
|
||||||
|
|
||||||
|
def test_api_documents_update_anonymous():
|
||||||
|
"""Anonymous users should not be allowed to update a document."""
|
||||||
|
document = factories.DocumentFactory()
|
||||||
|
old_document_values = serializers.DocumentSerializer(instance=document).data
|
||||||
|
|
||||||
|
new_document_values = serializers.DocumentSerializer(
|
||||||
|
instance=factories.DocumentFactory()
|
||||||
|
).data
|
||||||
|
response = APIClient().put(
|
||||||
|
f"/api/v1.0/documents/{document.id!s}/",
|
||||||
|
new_document_values,
|
||||||
|
format="json",
|
||||||
|
)
|
||||||
|
assert response.status_code == 401
|
||||||
|
assert response.json() == {
|
||||||
|
"detail": "Authentication credentials were not provided."
|
||||||
|
}
|
||||||
|
|
||||||
|
document.refresh_from_db()
|
||||||
|
document_values = serializers.DocumentSerializer(instance=document).data
|
||||||
|
assert document_values == old_document_values
|
||||||
|
|
||||||
|
|
||||||
|
def test_api_documents_update_authenticated_unrelated():
|
||||||
|
"""
|
||||||
|
Authenticated users should not be allowed to update a document to which they are not related.
|
||||||
|
"""
|
||||||
|
user = factories.UserFactory()
|
||||||
|
|
||||||
|
client = APIClient()
|
||||||
|
client.force_login(user)
|
||||||
|
|
||||||
|
document = factories.DocumentFactory(is_public=False)
|
||||||
|
old_document_values = serializers.DocumentSerializer(instance=document).data
|
||||||
|
|
||||||
|
new_document_values = serializers.DocumentSerializer(
|
||||||
|
instance=factories.DocumentFactory()
|
||||||
|
).data
|
||||||
|
response = client.put(
|
||||||
|
f"/api/v1.0/documents/{document.id!s}/",
|
||||||
|
new_document_values,
|
||||||
|
format="json",
|
||||||
|
)
|
||||||
|
|
||||||
|
assert response.status_code == 404
|
||||||
|
assert response.json() == {"detail": "Not found."}
|
||||||
|
|
||||||
|
document.refresh_from_db()
|
||||||
|
document_values = serializers.DocumentSerializer(instance=document).data
|
||||||
|
assert document_values == old_document_values
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("via", VIA)
|
||||||
|
def test_api_documents_update_authenticated_members(via, mock_user_get_teams):
|
||||||
|
"""
|
||||||
|
Users who are members of a document but not administrators should
|
||||||
|
not be allowed to update it.
|
||||||
|
"""
|
||||||
|
user = factories.UserFactory()
|
||||||
|
|
||||||
|
client = APIClient()
|
||||||
|
client.force_login(user)
|
||||||
|
|
||||||
|
document = factories.DocumentFactory()
|
||||||
|
if via == USER:
|
||||||
|
factories.UserDocumentAccessFactory(document=document, user=user, role="member")
|
||||||
|
elif via == TEAM:
|
||||||
|
mock_user_get_teams.return_value = ["lasuite", "unknown"]
|
||||||
|
factories.TeamDocumentAccessFactory(
|
||||||
|
document=document, team="lasuite", role="member"
|
||||||
|
)
|
||||||
|
|
||||||
|
old_document_values = serializers.DocumentSerializer(instance=document).data
|
||||||
|
|
||||||
|
new_document_values = serializers.DocumentSerializer(
|
||||||
|
instance=factories.DocumentFactory()
|
||||||
|
).data
|
||||||
|
response = client.put(
|
||||||
|
f"/api/v1.0/documents/{document.id!s}/",
|
||||||
|
new_document_values,
|
||||||
|
format="json",
|
||||||
|
)
|
||||||
|
|
||||||
|
assert response.status_code == 403
|
||||||
|
assert response.json() == {
|
||||||
|
"detail": "You do not have permission to perform this action."
|
||||||
|
}
|
||||||
|
|
||||||
|
document.refresh_from_db()
|
||||||
|
document_values = serializers.DocumentSerializer(instance=document).data
|
||||||
|
assert document_values == old_document_values
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("role", ["administrator", "owner"])
|
||||||
|
@pytest.mark.parametrize("via", VIA)
|
||||||
|
def test_api_documents_update_authenticated_administrator_or_owner(
|
||||||
|
via, role, mock_user_get_teams
|
||||||
|
):
|
||||||
|
"""Administrator or owner of a document should be allowed to update it."""
|
||||||
|
user = factories.UserFactory()
|
||||||
|
|
||||||
|
client = APIClient()
|
||||||
|
client.force_login(user)
|
||||||
|
|
||||||
|
document = factories.DocumentFactory()
|
||||||
|
if via == USER:
|
||||||
|
factories.UserDocumentAccessFactory(document=document, user=user, role=role)
|
||||||
|
elif via == TEAM:
|
||||||
|
mock_user_get_teams.return_value = ["lasuite", "unknown"]
|
||||||
|
factories.TeamDocumentAccessFactory(
|
||||||
|
document=document, team="lasuite", role=role
|
||||||
|
)
|
||||||
|
|
||||||
|
old_document_values = serializers.DocumentSerializer(instance=document).data
|
||||||
|
|
||||||
|
new_document_values = serializers.DocumentSerializer(
|
||||||
|
instance=factories.DocumentFactory()
|
||||||
|
).data
|
||||||
|
response = client.put(
|
||||||
|
f"/api/v1.0/documents/{document.id!s}/",
|
||||||
|
new_document_values,
|
||||||
|
format="json",
|
||||||
|
)
|
||||||
|
assert response.status_code == 200
|
||||||
|
|
||||||
|
document.refresh_from_db()
|
||||||
|
document_values = serializers.DocumentSerializer(instance=document).data
|
||||||
|
for key, value in document_values.items():
|
||||||
|
if key in ["id", "accesses"]:
|
||||||
|
assert value == old_document_values[key]
|
||||||
|
else:
|
||||||
|
assert value == new_document_values[key]
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("via", VIA)
|
||||||
|
def test_api_documents_update_authenticated_owners(via, mock_user_get_teams):
|
||||||
|
"""Administrators of a document should be allowed to update it."""
|
||||||
|
user = factories.UserFactory()
|
||||||
|
|
||||||
|
client = APIClient()
|
||||||
|
client.force_login(user)
|
||||||
|
|
||||||
|
document = factories.DocumentFactory()
|
||||||
|
if via == USER:
|
||||||
|
factories.UserDocumentAccessFactory(document=document, user=user, role="owner")
|
||||||
|
elif via == TEAM:
|
||||||
|
mock_user_get_teams.return_value = ["lasuite", "unknown"]
|
||||||
|
factories.TeamDocumentAccessFactory(
|
||||||
|
document=document, team="lasuite", role="owner"
|
||||||
|
)
|
||||||
|
|
||||||
|
old_document_values = serializers.DocumentSerializer(instance=document).data
|
||||||
|
|
||||||
|
new_document_values = serializers.DocumentSerializer(
|
||||||
|
instance=factories.DocumentFactory()
|
||||||
|
).data
|
||||||
|
|
||||||
|
response = client.put(
|
||||||
|
f"/api/v1.0/documents/{document.id!s}/", new_document_values, format="json"
|
||||||
|
)
|
||||||
|
|
||||||
|
assert response.status_code == 200
|
||||||
|
document.refresh_from_db()
|
||||||
|
document_values = serializers.DocumentSerializer(instance=document).data
|
||||||
|
for key, value in document_values.items():
|
||||||
|
if key in ["id", "accesses"]:
|
||||||
|
assert value == old_document_values[key]
|
||||||
|
else:
|
||||||
|
assert value == new_document_values[key]
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("via", VIA)
|
||||||
|
def test_api_documents_update_administrator_or_owner_of_another(
|
||||||
|
via, mock_user_get_teams
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Being administrator or owner of a document should not grant authorization to update
|
||||||
|
another document.
|
||||||
|
"""
|
||||||
|
user = factories.UserFactory()
|
||||||
|
|
||||||
|
client = APIClient()
|
||||||
|
client.force_login(user)
|
||||||
|
|
||||||
|
document = factories.DocumentFactory()
|
||||||
|
if via == USER:
|
||||||
|
factories.UserDocumentAccessFactory(
|
||||||
|
document=document, user=user, role=random.choice(["administrator", "owner"])
|
||||||
|
)
|
||||||
|
elif via == TEAM:
|
||||||
|
mock_user_get_teams.return_value = ["lasuite", "unknown"]
|
||||||
|
factories.TeamDocumentAccessFactory(
|
||||||
|
document=document,
|
||||||
|
team="lasuite",
|
||||||
|
role=random.choice(["administrator", "owner"]),
|
||||||
|
)
|
||||||
|
|
||||||
|
is_public = random.choice([True, False])
|
||||||
|
document = factories.DocumentFactory(title="Old title", is_public=is_public)
|
||||||
|
old_document_values = serializers.DocumentSerializer(instance=document).data
|
||||||
|
|
||||||
|
new_document_values = serializers.DocumentSerializer(
|
||||||
|
instance=factories.DocumentFactory()
|
||||||
|
).data
|
||||||
|
response = client.put(
|
||||||
|
f"/api/v1.0/documents/{document.id!s}/",
|
||||||
|
new_document_values,
|
||||||
|
format="json",
|
||||||
|
)
|
||||||
|
|
||||||
|
assert response.status_code == 403 if is_public else 404
|
||||||
|
|
||||||
|
document.refresh_from_db()
|
||||||
|
document_values = serializers.DocumentSerializer(instance=document).data
|
||||||
|
assert document_values == old_document_values
|
||||||
@@ -107,7 +107,7 @@ def test_api_templates_generate_document_related(via, mock_user_get_teams):
|
|||||||
data = {"body": "# Test markdown body"}
|
data = {"body": "# Test markdown body"}
|
||||||
|
|
||||||
response = client.post(
|
response = client.post(
|
||||||
f"/api/v1.0/templates/{access.template.id!s}/generate-document/",
|
f"/api/v1.0/templates/{access.template_id!s}/generate-document/",
|
||||||
data,
|
data,
|
||||||
format="json",
|
format="json",
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -108,7 +108,7 @@ def test_api_templates_list_pagination(
|
|||||||
client.force_login(user)
|
client.force_login(user)
|
||||||
|
|
||||||
template_ids = [
|
template_ids = [
|
||||||
str(access.template.id)
|
str(access.template_id)
|
||||||
for access in factories.UserTemplateAccessFactory.create_batch(3, user=user)
|
for access in factories.UserTemplateAccessFactory.create_batch(3, user=user)
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|||||||
967
src/backend/core/tests/test_api_document_accesses.py
Normal file
967
src/backend/core/tests/test_api_document_accesses.py
Normal file
@@ -0,0 +1,967 @@
|
|||||||
|
"""
|
||||||
|
Test document accesses API endpoints for users in impress's core app.
|
||||||
|
"""
|
||||||
|
import random
|
||||||
|
from uuid import uuid4
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
from rest_framework.test import APIClient
|
||||||
|
|
||||||
|
from core import factories, models
|
||||||
|
from core.api import serializers
|
||||||
|
from core.tests.conftest import TEAM, USER, VIA
|
||||||
|
|
||||||
|
pytestmark = pytest.mark.django_db
|
||||||
|
|
||||||
|
|
||||||
|
def test_api_document_accesses_list_anonymous():
|
||||||
|
"""Anonymous users should not be allowed to list document accesses."""
|
||||||
|
document = factories.DocumentFactory()
|
||||||
|
factories.UserDocumentAccessFactory.create_batch(2, document=document)
|
||||||
|
|
||||||
|
response = APIClient().get(f"/api/v1.0/documents/{document.id!s}/accesses/")
|
||||||
|
assert response.status_code == 401
|
||||||
|
assert response.json() == {
|
||||||
|
"detail": "Authentication credentials were not provided."
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def test_api_document_accesses_list_authenticated_unrelated():
|
||||||
|
"""
|
||||||
|
Authenticated users should not be allowed to list document accesses for a document
|
||||||
|
to which they are not related.
|
||||||
|
"""
|
||||||
|
user = factories.UserFactory()
|
||||||
|
|
||||||
|
client = APIClient()
|
||||||
|
client.force_login(user)
|
||||||
|
|
||||||
|
document = factories.DocumentFactory()
|
||||||
|
factories.UserDocumentAccessFactory.create_batch(3, document=document)
|
||||||
|
|
||||||
|
# Accesses for other documents to which the user is related should not be listed either
|
||||||
|
other_access = factories.UserDocumentAccessFactory(user=user)
|
||||||
|
factories.UserDocumentAccessFactory(document=other_access.document)
|
||||||
|
|
||||||
|
response = client.get(
|
||||||
|
f"/api/v1.0/documents/{document.id!s}/accesses/",
|
||||||
|
)
|
||||||
|
assert response.status_code == 200
|
||||||
|
assert response.json() == {
|
||||||
|
"count": 0,
|
||||||
|
"next": None,
|
||||||
|
"previous": None,
|
||||||
|
"results": [],
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("via", VIA)
|
||||||
|
def test_api_document_accesses_list_authenticated_related(via, mock_user_get_teams):
|
||||||
|
"""
|
||||||
|
Authenticated users should be able to list document accesses for a document
|
||||||
|
to which they are directly related, whatever their role in the document.
|
||||||
|
"""
|
||||||
|
user = factories.UserFactory()
|
||||||
|
|
||||||
|
client = APIClient()
|
||||||
|
client.force_login(user)
|
||||||
|
|
||||||
|
document = factories.DocumentFactory()
|
||||||
|
if via == USER:
|
||||||
|
user_access = models.DocumentAccess.objects.create(
|
||||||
|
document=document,
|
||||||
|
user=user,
|
||||||
|
role=random.choice(models.RoleChoices.choices)[0],
|
||||||
|
)
|
||||||
|
elif via == TEAM:
|
||||||
|
mock_user_get_teams.return_value = ["lasuite", "unknown"]
|
||||||
|
user_access = models.DocumentAccess.objects.create(
|
||||||
|
document=document,
|
||||||
|
team="lasuite",
|
||||||
|
role=random.choice(models.RoleChoices.choices)[0],
|
||||||
|
)
|
||||||
|
|
||||||
|
access1 = factories.TeamDocumentAccessFactory(document=document)
|
||||||
|
access2 = factories.UserDocumentAccessFactory(document=document)
|
||||||
|
|
||||||
|
# Accesses for other documents to which the user is related should not be listed either
|
||||||
|
other_access = factories.UserDocumentAccessFactory(user=user)
|
||||||
|
factories.UserDocumentAccessFactory(document=other_access.document)
|
||||||
|
|
||||||
|
response = client.get(
|
||||||
|
f"/api/v1.0/documents/{document.id!s}/accesses/",
|
||||||
|
)
|
||||||
|
|
||||||
|
assert response.status_code == 200
|
||||||
|
content = response.json()
|
||||||
|
assert len(content["results"]) == 3
|
||||||
|
assert sorted(content["results"], key=lambda x: x["id"]) == sorted(
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"id": str(user_access.id),
|
||||||
|
"user": str(user.id) if via == "user" else None,
|
||||||
|
"team": "lasuite" if via == "team" else "",
|
||||||
|
"role": user_access.role,
|
||||||
|
"abilities": user_access.get_abilities(user),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": str(access1.id),
|
||||||
|
"user": None,
|
||||||
|
"team": access1.team,
|
||||||
|
"role": access1.role,
|
||||||
|
"abilities": access1.get_abilities(user),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": str(access2.id),
|
||||||
|
"user": str(access2.user.id),
|
||||||
|
"team": "",
|
||||||
|
"role": access2.role,
|
||||||
|
"abilities": access2.get_abilities(user),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
key=lambda x: x["id"],
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_api_document_accesses_retrieve_anonymous():
|
||||||
|
"""
|
||||||
|
Anonymous users should not be allowed to retrieve a document access.
|
||||||
|
"""
|
||||||
|
access = factories.UserDocumentAccessFactory()
|
||||||
|
|
||||||
|
response = APIClient().get(
|
||||||
|
f"/api/v1.0/documents/{access.document_id!s}/accesses/{access.id!s}/",
|
||||||
|
)
|
||||||
|
|
||||||
|
assert response.status_code == 401
|
||||||
|
assert response.json() == {
|
||||||
|
"detail": "Authentication credentials were not provided."
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def test_api_document_accesses_retrieve_authenticated_unrelated():
|
||||||
|
"""
|
||||||
|
Authenticated users should not be allowed to retrieve a document access for
|
||||||
|
a document to which they are not related.
|
||||||
|
"""
|
||||||
|
user = factories.UserFactory()
|
||||||
|
|
||||||
|
client = APIClient()
|
||||||
|
client.force_login(user)
|
||||||
|
|
||||||
|
document = factories.DocumentFactory()
|
||||||
|
access = factories.UserDocumentAccessFactory(document=document)
|
||||||
|
|
||||||
|
response = client.get(
|
||||||
|
f"/api/v1.0/documents/{document.id!s}/accesses/{access.id!s}/",
|
||||||
|
)
|
||||||
|
assert response.status_code == 403
|
||||||
|
assert response.json() == {
|
||||||
|
"detail": "You do not have permission to perform this action."
|
||||||
|
}
|
||||||
|
|
||||||
|
# Accesses related to another document should be excluded even if the user is related to it
|
||||||
|
for access in [
|
||||||
|
factories.UserDocumentAccessFactory(),
|
||||||
|
factories.UserDocumentAccessFactory(user=user),
|
||||||
|
]:
|
||||||
|
response = client.get(
|
||||||
|
f"/api/v1.0/documents/{document.id!s}/accesses/{access.id!s}/",
|
||||||
|
)
|
||||||
|
|
||||||
|
assert response.status_code == 404
|
||||||
|
assert response.json() == {"detail": "Not found."}
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("via", VIA)
|
||||||
|
def test_api_document_accesses_retrieve_authenticated_related(via, mock_user_get_teams):
|
||||||
|
"""
|
||||||
|
A user who is related to a document should be allowed to retrieve the
|
||||||
|
associated document user accesses.
|
||||||
|
"""
|
||||||
|
user = factories.UserFactory()
|
||||||
|
|
||||||
|
client = APIClient()
|
||||||
|
client.force_login(user)
|
||||||
|
|
||||||
|
document = factories.DocumentFactory()
|
||||||
|
if via == USER:
|
||||||
|
factories.UserDocumentAccessFactory(document=document, user=user)
|
||||||
|
elif via == TEAM:
|
||||||
|
mock_user_get_teams.return_value = ["lasuite", "unknown"]
|
||||||
|
factories.TeamDocumentAccessFactory(document=document, team="lasuite")
|
||||||
|
|
||||||
|
access = factories.UserDocumentAccessFactory(document=document)
|
||||||
|
|
||||||
|
response = client.get(
|
||||||
|
f"/api/v1.0/documents/{document.id!s}/accesses/{access.id!s}/",
|
||||||
|
)
|
||||||
|
|
||||||
|
assert response.status_code == 200
|
||||||
|
assert response.json() == {
|
||||||
|
"id": str(access.id),
|
||||||
|
"user": str(access.user.id),
|
||||||
|
"team": "",
|
||||||
|
"role": access.role,
|
||||||
|
"abilities": access.get_abilities(user),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def test_api_document_accesses_create_anonymous():
|
||||||
|
"""Anonymous users should not be allowed to create document accesses."""
|
||||||
|
user = factories.UserFactory()
|
||||||
|
document = factories.DocumentFactory()
|
||||||
|
|
||||||
|
response = APIClient().post(
|
||||||
|
f"/api/v1.0/documents/{document.id!s}/accesses/",
|
||||||
|
{
|
||||||
|
"user": str(user.id),
|
||||||
|
"document": str(document.id),
|
||||||
|
"role": random.choice(models.RoleChoices.choices)[0],
|
||||||
|
},
|
||||||
|
format="json",
|
||||||
|
)
|
||||||
|
|
||||||
|
assert response.status_code == 401
|
||||||
|
assert response.json() == {
|
||||||
|
"detail": "Authentication credentials were not provided."
|
||||||
|
}
|
||||||
|
assert models.DocumentAccess.objects.exists() is False
|
||||||
|
|
||||||
|
|
||||||
|
def test_api_document_accesses_create_authenticated_unrelated():
|
||||||
|
"""
|
||||||
|
Authenticated users should not be allowed to create document accesses for a document to
|
||||||
|
which they are not related.
|
||||||
|
"""
|
||||||
|
user = factories.UserFactory()
|
||||||
|
|
||||||
|
client = APIClient()
|
||||||
|
client.force_login(user)
|
||||||
|
|
||||||
|
other_user = factories.UserFactory()
|
||||||
|
document = factories.DocumentFactory()
|
||||||
|
|
||||||
|
response = client.post(
|
||||||
|
f"/api/v1.0/documents/{document.id!s}/accesses/",
|
||||||
|
{
|
||||||
|
"user": str(other_user.id),
|
||||||
|
},
|
||||||
|
format="json",
|
||||||
|
)
|
||||||
|
|
||||||
|
assert response.status_code == 403
|
||||||
|
assert not models.DocumentAccess.objects.filter(user=other_user).exists()
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("via", VIA)
|
||||||
|
def test_api_document_accesses_create_authenticated_member(via, mock_user_get_teams):
|
||||||
|
"""Members of a document should not be allowed to create document accesses."""
|
||||||
|
user = factories.UserFactory()
|
||||||
|
|
||||||
|
client = APIClient()
|
||||||
|
client.force_login(user)
|
||||||
|
|
||||||
|
document = factories.DocumentFactory()
|
||||||
|
if via == USER:
|
||||||
|
factories.UserDocumentAccessFactory(document=document, user=user, role="member")
|
||||||
|
elif via == TEAM:
|
||||||
|
mock_user_get_teams.return_value = ["lasuite", "unknown"]
|
||||||
|
factories.TeamDocumentAccessFactory(
|
||||||
|
document=document, team="lasuite", role="member"
|
||||||
|
)
|
||||||
|
|
||||||
|
other_user = factories.UserFactory()
|
||||||
|
|
||||||
|
for role in [role[0] for role in models.RoleChoices.choices]:
|
||||||
|
response = client.post(
|
||||||
|
f"/api/v1.0/documents/{document.id!s}/accesses/",
|
||||||
|
{
|
||||||
|
"user": str(other_user.id),
|
||||||
|
"role": role,
|
||||||
|
},
|
||||||
|
format="json",
|
||||||
|
)
|
||||||
|
|
||||||
|
assert response.status_code == 403
|
||||||
|
|
||||||
|
assert not models.DocumentAccess.objects.filter(user=other_user).exists()
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("via", VIA)
|
||||||
|
def test_api_document_accesses_create_authenticated_administrator(
|
||||||
|
via, mock_user_get_teams
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Administrators of a document should be able to create document accesses
|
||||||
|
except for the "owner" role.
|
||||||
|
"""
|
||||||
|
user = factories.UserFactory()
|
||||||
|
|
||||||
|
client = APIClient()
|
||||||
|
client.force_login(user)
|
||||||
|
|
||||||
|
document = factories.DocumentFactory()
|
||||||
|
if via == USER:
|
||||||
|
factories.UserDocumentAccessFactory(
|
||||||
|
document=document, user=user, role="administrator"
|
||||||
|
)
|
||||||
|
elif via == TEAM:
|
||||||
|
mock_user_get_teams.return_value = ["lasuite", "unknown"]
|
||||||
|
factories.TeamDocumentAccessFactory(
|
||||||
|
document=document, team="lasuite", role="administrator"
|
||||||
|
)
|
||||||
|
|
||||||
|
other_user = factories.UserFactory()
|
||||||
|
|
||||||
|
# It should not be allowed to create an owner access
|
||||||
|
response = client.post(
|
||||||
|
f"/api/v1.0/documents/{document.id!s}/accesses/",
|
||||||
|
{
|
||||||
|
"user": str(other_user.id),
|
||||||
|
"role": "owner",
|
||||||
|
},
|
||||||
|
format="json",
|
||||||
|
)
|
||||||
|
|
||||||
|
assert response.status_code == 403
|
||||||
|
assert response.json() == {
|
||||||
|
"detail": "Only owners of a resource can assign other users as owners."
|
||||||
|
}
|
||||||
|
|
||||||
|
# It should be allowed to create a lower access
|
||||||
|
role = random.choice(
|
||||||
|
[role[0] for role in models.RoleChoices.choices if role[0] != "owner"]
|
||||||
|
)
|
||||||
|
|
||||||
|
response = client.post(
|
||||||
|
f"/api/v1.0/documents/{document.id!s}/accesses/",
|
||||||
|
{
|
||||||
|
"user": str(other_user.id),
|
||||||
|
"role": role,
|
||||||
|
},
|
||||||
|
format="json",
|
||||||
|
)
|
||||||
|
|
||||||
|
assert response.status_code == 201
|
||||||
|
assert models.DocumentAccess.objects.filter(user=other_user).count() == 1
|
||||||
|
new_document_access = models.DocumentAccess.objects.filter(user=other_user).get()
|
||||||
|
assert response.json() == {
|
||||||
|
"abilities": new_document_access.get_abilities(user),
|
||||||
|
"id": str(new_document_access.id),
|
||||||
|
"team": "",
|
||||||
|
"role": role,
|
||||||
|
"user": str(other_user.id),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("via", VIA)
|
||||||
|
def test_api_document_accesses_create_authenticated_owner(via, mock_user_get_teams):
|
||||||
|
"""
|
||||||
|
Owners of a document should be able to create document accesses whatever the role.
|
||||||
|
"""
|
||||||
|
user = factories.UserFactory()
|
||||||
|
|
||||||
|
client = APIClient()
|
||||||
|
client.force_login(user)
|
||||||
|
|
||||||
|
document = factories.DocumentFactory()
|
||||||
|
if via == USER:
|
||||||
|
factories.UserDocumentAccessFactory(document=document, user=user, role="owner")
|
||||||
|
elif via == TEAM:
|
||||||
|
mock_user_get_teams.return_value = ["lasuite", "unknown"]
|
||||||
|
factories.TeamDocumentAccessFactory(
|
||||||
|
document=document, team="lasuite", role="owner"
|
||||||
|
)
|
||||||
|
|
||||||
|
other_user = factories.UserFactory()
|
||||||
|
|
||||||
|
role = random.choice([role[0] for role in models.RoleChoices.choices])
|
||||||
|
|
||||||
|
response = client.post(
|
||||||
|
f"/api/v1.0/documents/{document.id!s}/accesses/",
|
||||||
|
{
|
||||||
|
"user": str(other_user.id),
|
||||||
|
"role": role,
|
||||||
|
},
|
||||||
|
format="json",
|
||||||
|
)
|
||||||
|
|
||||||
|
assert response.status_code == 201
|
||||||
|
assert models.DocumentAccess.objects.filter(user=other_user).count() == 1
|
||||||
|
new_document_access = models.DocumentAccess.objects.filter(user=other_user).get()
|
||||||
|
assert response.json() == {
|
||||||
|
"id": str(new_document_access.id),
|
||||||
|
"user": str(other_user.id),
|
||||||
|
"team": "",
|
||||||
|
"role": role,
|
||||||
|
"abilities": new_document_access.get_abilities(user),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def test_api_document_accesses_update_anonymous():
|
||||||
|
"""Anonymous users should not be allowed to update a document access."""
|
||||||
|
access = factories.UserDocumentAccessFactory()
|
||||||
|
old_values = serializers.DocumentAccessSerializer(instance=access).data
|
||||||
|
|
||||||
|
new_values = {
|
||||||
|
"id": uuid4(),
|
||||||
|
"user": factories.UserFactory().id,
|
||||||
|
"role": random.choice(models.RoleChoices.choices)[0],
|
||||||
|
}
|
||||||
|
|
||||||
|
api_client = APIClient()
|
||||||
|
for field, value in new_values.items():
|
||||||
|
response = api_client.put(
|
||||||
|
f"/api/v1.0/documents/{access.document_id!s}/accesses/{access.id!s}/",
|
||||||
|
{**old_values, field: value},
|
||||||
|
format="json",
|
||||||
|
)
|
||||||
|
assert response.status_code == 401
|
||||||
|
|
||||||
|
access.refresh_from_db()
|
||||||
|
updated_values = serializers.DocumentAccessSerializer(instance=access).data
|
||||||
|
assert updated_values == old_values
|
||||||
|
|
||||||
|
|
||||||
|
def test_api_document_accesses_update_authenticated_unrelated():
|
||||||
|
"""
|
||||||
|
Authenticated users should not be allowed to update a document access for a document to which
|
||||||
|
they are not related.
|
||||||
|
"""
|
||||||
|
user = factories.UserFactory()
|
||||||
|
|
||||||
|
client = APIClient()
|
||||||
|
client.force_login(user)
|
||||||
|
|
||||||
|
access = factories.UserDocumentAccessFactory()
|
||||||
|
old_values = serializers.DocumentAccessSerializer(instance=access).data
|
||||||
|
|
||||||
|
new_values = {
|
||||||
|
"id": uuid4(),
|
||||||
|
"user": factories.UserFactory().id,
|
||||||
|
"role": random.choice(models.RoleChoices.choices)[0],
|
||||||
|
}
|
||||||
|
|
||||||
|
for field, value in new_values.items():
|
||||||
|
response = client.put(
|
||||||
|
f"/api/v1.0/documents/{access.document_id!s}/accesses/{access.id!s}/",
|
||||||
|
{**old_values, field: value},
|
||||||
|
format="json",
|
||||||
|
)
|
||||||
|
assert response.status_code == 403
|
||||||
|
|
||||||
|
access.refresh_from_db()
|
||||||
|
updated_values = serializers.DocumentAccessSerializer(instance=access).data
|
||||||
|
assert updated_values == old_values
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("via", VIA)
|
||||||
|
def test_api_document_accesses_update_authenticated_member(via, mock_user_get_teams):
|
||||||
|
"""Members of a document should not be allowed to update its accesses."""
|
||||||
|
user = factories.UserFactory()
|
||||||
|
|
||||||
|
client = APIClient()
|
||||||
|
client.force_login(user)
|
||||||
|
|
||||||
|
document = factories.DocumentFactory()
|
||||||
|
if via == USER:
|
||||||
|
factories.UserDocumentAccessFactory(document=document, user=user, role="member")
|
||||||
|
elif via == TEAM:
|
||||||
|
mock_user_get_teams.return_value = ["lasuite", "unknown"]
|
||||||
|
factories.TeamDocumentAccessFactory(
|
||||||
|
document=document, team="lasuite", role="member"
|
||||||
|
)
|
||||||
|
|
||||||
|
access = factories.UserDocumentAccessFactory(document=document)
|
||||||
|
old_values = serializers.DocumentAccessSerializer(instance=access).data
|
||||||
|
|
||||||
|
new_values = {
|
||||||
|
"id": uuid4(),
|
||||||
|
"user": factories.UserFactory().id,
|
||||||
|
"role": random.choice(models.RoleChoices.choices)[0],
|
||||||
|
}
|
||||||
|
|
||||||
|
for field, value in new_values.items():
|
||||||
|
response = client.put(
|
||||||
|
f"/api/v1.0/documents/{access.document_id!s}/accesses/{access.id!s}/",
|
||||||
|
{**old_values, field: value},
|
||||||
|
format="json",
|
||||||
|
)
|
||||||
|
assert response.status_code == 403
|
||||||
|
|
||||||
|
access.refresh_from_db()
|
||||||
|
updated_values = serializers.DocumentAccessSerializer(instance=access).data
|
||||||
|
assert updated_values == old_values
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("via", VIA)
|
||||||
|
def test_api_document_accesses_update_administrator_except_owner(
|
||||||
|
via, mock_user_get_teams
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
A user who is a direct administrator in a document should be allowed to update a user
|
||||||
|
access for this document, as long as they don't try to set the role to owner.
|
||||||
|
"""
|
||||||
|
user = factories.UserFactory()
|
||||||
|
|
||||||
|
client = APIClient()
|
||||||
|
client.force_login(user)
|
||||||
|
|
||||||
|
document = factories.DocumentFactory()
|
||||||
|
if via == USER:
|
||||||
|
factories.UserDocumentAccessFactory(
|
||||||
|
document=document, user=user, role="administrator"
|
||||||
|
)
|
||||||
|
elif via == TEAM:
|
||||||
|
mock_user_get_teams.return_value = ["lasuite", "unknown"]
|
||||||
|
factories.TeamDocumentAccessFactory(
|
||||||
|
document=document, team="lasuite", role="administrator"
|
||||||
|
)
|
||||||
|
|
||||||
|
access = factories.UserDocumentAccessFactory(
|
||||||
|
document=document,
|
||||||
|
role=random.choice(["administrator", "member"]),
|
||||||
|
)
|
||||||
|
old_values = serializers.DocumentAccessSerializer(instance=access).data
|
||||||
|
|
||||||
|
new_values = {
|
||||||
|
"id": uuid4(),
|
||||||
|
"user_id": factories.UserFactory().id,
|
||||||
|
"role": random.choice(["administrator", "member"]),
|
||||||
|
}
|
||||||
|
|
||||||
|
for field, value in new_values.items():
|
||||||
|
new_data = {**old_values, field: value}
|
||||||
|
response = client.put(
|
||||||
|
f"/api/v1.0/documents/{document.id!s}/accesses/{access.id!s}/",
|
||||||
|
data=new_data,
|
||||||
|
format="json",
|
||||||
|
)
|
||||||
|
|
||||||
|
if (
|
||||||
|
new_data["role"] == old_values["role"]
|
||||||
|
): # we are not really updating the role
|
||||||
|
assert response.status_code == 403
|
||||||
|
else:
|
||||||
|
assert response.status_code == 200
|
||||||
|
|
||||||
|
access.refresh_from_db()
|
||||||
|
updated_values = serializers.DocumentAccessSerializer(instance=access).data
|
||||||
|
if field == "role":
|
||||||
|
assert updated_values == {**old_values, "role": new_values["role"]}
|
||||||
|
else:
|
||||||
|
assert updated_values == old_values
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("via", VIA)
|
||||||
|
def test_api_document_accesses_update_administrator_from_owner(
|
||||||
|
via, mock_user_get_teams
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
A user who is an administrator in a document, should not be allowed to update
|
||||||
|
the user access of an "owner" for this document.
|
||||||
|
"""
|
||||||
|
user = factories.UserFactory()
|
||||||
|
|
||||||
|
client = APIClient()
|
||||||
|
client.force_login(user)
|
||||||
|
|
||||||
|
document = factories.DocumentFactory()
|
||||||
|
if via == USER:
|
||||||
|
factories.UserDocumentAccessFactory(
|
||||||
|
document=document, user=user, role="administrator"
|
||||||
|
)
|
||||||
|
elif via == TEAM:
|
||||||
|
mock_user_get_teams.return_value = ["lasuite", "unknown"]
|
||||||
|
factories.TeamDocumentAccessFactory(
|
||||||
|
document=document, team="lasuite", role="administrator"
|
||||||
|
)
|
||||||
|
|
||||||
|
other_user = factories.UserFactory()
|
||||||
|
access = factories.UserDocumentAccessFactory(
|
||||||
|
document=document, user=other_user, role="owner"
|
||||||
|
)
|
||||||
|
old_values = serializers.DocumentAccessSerializer(instance=access).data
|
||||||
|
|
||||||
|
new_values = {
|
||||||
|
"id": uuid4(),
|
||||||
|
"user_id": factories.UserFactory().id,
|
||||||
|
"role": random.choice(models.RoleChoices.choices)[0],
|
||||||
|
}
|
||||||
|
|
||||||
|
for field, value in new_values.items():
|
||||||
|
response = client.put(
|
||||||
|
f"/api/v1.0/documents/{document.id!s}/accesses/{access.id!s}/",
|
||||||
|
data={**old_values, field: value},
|
||||||
|
format="json",
|
||||||
|
)
|
||||||
|
|
||||||
|
assert response.status_code == 403
|
||||||
|
access.refresh_from_db()
|
||||||
|
updated_values = serializers.DocumentAccessSerializer(instance=access).data
|
||||||
|
assert updated_values == old_values
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("via", VIA)
|
||||||
|
def test_api_document_accesses_update_administrator_to_owner(via, mock_user_get_teams):
|
||||||
|
"""
|
||||||
|
A user who is an administrator in a document, should not be allowed to update
|
||||||
|
the user access of another user to grant document ownership.
|
||||||
|
"""
|
||||||
|
user = factories.UserFactory()
|
||||||
|
|
||||||
|
client = APIClient()
|
||||||
|
client.force_login(user)
|
||||||
|
|
||||||
|
document = factories.DocumentFactory()
|
||||||
|
if via == USER:
|
||||||
|
factories.UserDocumentAccessFactory(
|
||||||
|
document=document, user=user, role="administrator"
|
||||||
|
)
|
||||||
|
elif via == TEAM:
|
||||||
|
mock_user_get_teams.return_value = ["lasuite", "unknown"]
|
||||||
|
factories.TeamDocumentAccessFactory(
|
||||||
|
document=document, team="lasuite", role="administrator"
|
||||||
|
)
|
||||||
|
|
||||||
|
other_user = factories.UserFactory()
|
||||||
|
access = factories.UserDocumentAccessFactory(
|
||||||
|
document=document,
|
||||||
|
user=other_user,
|
||||||
|
role=random.choice(["administrator", "member"]),
|
||||||
|
)
|
||||||
|
old_values = serializers.DocumentAccessSerializer(instance=access).data
|
||||||
|
|
||||||
|
new_values = {
|
||||||
|
"id": uuid4(),
|
||||||
|
"user_id": factories.UserFactory().id,
|
||||||
|
"role": "owner",
|
||||||
|
}
|
||||||
|
|
||||||
|
for field, value in new_values.items():
|
||||||
|
new_data = {**old_values, field: value}
|
||||||
|
response = client.put(
|
||||||
|
f"/api/v1.0/documents/{document.id!s}/accesses/{access.id!s}/",
|
||||||
|
data=new_data,
|
||||||
|
format="json",
|
||||||
|
)
|
||||||
|
# We are not allowed or not really updating the role
|
||||||
|
if field == "role" or new_data["role"] == old_values["role"]:
|
||||||
|
assert response.status_code == 403
|
||||||
|
else:
|
||||||
|
assert response.status_code == 200
|
||||||
|
|
||||||
|
access.refresh_from_db()
|
||||||
|
updated_values = serializers.DocumentAccessSerializer(instance=access).data
|
||||||
|
assert updated_values == old_values
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("via", VIA)
|
||||||
|
def test_api_document_accesses_update_owner(via, mock_user_get_teams):
|
||||||
|
"""
|
||||||
|
A user who is an owner in a document should be allowed to update
|
||||||
|
a user access for this document whatever the role.
|
||||||
|
"""
|
||||||
|
user = factories.UserFactory()
|
||||||
|
|
||||||
|
client = APIClient()
|
||||||
|
client.force_login(user)
|
||||||
|
|
||||||
|
document = factories.DocumentFactory()
|
||||||
|
if via == USER:
|
||||||
|
factories.UserDocumentAccessFactory(document=document, user=user, role="owner")
|
||||||
|
elif via == TEAM:
|
||||||
|
mock_user_get_teams.return_value = ["lasuite", "unknown"]
|
||||||
|
factories.TeamDocumentAccessFactory(
|
||||||
|
document=document, team="lasuite", role="owner"
|
||||||
|
)
|
||||||
|
|
||||||
|
factories.UserFactory()
|
||||||
|
access = factories.UserDocumentAccessFactory(
|
||||||
|
document=document,
|
||||||
|
)
|
||||||
|
old_values = serializers.DocumentAccessSerializer(instance=access).data
|
||||||
|
|
||||||
|
new_values = {
|
||||||
|
"id": uuid4(),
|
||||||
|
"user_id": factories.UserFactory().id,
|
||||||
|
"role": random.choice(models.RoleChoices.choices)[0],
|
||||||
|
}
|
||||||
|
|
||||||
|
for field, value in new_values.items():
|
||||||
|
new_data = {**old_values, field: value}
|
||||||
|
response = client.put(
|
||||||
|
f"/api/v1.0/documents/{document.id!s}/accesses/{access.id!s}/",
|
||||||
|
data=new_data,
|
||||||
|
format="json",
|
||||||
|
)
|
||||||
|
|
||||||
|
if (
|
||||||
|
new_data["role"] == old_values["role"]
|
||||||
|
): # we are not really updating the role
|
||||||
|
assert response.status_code == 403
|
||||||
|
else:
|
||||||
|
assert response.status_code == 200
|
||||||
|
|
||||||
|
access.refresh_from_db()
|
||||||
|
updated_values = serializers.DocumentAccessSerializer(instance=access).data
|
||||||
|
|
||||||
|
if field == "role":
|
||||||
|
assert updated_values == {**old_values, "role": new_values["role"]}
|
||||||
|
else:
|
||||||
|
assert updated_values == old_values
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("via", VIA)
|
||||||
|
def test_api_document_accesses_update_owner_self(via, mock_user_get_teams):
|
||||||
|
"""
|
||||||
|
A user who is owner of a document should be allowed to update
|
||||||
|
their own user access provided there are other owners in the document.
|
||||||
|
"""
|
||||||
|
user = factories.UserFactory()
|
||||||
|
|
||||||
|
client = APIClient()
|
||||||
|
client.force_login(user)
|
||||||
|
|
||||||
|
document = factories.DocumentFactory()
|
||||||
|
if via == USER:
|
||||||
|
access = factories.UserDocumentAccessFactory(
|
||||||
|
document=document, user=user, role="owner"
|
||||||
|
)
|
||||||
|
elif via == TEAM:
|
||||||
|
mock_user_get_teams.return_value = ["lasuite", "unknown"]
|
||||||
|
access = factories.TeamDocumentAccessFactory(
|
||||||
|
document=document, team="lasuite", role="owner"
|
||||||
|
)
|
||||||
|
|
||||||
|
old_values = serializers.DocumentAccessSerializer(instance=access).data
|
||||||
|
new_role = random.choice(["administrator", "member"])
|
||||||
|
|
||||||
|
response = client.put(
|
||||||
|
f"/api/v1.0/documents/{document.id!s}/accesses/{access.id!s}/",
|
||||||
|
data={**old_values, "role": new_role},
|
||||||
|
format="json",
|
||||||
|
)
|
||||||
|
|
||||||
|
assert response.status_code == 403
|
||||||
|
access.refresh_from_db()
|
||||||
|
assert access.role == "owner"
|
||||||
|
|
||||||
|
# Add another owner and it should now work
|
||||||
|
factories.UserDocumentAccessFactory(document=document, role="owner")
|
||||||
|
|
||||||
|
response = client.put(
|
||||||
|
f"/api/v1.0/documents/{document.id!s}/accesses/{access.id!s}/",
|
||||||
|
data={**old_values, "role": new_role},
|
||||||
|
format="json",
|
||||||
|
)
|
||||||
|
|
||||||
|
assert response.status_code == 200
|
||||||
|
access.refresh_from_db()
|
||||||
|
assert access.role == new_role
|
||||||
|
|
||||||
|
|
||||||
|
# Delete
|
||||||
|
|
||||||
|
|
||||||
|
def test_api_document_accesses_delete_anonymous():
|
||||||
|
"""Anonymous users should not be allowed to destroy a document access."""
|
||||||
|
access = factories.UserDocumentAccessFactory()
|
||||||
|
|
||||||
|
response = APIClient().delete(
|
||||||
|
f"/api/v1.0/documents/{access.document_id!s}/accesses/{access.id!s}/",
|
||||||
|
)
|
||||||
|
|
||||||
|
assert response.status_code == 401
|
||||||
|
assert models.DocumentAccess.objects.count() == 1
|
||||||
|
|
||||||
|
|
||||||
|
def test_api_document_accesses_delete_authenticated():
|
||||||
|
"""
|
||||||
|
Authenticated users should not be allowed to delete a document access for a
|
||||||
|
document to which they are not related.
|
||||||
|
"""
|
||||||
|
user = factories.UserFactory()
|
||||||
|
|
||||||
|
client = APIClient()
|
||||||
|
client.force_login(user)
|
||||||
|
|
||||||
|
access = factories.UserDocumentAccessFactory()
|
||||||
|
|
||||||
|
response = client.delete(
|
||||||
|
f"/api/v1.0/documents/{access.document_id!s}/accesses/{access.id!s}/",
|
||||||
|
)
|
||||||
|
|
||||||
|
assert response.status_code == 403
|
||||||
|
assert models.DocumentAccess.objects.count() == 1
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("via", VIA)
|
||||||
|
def test_api_document_accesses_delete_member(via, mock_user_get_teams):
|
||||||
|
"""
|
||||||
|
Authenticated users should not be allowed to delete a document access for a
|
||||||
|
document in which they are a simple member.
|
||||||
|
"""
|
||||||
|
user = factories.UserFactory()
|
||||||
|
|
||||||
|
client = APIClient()
|
||||||
|
client.force_login(user)
|
||||||
|
|
||||||
|
document = factories.DocumentFactory()
|
||||||
|
if via == USER:
|
||||||
|
factories.UserDocumentAccessFactory(document=document, user=user, role="member")
|
||||||
|
elif via == TEAM:
|
||||||
|
mock_user_get_teams.return_value = ["lasuite", "unknown"]
|
||||||
|
factories.TeamDocumentAccessFactory(
|
||||||
|
document=document, team="lasuite", role="member"
|
||||||
|
)
|
||||||
|
|
||||||
|
access = factories.UserDocumentAccessFactory(document=document)
|
||||||
|
|
||||||
|
assert models.DocumentAccess.objects.count() == 2
|
||||||
|
assert models.DocumentAccess.objects.filter(user=access.user).exists()
|
||||||
|
|
||||||
|
response = client.delete(
|
||||||
|
f"/api/v1.0/documents/{document.id!s}/accesses/{access.id!s}/",
|
||||||
|
)
|
||||||
|
|
||||||
|
assert response.status_code == 403
|
||||||
|
assert models.DocumentAccess.objects.count() == 2
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("via", VIA)
|
||||||
|
def test_api_document_accesses_delete_administrators_except_owners(
|
||||||
|
via, mock_user_get_teams
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Users who are administrators in a document should be allowed to delete an access
|
||||||
|
from the document provided it is not ownership.
|
||||||
|
"""
|
||||||
|
user = factories.UserFactory()
|
||||||
|
|
||||||
|
client = APIClient()
|
||||||
|
client.force_login(user)
|
||||||
|
|
||||||
|
document = factories.DocumentFactory()
|
||||||
|
if via == USER:
|
||||||
|
factories.UserDocumentAccessFactory(
|
||||||
|
document=document, user=user, role="administrator"
|
||||||
|
)
|
||||||
|
elif via == TEAM:
|
||||||
|
mock_user_get_teams.return_value = ["lasuite", "unknown"]
|
||||||
|
factories.TeamDocumentAccessFactory(
|
||||||
|
document=document, team="lasuite", role="administrator"
|
||||||
|
)
|
||||||
|
|
||||||
|
access = factories.UserDocumentAccessFactory(
|
||||||
|
document=document, role=random.choice(["member", "administrator"])
|
||||||
|
)
|
||||||
|
|
||||||
|
assert models.DocumentAccess.objects.count() == 2
|
||||||
|
assert models.DocumentAccess.objects.filter(user=access.user).exists()
|
||||||
|
|
||||||
|
response = client.delete(
|
||||||
|
f"/api/v1.0/documents/{document.id!s}/accesses/{access.id!s}/",
|
||||||
|
)
|
||||||
|
|
||||||
|
assert response.status_code == 204
|
||||||
|
assert models.DocumentAccess.objects.count() == 1
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("via", VIA)
|
||||||
|
def test_api_document_accesses_delete_administrator_on_owners(via, mock_user_get_teams):
|
||||||
|
"""
|
||||||
|
Users who are administrators in a document should not be allowed to delete an ownership
|
||||||
|
access from the document.
|
||||||
|
"""
|
||||||
|
user = factories.UserFactory()
|
||||||
|
|
||||||
|
client = APIClient()
|
||||||
|
client.force_login(user)
|
||||||
|
|
||||||
|
document = factories.DocumentFactory()
|
||||||
|
if via == USER:
|
||||||
|
factories.UserDocumentAccessFactory(
|
||||||
|
document=document, user=user, role="administrator"
|
||||||
|
)
|
||||||
|
elif via == TEAM:
|
||||||
|
mock_user_get_teams.return_value = ["lasuite", "unknown"]
|
||||||
|
factories.TeamDocumentAccessFactory(
|
||||||
|
document=document, team="lasuite", role="administrator"
|
||||||
|
)
|
||||||
|
|
||||||
|
access = factories.UserDocumentAccessFactory(document=document, role="owner")
|
||||||
|
|
||||||
|
assert models.DocumentAccess.objects.count() == 2
|
||||||
|
assert models.DocumentAccess.objects.filter(user=access.user).exists()
|
||||||
|
|
||||||
|
response = client.delete(
|
||||||
|
f"/api/v1.0/documents/{document.id!s}/accesses/{access.id!s}/",
|
||||||
|
)
|
||||||
|
|
||||||
|
assert response.status_code == 403
|
||||||
|
assert models.DocumentAccess.objects.count() == 2
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("via", VIA)
|
||||||
|
def test_api_document_accesses_delete_owners(via, mock_user_get_teams):
|
||||||
|
"""
|
||||||
|
Users should be able to delete the document access of another user
|
||||||
|
for a document of which they are owner.
|
||||||
|
"""
|
||||||
|
user = factories.UserFactory()
|
||||||
|
|
||||||
|
client = APIClient()
|
||||||
|
client.force_login(user)
|
||||||
|
|
||||||
|
document = factories.DocumentFactory()
|
||||||
|
if via == USER:
|
||||||
|
factories.UserDocumentAccessFactory(document=document, user=user, role="owner")
|
||||||
|
elif via == TEAM:
|
||||||
|
mock_user_get_teams.return_value = ["lasuite", "unknown"]
|
||||||
|
factories.TeamDocumentAccessFactory(
|
||||||
|
document=document, team="lasuite", role="owner"
|
||||||
|
)
|
||||||
|
|
||||||
|
access = factories.UserDocumentAccessFactory(document=document)
|
||||||
|
|
||||||
|
assert models.DocumentAccess.objects.count() == 2
|
||||||
|
assert models.DocumentAccess.objects.filter(user=access.user).exists()
|
||||||
|
|
||||||
|
response = client.delete(
|
||||||
|
f"/api/v1.0/documents/{document.id!s}/accesses/{access.id!s}/",
|
||||||
|
)
|
||||||
|
|
||||||
|
assert response.status_code == 204
|
||||||
|
assert models.DocumentAccess.objects.count() == 1
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("via", VIA)
|
||||||
|
def test_api_document_accesses_delete_owners_last_owner(via, mock_user_get_teams):
|
||||||
|
"""
|
||||||
|
It should not be possible to delete the last owner access from a document
|
||||||
|
"""
|
||||||
|
user = factories.UserFactory()
|
||||||
|
|
||||||
|
client = APIClient()
|
||||||
|
client.force_login(user)
|
||||||
|
|
||||||
|
document = factories.DocumentFactory()
|
||||||
|
if via == USER:
|
||||||
|
access = factories.UserDocumentAccessFactory(
|
||||||
|
document=document, user=user, role="owner"
|
||||||
|
)
|
||||||
|
elif via == TEAM:
|
||||||
|
mock_user_get_teams.return_value = ["lasuite", "unknown"]
|
||||||
|
access = factories.TeamDocumentAccessFactory(
|
||||||
|
document=document, team="lasuite", role="owner"
|
||||||
|
)
|
||||||
|
|
||||||
|
assert models.DocumentAccess.objects.count() == 1
|
||||||
|
response = client.delete(
|
||||||
|
f"/api/v1.0/documents/{document.id!s}/accesses/{access.id!s}/",
|
||||||
|
)
|
||||||
|
|
||||||
|
assert response.status_code == 403
|
||||||
|
assert models.DocumentAccess.objects.count() == 1
|
||||||
@@ -130,7 +130,7 @@ def test_api_template_accesses_retrieve_anonymous():
|
|||||||
access = factories.UserTemplateAccessFactory()
|
access = factories.UserTemplateAccessFactory()
|
||||||
|
|
||||||
response = APIClient().get(
|
response = APIClient().get(
|
||||||
f"/api/v1.0/templates/{access.template.id!s}/accesses/{access.id!s}/",
|
f"/api/v1.0/templates/{access.template_id!s}/accesses/{access.id!s}/",
|
||||||
)
|
)
|
||||||
|
|
||||||
assert response.status_code == 401
|
assert response.status_code == 401
|
||||||
@@ -326,7 +326,7 @@ def test_api_template_accesses_create_authenticated_administrator(
|
|||||||
|
|
||||||
assert response.status_code == 403
|
assert response.status_code == 403
|
||||||
assert response.json() == {
|
assert response.json() == {
|
||||||
"detail": "Only owners of a template can assign other users as owners."
|
"detail": "Only owners of a resource can assign other users as owners."
|
||||||
}
|
}
|
||||||
|
|
||||||
# It should be allowed to create a lower access
|
# It should be allowed to create a lower access
|
||||||
@@ -413,7 +413,7 @@ def test_api_template_accesses_update_anonymous():
|
|||||||
api_client = APIClient()
|
api_client = APIClient()
|
||||||
for field, value in new_values.items():
|
for field, value in new_values.items():
|
||||||
response = api_client.put(
|
response = api_client.put(
|
||||||
f"/api/v1.0/templates/{access.template.id!s}/accesses/{access.id!s}/",
|
f"/api/v1.0/templates/{access.template_id!s}/accesses/{access.id!s}/",
|
||||||
{**old_values, field: value},
|
{**old_values, field: value},
|
||||||
format="json",
|
format="json",
|
||||||
)
|
)
|
||||||
@@ -445,7 +445,7 @@ def test_api_template_accesses_update_authenticated_unrelated():
|
|||||||
|
|
||||||
for field, value in new_values.items():
|
for field, value in new_values.items():
|
||||||
response = client.put(
|
response = client.put(
|
||||||
f"/api/v1.0/templates/{access.template.id!s}/accesses/{access.id!s}/",
|
f"/api/v1.0/templates/{access.template_id!s}/accesses/{access.id!s}/",
|
||||||
{**old_values, field: value},
|
{**old_values, field: value},
|
||||||
format="json",
|
format="json",
|
||||||
)
|
)
|
||||||
@@ -484,7 +484,7 @@ def test_api_template_accesses_update_authenticated_member(via, mock_user_get_te
|
|||||||
|
|
||||||
for field, value in new_values.items():
|
for field, value in new_values.items():
|
||||||
response = client.put(
|
response = client.put(
|
||||||
f"/api/v1.0/templates/{access.template.id!s}/accesses/{access.id!s}/",
|
f"/api/v1.0/templates/{access.template_id!s}/accesses/{access.id!s}/",
|
||||||
{**old_values, field: value},
|
{**old_values, field: value},
|
||||||
format="json",
|
format="json",
|
||||||
)
|
)
|
||||||
@@ -770,7 +770,7 @@ def test_api_template_accesses_delete_anonymous():
|
|||||||
access = factories.UserTemplateAccessFactory()
|
access = factories.UserTemplateAccessFactory()
|
||||||
|
|
||||||
response = APIClient().delete(
|
response = APIClient().delete(
|
||||||
f"/api/v1.0/templates/{access.template.id!s}/accesses/{access.id!s}/",
|
f"/api/v1.0/templates/{access.template_id!s}/accesses/{access.id!s}/",
|
||||||
)
|
)
|
||||||
|
|
||||||
assert response.status_code == 401
|
assert response.status_code == 401
|
||||||
@@ -790,7 +790,7 @@ def test_api_template_accesses_delete_authenticated():
|
|||||||
access = factories.UserTemplateAccessFactory()
|
access = factories.UserTemplateAccessFactory()
|
||||||
|
|
||||||
response = client.delete(
|
response = client.delete(
|
||||||
f"/api/v1.0/templates/{access.template.id!s}/accesses/{access.id!s}/",
|
f"/api/v1.0/templates/{access.template_id!s}/accesses/{access.id!s}/",
|
||||||
)
|
)
|
||||||
|
|
||||||
assert response.status_code == 403
|
assert response.status_code == 403
|
||||||
|
|||||||
311
src/backend/core/tests/test_models_document_accesses.py
Normal file
311
src/backend/core/tests/test_models_document_accesses.py
Normal file
@@ -0,0 +1,311 @@
|
|||||||
|
"""
|
||||||
|
Unit tests for the DocumentAccess model
|
||||||
|
"""
|
||||||
|
from django.contrib.auth.models import AnonymousUser
|
||||||
|
from django.core.exceptions import ValidationError
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from core import factories
|
||||||
|
|
||||||
|
pytestmark = pytest.mark.django_db
|
||||||
|
|
||||||
|
|
||||||
|
def test_models_document_accesses_str():
|
||||||
|
"""
|
||||||
|
The str representation should include user email, document title and role.
|
||||||
|
"""
|
||||||
|
user = factories.UserFactory(email="david.bowman@example.com")
|
||||||
|
access = factories.UserDocumentAccessFactory(
|
||||||
|
role="member",
|
||||||
|
user=user,
|
||||||
|
document__title="admins",
|
||||||
|
)
|
||||||
|
assert str(access) == "david.bowman@example.com is member in document admins"
|
||||||
|
|
||||||
|
|
||||||
|
def test_models_document_accesses_unique_user():
|
||||||
|
"""Document accesses should be unique for a given couple of user and document."""
|
||||||
|
access = factories.UserDocumentAccessFactory()
|
||||||
|
|
||||||
|
with pytest.raises(
|
||||||
|
ValidationError,
|
||||||
|
match="This user is already in this document.",
|
||||||
|
):
|
||||||
|
factories.UserDocumentAccessFactory(user=access.user, document=access.document)
|
||||||
|
|
||||||
|
|
||||||
|
def test_models_document_accesses_several_empty_teams():
|
||||||
|
"""A document can have several document accesses with an empty team."""
|
||||||
|
access = factories.UserDocumentAccessFactory()
|
||||||
|
factories.UserDocumentAccessFactory(document=access.document)
|
||||||
|
|
||||||
|
|
||||||
|
def test_models_document_accesses_unique_team():
|
||||||
|
"""Document accesses should be unique for a given couple of team and document."""
|
||||||
|
access = factories.TeamDocumentAccessFactory()
|
||||||
|
|
||||||
|
with pytest.raises(
|
||||||
|
ValidationError,
|
||||||
|
match="This team is already in this document.",
|
||||||
|
):
|
||||||
|
factories.TeamDocumentAccessFactory(team=access.team, document=access.document)
|
||||||
|
|
||||||
|
|
||||||
|
def test_models_document_accesses_several_null_users():
|
||||||
|
"""A document can have several document accesses with a null user."""
|
||||||
|
access = factories.TeamDocumentAccessFactory()
|
||||||
|
factories.TeamDocumentAccessFactory(document=access.document)
|
||||||
|
|
||||||
|
|
||||||
|
def test_models_document_accesses_user_and_team_set():
|
||||||
|
"""User and team can't both be set on a document access."""
|
||||||
|
with pytest.raises(
|
||||||
|
ValidationError,
|
||||||
|
match="Either user or team must be set, not both.",
|
||||||
|
):
|
||||||
|
factories.UserDocumentAccessFactory(team="my-team")
|
||||||
|
|
||||||
|
|
||||||
|
def test_models_document_accesses_user_and_team_empty():
|
||||||
|
"""User and team can't both be empty on a document access."""
|
||||||
|
with pytest.raises(
|
||||||
|
ValidationError,
|
||||||
|
match="Either user or team must be set, not both.",
|
||||||
|
):
|
||||||
|
factories.UserDocumentAccessFactory(user=None)
|
||||||
|
|
||||||
|
|
||||||
|
# get_abilities
|
||||||
|
|
||||||
|
|
||||||
|
def test_models_document_access_get_abilities_anonymous():
|
||||||
|
"""Check abilities returned for an anonymous user."""
|
||||||
|
access = factories.UserDocumentAccessFactory()
|
||||||
|
abilities = access.get_abilities(AnonymousUser())
|
||||||
|
assert abilities == {
|
||||||
|
"destroy": False,
|
||||||
|
"retrieve": False,
|
||||||
|
"update": False,
|
||||||
|
"set_role_to": [],
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def test_models_document_access_get_abilities_authenticated():
|
||||||
|
"""Check abilities returned for an authenticated user."""
|
||||||
|
access = factories.UserDocumentAccessFactory()
|
||||||
|
user = factories.UserFactory()
|
||||||
|
abilities = access.get_abilities(user)
|
||||||
|
assert abilities == {
|
||||||
|
"destroy": False,
|
||||||
|
"retrieve": False,
|
||||||
|
"update": False,
|
||||||
|
"set_role_to": [],
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# - for owner
|
||||||
|
|
||||||
|
|
||||||
|
def test_models_document_access_get_abilities_for_owner_of_self_allowed():
|
||||||
|
"""
|
||||||
|
Check abilities of self access for the owner of a document when
|
||||||
|
there is more than one owner left.
|
||||||
|
"""
|
||||||
|
access = factories.UserDocumentAccessFactory(role="owner")
|
||||||
|
factories.UserDocumentAccessFactory(document=access.document, role="owner")
|
||||||
|
abilities = access.get_abilities(access.user)
|
||||||
|
assert abilities == {
|
||||||
|
"destroy": True,
|
||||||
|
"retrieve": True,
|
||||||
|
"update": True,
|
||||||
|
"set_role_to": ["administrator", "member"],
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def test_models_document_access_get_abilities_for_owner_of_self_last():
|
||||||
|
"""
|
||||||
|
Check abilities of self access for the owner of a document when there is only one owner left.
|
||||||
|
"""
|
||||||
|
access = factories.UserDocumentAccessFactory(role="owner")
|
||||||
|
abilities = access.get_abilities(access.user)
|
||||||
|
assert abilities == {
|
||||||
|
"destroy": False,
|
||||||
|
"retrieve": True,
|
||||||
|
"update": False,
|
||||||
|
"set_role_to": [],
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def test_models_document_access_get_abilities_for_owner_of_owner():
|
||||||
|
"""Check abilities of owner access for the owner of a document."""
|
||||||
|
access = factories.UserDocumentAccessFactory(role="owner")
|
||||||
|
factories.UserDocumentAccessFactory(document=access.document) # another one
|
||||||
|
user = factories.UserDocumentAccessFactory(
|
||||||
|
document=access.document, role="owner"
|
||||||
|
).user
|
||||||
|
abilities = access.get_abilities(user)
|
||||||
|
assert abilities == {
|
||||||
|
"destroy": True,
|
||||||
|
"retrieve": True,
|
||||||
|
"update": True,
|
||||||
|
"set_role_to": ["administrator", "member"],
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def test_models_document_access_get_abilities_for_owner_of_administrator():
|
||||||
|
"""Check abilities of administrator access for the owner of a document."""
|
||||||
|
access = factories.UserDocumentAccessFactory(role="administrator")
|
||||||
|
factories.UserDocumentAccessFactory(document=access.document) # another one
|
||||||
|
user = factories.UserDocumentAccessFactory(
|
||||||
|
document=access.document, role="owner"
|
||||||
|
).user
|
||||||
|
abilities = access.get_abilities(user)
|
||||||
|
assert abilities == {
|
||||||
|
"destroy": True,
|
||||||
|
"retrieve": True,
|
||||||
|
"update": True,
|
||||||
|
"set_role_to": ["owner", "member"],
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def test_models_document_access_get_abilities_for_owner_of_member():
|
||||||
|
"""Check abilities of member access for the owner of a document."""
|
||||||
|
access = factories.UserDocumentAccessFactory(role="member")
|
||||||
|
factories.UserDocumentAccessFactory(document=access.document) # another one
|
||||||
|
user = factories.UserDocumentAccessFactory(
|
||||||
|
document=access.document, role="owner"
|
||||||
|
).user
|
||||||
|
abilities = access.get_abilities(user)
|
||||||
|
assert abilities == {
|
||||||
|
"destroy": True,
|
||||||
|
"retrieve": True,
|
||||||
|
"update": True,
|
||||||
|
"set_role_to": ["owner", "administrator"],
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# - for administrator
|
||||||
|
|
||||||
|
|
||||||
|
def test_models_document_access_get_abilities_for_administrator_of_owner():
|
||||||
|
"""Check abilities of owner access for the administrator of a document."""
|
||||||
|
access = factories.UserDocumentAccessFactory(role="owner")
|
||||||
|
factories.UserDocumentAccessFactory(document=access.document) # another one
|
||||||
|
user = factories.UserDocumentAccessFactory(
|
||||||
|
document=access.document, role="administrator"
|
||||||
|
).user
|
||||||
|
abilities = access.get_abilities(user)
|
||||||
|
assert abilities == {
|
||||||
|
"destroy": False,
|
||||||
|
"retrieve": True,
|
||||||
|
"update": False,
|
||||||
|
"set_role_to": [],
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def test_models_document_access_get_abilities_for_administrator_of_administrator():
|
||||||
|
"""Check abilities of administrator access for the administrator of a document."""
|
||||||
|
access = factories.UserDocumentAccessFactory(role="administrator")
|
||||||
|
factories.UserDocumentAccessFactory(document=access.document) # another one
|
||||||
|
user = factories.UserDocumentAccessFactory(
|
||||||
|
document=access.document, role="administrator"
|
||||||
|
).user
|
||||||
|
abilities = access.get_abilities(user)
|
||||||
|
assert abilities == {
|
||||||
|
"destroy": True,
|
||||||
|
"retrieve": True,
|
||||||
|
"update": True,
|
||||||
|
"set_role_to": ["member"],
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def test_models_document_access_get_abilities_for_administrator_of_member():
|
||||||
|
"""Check abilities of member access for the administrator of a document."""
|
||||||
|
access = factories.UserDocumentAccessFactory(role="member")
|
||||||
|
factories.UserDocumentAccessFactory(document=access.document) # another one
|
||||||
|
user = factories.UserDocumentAccessFactory(
|
||||||
|
document=access.document, role="administrator"
|
||||||
|
).user
|
||||||
|
abilities = access.get_abilities(user)
|
||||||
|
assert abilities == {
|
||||||
|
"destroy": True,
|
||||||
|
"retrieve": True,
|
||||||
|
"update": True,
|
||||||
|
"set_role_to": ["administrator"],
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# - for member
|
||||||
|
|
||||||
|
|
||||||
|
def test_models_document_access_get_abilities_for_member_of_owner():
|
||||||
|
"""Check abilities of owner access for the member of a document."""
|
||||||
|
access = factories.UserDocumentAccessFactory(role="owner")
|
||||||
|
factories.UserDocumentAccessFactory(document=access.document) # another one
|
||||||
|
user = factories.UserDocumentAccessFactory(
|
||||||
|
document=access.document, role="member"
|
||||||
|
).user
|
||||||
|
abilities = access.get_abilities(user)
|
||||||
|
assert abilities == {
|
||||||
|
"destroy": False,
|
||||||
|
"retrieve": True,
|
||||||
|
"update": False,
|
||||||
|
"set_role_to": [],
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def test_models_document_access_get_abilities_for_member_of_administrator():
|
||||||
|
"""Check abilities of administrator access for the member of a document."""
|
||||||
|
access = factories.UserDocumentAccessFactory(role="administrator")
|
||||||
|
factories.UserDocumentAccessFactory(document=access.document) # another one
|
||||||
|
user = factories.UserDocumentAccessFactory(
|
||||||
|
document=access.document, role="member"
|
||||||
|
).user
|
||||||
|
abilities = access.get_abilities(user)
|
||||||
|
assert abilities == {
|
||||||
|
"destroy": False,
|
||||||
|
"retrieve": True,
|
||||||
|
"update": False,
|
||||||
|
"set_role_to": [],
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def test_models_document_access_get_abilities_for_member_of_member_user(
|
||||||
|
django_assert_num_queries
|
||||||
|
):
|
||||||
|
"""Check abilities of member access for the member of a document."""
|
||||||
|
access = factories.UserDocumentAccessFactory(role="member")
|
||||||
|
factories.UserDocumentAccessFactory(document=access.document) # another one
|
||||||
|
user = factories.UserDocumentAccessFactory(
|
||||||
|
document=access.document, role="member"
|
||||||
|
).user
|
||||||
|
|
||||||
|
with django_assert_num_queries(1):
|
||||||
|
abilities = access.get_abilities(user)
|
||||||
|
|
||||||
|
assert abilities == {
|
||||||
|
"destroy": False,
|
||||||
|
"retrieve": True,
|
||||||
|
"update": False,
|
||||||
|
"set_role_to": [],
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def test_models_document_access_get_abilities_preset_role(django_assert_num_queries):
|
||||||
|
"""No query is done if the role is preset, e.g., with a query annotation."""
|
||||||
|
access = factories.UserDocumentAccessFactory(role="member")
|
||||||
|
user = factories.UserDocumentAccessFactory(
|
||||||
|
document=access.document, role="member"
|
||||||
|
).user
|
||||||
|
access.user_roles = ["member"]
|
||||||
|
|
||||||
|
with django_assert_num_queries(0):
|
||||||
|
abilities = access.get_abilities(user)
|
||||||
|
|
||||||
|
assert abilities == {
|
||||||
|
"destroy": False,
|
||||||
|
"retrieve": True,
|
||||||
|
"update": False,
|
||||||
|
"set_role_to": [],
|
||||||
|
}
|
||||||
153
src/backend/core/tests/test_models_documents.py
Normal file
153
src/backend/core/tests/test_models_documents.py
Normal file
@@ -0,0 +1,153 @@
|
|||||||
|
"""
|
||||||
|
Unit tests for the Document model
|
||||||
|
"""
|
||||||
|
from django.contrib.auth.models import AnonymousUser
|
||||||
|
from django.core.exceptions import ValidationError
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from core import factories, models
|
||||||
|
|
||||||
|
pytestmark = pytest.mark.django_db
|
||||||
|
|
||||||
|
|
||||||
|
def test_models_documents_str():
|
||||||
|
"""The str representation should be the title of the document."""
|
||||||
|
document = factories.DocumentFactory(title="admins")
|
||||||
|
assert str(document) == "admins"
|
||||||
|
|
||||||
|
|
||||||
|
def test_models_documents_id_unique():
|
||||||
|
"""The "id" field should be unique."""
|
||||||
|
document = factories.DocumentFactory()
|
||||||
|
with pytest.raises(ValidationError, match="Document with this Id already exists."):
|
||||||
|
factories.DocumentFactory(id=document.id)
|
||||||
|
|
||||||
|
|
||||||
|
def test_models_documents_title_null():
|
||||||
|
"""The "title" field should not be null."""
|
||||||
|
with pytest.raises(ValidationError, match="This field cannot be null."):
|
||||||
|
models.Document.objects.create(title=None)
|
||||||
|
|
||||||
|
|
||||||
|
def test_models_documents_title_empty():
|
||||||
|
"""The "title" field should not be empty."""
|
||||||
|
with pytest.raises(ValidationError, match="This field cannot be blank."):
|
||||||
|
models.Document.objects.create(title="")
|
||||||
|
|
||||||
|
|
||||||
|
def test_models_documents_title_max_length():
|
||||||
|
"""The "title" field should be 100 characters maximum."""
|
||||||
|
factories.DocumentFactory(title="a" * 255)
|
||||||
|
with pytest.raises(
|
||||||
|
ValidationError,
|
||||||
|
match=r"Ensure this value has at most 255 characters \(it has 256\)\.",
|
||||||
|
):
|
||||||
|
factories.DocumentFactory(title="a" * 256)
|
||||||
|
|
||||||
|
|
||||||
|
# get_abilities
|
||||||
|
|
||||||
|
|
||||||
|
def test_models_documents_get_abilities_anonymous_public():
|
||||||
|
"""Check abilities returned for an anonymous user if the document is public."""
|
||||||
|
document = factories.DocumentFactory(is_public=True)
|
||||||
|
abilities = document.get_abilities(AnonymousUser())
|
||||||
|
assert abilities == {
|
||||||
|
"destroy": False,
|
||||||
|
"retrieve": True,
|
||||||
|
"update": False,
|
||||||
|
"manage_accesses": False,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def test_models_documents_get_abilities_anonymous_not_public():
|
||||||
|
"""Check abilities returned for an anonymous user if the document is private."""
|
||||||
|
document = factories.DocumentFactory(is_public=False)
|
||||||
|
abilities = document.get_abilities(AnonymousUser())
|
||||||
|
assert abilities == {
|
||||||
|
"destroy": False,
|
||||||
|
"retrieve": False,
|
||||||
|
"update": False,
|
||||||
|
"manage_accesses": False,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def test_models_documents_get_abilities_authenticated_public():
|
||||||
|
"""Check abilities returned for an authenticated user if the user is public."""
|
||||||
|
document = factories.DocumentFactory(is_public=True)
|
||||||
|
abilities = document.get_abilities(factories.UserFactory())
|
||||||
|
assert abilities == {
|
||||||
|
"destroy": False,
|
||||||
|
"retrieve": True,
|
||||||
|
"update": False,
|
||||||
|
"manage_accesses": False,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def test_models_documents_get_abilities_authenticated_not_public():
|
||||||
|
"""Check abilities returned for an authenticated user if the document is private."""
|
||||||
|
document = factories.DocumentFactory(is_public=False)
|
||||||
|
abilities = document.get_abilities(factories.UserFactory())
|
||||||
|
assert abilities == {
|
||||||
|
"destroy": False,
|
||||||
|
"retrieve": False,
|
||||||
|
"update": False,
|
||||||
|
"manage_accesses": False,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def test_models_documents_get_abilities_owner():
|
||||||
|
"""Check abilities returned for the owner of a document."""
|
||||||
|
user = factories.UserFactory()
|
||||||
|
access = factories.UserDocumentAccessFactory(role="owner", user=user)
|
||||||
|
abilities = access.document.get_abilities(access.user)
|
||||||
|
assert abilities == {
|
||||||
|
"destroy": True,
|
||||||
|
"retrieve": True,
|
||||||
|
"update": True,
|
||||||
|
"manage_accesses": True,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def test_models_documents_get_abilities_administrator():
|
||||||
|
"""Check abilities returned for the administrator of a document."""
|
||||||
|
access = factories.UserDocumentAccessFactory(role="administrator")
|
||||||
|
abilities = access.document.get_abilities(access.user)
|
||||||
|
assert abilities == {
|
||||||
|
"destroy": False,
|
||||||
|
"retrieve": True,
|
||||||
|
"update": True,
|
||||||
|
"manage_accesses": True,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def test_models_documents_get_abilities_member_user(django_assert_num_queries):
|
||||||
|
"""Check abilities returned for the member of a document."""
|
||||||
|
access = factories.UserDocumentAccessFactory(role="member")
|
||||||
|
|
||||||
|
with django_assert_num_queries(1):
|
||||||
|
abilities = access.document.get_abilities(access.user)
|
||||||
|
|
||||||
|
assert abilities == {
|
||||||
|
"destroy": False,
|
||||||
|
"retrieve": True,
|
||||||
|
"update": False,
|
||||||
|
"manage_accesses": False,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def test_models_documents_get_abilities_preset_role(django_assert_num_queries):
|
||||||
|
"""No query is done if the role is preset e.g. with query annotation."""
|
||||||
|
access = factories.UserDocumentAccessFactory(role="member")
|
||||||
|
access.document.user_roles = ["member"]
|
||||||
|
|
||||||
|
with django_assert_num_queries(0):
|
||||||
|
abilities = access.document.get_abilities(access.user)
|
||||||
|
|
||||||
|
assert abilities == {
|
||||||
|
"destroy": False,
|
||||||
|
"retrieve": True,
|
||||||
|
"update": False,
|
||||||
|
"manage_accesses": False,
|
||||||
|
}
|
||||||
@@ -10,8 +10,17 @@ from core.api import viewsets
|
|||||||
# - Main endpoints
|
# - Main endpoints
|
||||||
router = DefaultRouter()
|
router = DefaultRouter()
|
||||||
router.register("templates", viewsets.TemplateViewSet, basename="templates")
|
router.register("templates", viewsets.TemplateViewSet, basename="templates")
|
||||||
|
router.register("documents", viewsets.DocumentViewSet, basename="documents")
|
||||||
router.register("users", viewsets.UserViewSet, basename="users")
|
router.register("users", viewsets.UserViewSet, basename="users")
|
||||||
|
|
||||||
|
# - Routes nested under a document
|
||||||
|
document_related_router = DefaultRouter()
|
||||||
|
document_related_router.register(
|
||||||
|
"accesses",
|
||||||
|
viewsets.DocumentAccessViewSet,
|
||||||
|
basename="document_accesses",
|
||||||
|
)
|
||||||
|
|
||||||
# - Routes nested under a template
|
# - Routes nested under a template
|
||||||
template_related_router = DefaultRouter()
|
template_related_router = DefaultRouter()
|
||||||
template_related_router.register(
|
template_related_router.register(
|
||||||
@@ -29,7 +38,11 @@ urlpatterns = [
|
|||||||
*router.urls,
|
*router.urls,
|
||||||
*oidc_urls,
|
*oidc_urls,
|
||||||
re_path(
|
re_path(
|
||||||
r"^templates/(?P<template_id>[0-9a-z-]*)/",
|
r"^documents/(?P<resource_id>[0-9a-z-]*)/",
|
||||||
|
include(document_related_router.urls),
|
||||||
|
),
|
||||||
|
re_path(
|
||||||
|
r"^templates/(?P<resource_id>[0-9a-z-]*)/",
|
||||||
include(template_related_router.urls),
|
include(template_related_router.urls),
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
|||||||
Reference in New Issue
Block a user