✨(models/api) add link access reach and role
Link access was either public or private and was only allowing readers. This commit makes link access more powerful: - link reach can be private (users need to obtain specific access by document's administrators), restricted (any authenticated user) or public (anybody including anonymous users) - link role can be reader or editor. It is thus now possible to give editor access to an anonymous user or any authenticated user.
This commit is contained in:
committed by
Samuel Paccoud
parent
41260de1c3
commit
494638d306
@@ -11,6 +11,7 @@ and this project adheres to
|
|||||||
|
|
||||||
## Added
|
## Added
|
||||||
|
|
||||||
|
- ✨Add link public/authenticated/restricted access with read/editor roles #234
|
||||||
- ✨(frontend) add copy link button #235
|
- ✨(frontend) add copy link button #235
|
||||||
- 🛂(frontend) access public docs without being logged #235
|
- 🛂(frontend) access public docs without being logged #235
|
||||||
|
|
||||||
@@ -148,4 +149,4 @@ and this project adheres to
|
|||||||
[1.2.0]: https://github.com/numerique-gouv/impress/releases/v1.2.0
|
[1.2.0]: https://github.com/numerique-gouv/impress/releases/v1.2.0
|
||||||
[1.1.0]: https://github.com/numerique-gouv/impress/releases/v1.1.0
|
[1.1.0]: https://github.com/numerique-gouv/impress/releases/v1.1.0
|
||||||
[1.0.0]: https://github.com/numerique-gouv/impress/releases/v1.0.0
|
[1.0.0]: https://github.com/numerique-gouv/impress/releases/v1.0.0
|
||||||
[0.1.0]: https://github.com/numerique-gouv/impress/releases/v0.1.0
|
[0.1.0]: https://github.com/numerique-gouv/impress/releases/v0.1.0
|
||||||
|
|||||||
@@ -92,6 +92,14 @@ class DocumentAdmin(admin.ModelAdmin):
|
|||||||
"""Document admin interface declaration."""
|
"""Document admin interface declaration."""
|
||||||
|
|
||||||
inlines = (DocumentAccessInline,)
|
inlines = (DocumentAccessInline,)
|
||||||
|
list_display = (
|
||||||
|
"id",
|
||||||
|
"title",
|
||||||
|
"link_reach",
|
||||||
|
"link_role",
|
||||||
|
"created_at",
|
||||||
|
"updated_at",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@admin.register(models.Invitation)
|
@admin.register(models.Invitation)
|
||||||
|
|||||||
@@ -62,6 +62,9 @@ class IsOwnedOrPublic(IsAuthenticated):
|
|||||||
class AccessPermission(permissions.BasePermission):
|
class AccessPermission(permissions.BasePermission):
|
||||||
"""Permission class for access objects."""
|
"""Permission class for access objects."""
|
||||||
|
|
||||||
|
def has_permission(self, request, view):
|
||||||
|
return request.user.is_authenticated or view.action != "create"
|
||||||
|
|
||||||
def has_object_permission(self, request, view, obj):
|
def has_object_permission(self, request, view, obj):
|
||||||
"""Check permission for a given object."""
|
"""Check permission for a given object."""
|
||||||
abilities = obj.get_abilities(request.user)
|
abilities = obj.get_abilities(request.user)
|
||||||
|
|||||||
@@ -148,11 +148,20 @@ class DocumentSerializer(BaseResourceSerializer):
|
|||||||
"title",
|
"title",
|
||||||
"accesses",
|
"accesses",
|
||||||
"abilities",
|
"abilities",
|
||||||
"is_public",
|
"link_role",
|
||||||
|
"link_reach",
|
||||||
|
"created_at",
|
||||||
|
"updated_at",
|
||||||
|
]
|
||||||
|
read_only_fields = [
|
||||||
|
"id",
|
||||||
|
"accesses",
|
||||||
|
"abilities",
|
||||||
|
"link_role",
|
||||||
|
"link_reach",
|
||||||
"created_at",
|
"created_at",
|
||||||
"updated_at",
|
"updated_at",
|
||||||
]
|
]
|
||||||
read_only_fields = ["id", "accesses", "abilities", "created_at", "updated_at"]
|
|
||||||
|
|
||||||
|
|
||||||
# Suppress the warning about not implementing `create` and `update` methods
|
# Suppress the warning about not implementing `create` and `update` methods
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ from urllib.parse import urlparse
|
|||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.contrib.postgres.aggregates import ArrayAgg
|
from django.contrib.postgres.aggregates import ArrayAgg
|
||||||
|
from django.core.exceptions import ValidationError
|
||||||
from django.core.files.storage import default_storage
|
from django.core.files.storage import default_storage
|
||||||
from django.db.models import (
|
from django.db.models import (
|
||||||
OuterRef,
|
OuterRef,
|
||||||
@@ -185,10 +186,11 @@ class ResourceViewsetMixin:
|
|||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
"""Custom queryset to get user related resources."""
|
"""Custom queryset to get user related resources."""
|
||||||
queryset = super().get_queryset()
|
queryset = super().get_queryset()
|
||||||
if not self.request.user.is_authenticated:
|
|
||||||
return queryset.filter(is_public=True)
|
|
||||||
|
|
||||||
user = self.request.user
|
user = self.request.user
|
||||||
|
|
||||||
|
if not user.is_authenticated:
|
||||||
|
return queryset
|
||||||
|
|
||||||
user_roles_query = (
|
user_roles_query = (
|
||||||
self.access_model_class.objects.filter(
|
self.access_model_class.objects.filter(
|
||||||
Q(user=user) | Q(team__in=user.teams),
|
Q(user=user) | Q(team__in=user.teams),
|
||||||
@@ -200,25 +202,6 @@ class ResourceViewsetMixin:
|
|||||||
)
|
)
|
||||||
return queryset.annotate(user_roles=Subquery(user_roles_query)).distinct()
|
return queryset.annotate(user_roles=Subquery(user_roles_query)).distinct()
|
||||||
|
|
||||||
def list(self, request, *args, **kwargs):
|
|
||||||
"""Restrict resources returned by the list endpoint"""
|
|
||||||
queryset = self.filter_queryset(self.get_queryset())
|
|
||||||
if self.request.user.is_authenticated:
|
|
||||||
user = self.request.user
|
|
||||||
queryset = queryset.filter(
|
|
||||||
Q(accesses__user=user) | Q(accesses__team__in=user.teams)
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
queryset = queryset.none()
|
|
||||||
|
|
||||||
page = self.paginate_queryset(queryset)
|
|
||||||
if page is not None:
|
|
||||||
serializer = self.get_serializer(page, many=True)
|
|
||||||
return self.get_paginated_response(serializer.data)
|
|
||||||
|
|
||||||
serializer = self.get_serializer(queryset, many=True)
|
|
||||||
return drf_response.Response(serializer.data)
|
|
||||||
|
|
||||||
def perform_create(self, serializer):
|
def perform_create(self, serializer):
|
||||||
"""Set the current user as owner of the newly created object."""
|
"""Set the current user as owner of the newly created object."""
|
||||||
obj = serializer.save()
|
obj = serializer.save()
|
||||||
@@ -324,14 +307,12 @@ class DocumentViewSet(
|
|||||||
ResourceViewsetMixin,
|
ResourceViewsetMixin,
|
||||||
mixins.CreateModelMixin,
|
mixins.CreateModelMixin,
|
||||||
mixins.DestroyModelMixin,
|
mixins.DestroyModelMixin,
|
||||||
mixins.RetrieveModelMixin,
|
|
||||||
mixins.UpdateModelMixin,
|
mixins.UpdateModelMixin,
|
||||||
viewsets.GenericViewSet,
|
viewsets.GenericViewSet,
|
||||||
):
|
):
|
||||||
"""Document ViewSet"""
|
"""Document ViewSet"""
|
||||||
|
|
||||||
permission_classes = [
|
permission_classes = [
|
||||||
permissions.IsAuthenticatedOrSafe,
|
|
||||||
permissions.AccessPermission,
|
permissions.AccessPermission,
|
||||||
]
|
]
|
||||||
serializer_class = serializers.DocumentSerializer
|
serializer_class = serializers.DocumentSerializer
|
||||||
@@ -353,12 +334,62 @@ class DocumentViewSet(
|
|||||||
**{self.resource_field_name: document},
|
**{self.resource_field_name: document},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def list(self, request, *args, **kwargs):
|
||||||
|
"""Restrict resources returned by the list endpoint"""
|
||||||
|
queryset = self.filter_queryset(self.get_queryset())
|
||||||
|
user = self.request.user
|
||||||
|
if user.is_authenticated:
|
||||||
|
queryset = queryset.filter(
|
||||||
|
Q(accesses__user=user)
|
||||||
|
| Q(accesses__team__in=user.teams)
|
||||||
|
| (
|
||||||
|
Q(link_traces__user=user)
|
||||||
|
& ~Q(link_reach=models.LinkReachChoices.RESTRICTED)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
queryset = queryset.none()
|
||||||
|
|
||||||
|
page = self.paginate_queryset(queryset)
|
||||||
|
if page is not None:
|
||||||
|
serializer = self.get_serializer(page, many=True)
|
||||||
|
return self.get_paginated_response(serializer.data)
|
||||||
|
|
||||||
|
serializer = self.get_serializer(queryset, many=True)
|
||||||
|
return drf_response.Response(serializer.data)
|
||||||
|
|
||||||
|
def retrieve(self, request, *args, **kwargs):
|
||||||
|
"""
|
||||||
|
Add a trace that the document was accessed by a user. This is used to list documents
|
||||||
|
on a user's list view even though the user has no specific role in the document (link
|
||||||
|
access when the link reach configuration of the document allows it).
|
||||||
|
"""
|
||||||
|
instance = self.get_object()
|
||||||
|
serializer = self.get_serializer(instance)
|
||||||
|
|
||||||
|
if self.request.user.is_authenticated:
|
||||||
|
try:
|
||||||
|
# Add a trace that the user visited the document (this is needed to include
|
||||||
|
# the document in the user's list view)
|
||||||
|
models.LinkTrace.objects.create(
|
||||||
|
document=instance,
|
||||||
|
user=self.request.user,
|
||||||
|
)
|
||||||
|
except ValidationError:
|
||||||
|
# The trace already exists, so we just pass without doing anything
|
||||||
|
pass
|
||||||
|
|
||||||
|
return drf_response.Response(serializer.data)
|
||||||
|
|
||||||
@decorators.action(detail=True, methods=["get"], url_path="versions")
|
@decorators.action(detail=True, methods=["get"], url_path="versions")
|
||||||
def versions_list(self, request, *args, **kwargs):
|
def versions_list(self, request, *args, **kwargs):
|
||||||
"""
|
"""
|
||||||
Return the document's versions but only those created after the user got access
|
Return the document's versions but only those created after the user got access
|
||||||
to the document
|
to the document
|
||||||
"""
|
"""
|
||||||
|
if not request.user.is_authenticated:
|
||||||
|
raise exceptions.PermissionDenied("Authentication required.")
|
||||||
|
|
||||||
document = self.get_object()
|
document = self.get_object()
|
||||||
user = request.user
|
user = request.user
|
||||||
from_datetime = min(
|
from_datetime = min(
|
||||||
@@ -555,6 +586,27 @@ class TemplateViewSet(
|
|||||||
resource_field_name = "template"
|
resource_field_name = "template"
|
||||||
queryset = models.Template.objects.all()
|
queryset = models.Template.objects.all()
|
||||||
|
|
||||||
|
def list(self, request, *args, **kwargs):
|
||||||
|
"""Restrict templates returned by the list endpoint"""
|
||||||
|
queryset = self.filter_queryset(self.get_queryset())
|
||||||
|
user = self.request.user
|
||||||
|
if user.is_authenticated:
|
||||||
|
queryset = queryset.filter(
|
||||||
|
Q(accesses__user=user)
|
||||||
|
| Q(accesses__team__in=user.teams)
|
||||||
|
| Q(is_public=True)
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
queryset = queryset.filter(is_public=True)
|
||||||
|
|
||||||
|
page = self.paginate_queryset(queryset)
|
||||||
|
if page is not None:
|
||||||
|
serializer = self.get_serializer(page, many=True)
|
||||||
|
return self.get_paginated_response(serializer.data)
|
||||||
|
|
||||||
|
serializer = self.get_serializer(queryset, many=True)
|
||||||
|
return drf_response.Response(serializer.data)
|
||||||
|
|
||||||
@decorators.action(
|
@decorators.action(
|
||||||
detail=True,
|
detail=True,
|
||||||
methods=["post"],
|
methods=["post"],
|
||||||
|
|||||||
@@ -35,8 +35,13 @@ class DocumentFactory(factory.django.DjangoModelFactory):
|
|||||||
skip_postgeneration_save = True
|
skip_postgeneration_save = True
|
||||||
|
|
||||||
title = factory.Sequence(lambda n: f"document{n}")
|
title = factory.Sequence(lambda n: f"document{n}")
|
||||||
is_public = factory.Faker("boolean")
|
|
||||||
content = factory.Sequence(lambda n: f"content{n}")
|
content = factory.Sequence(lambda n: f"content{n}")
|
||||||
|
link_reach = factory.fuzzy.FuzzyChoice(
|
||||||
|
[a[0] for a in models.LinkReachChoices.choices]
|
||||||
|
)
|
||||||
|
link_role = factory.fuzzy.FuzzyChoice(
|
||||||
|
[r[0] for r in models.LinkRoleChoices.choices]
|
||||||
|
)
|
||||||
|
|
||||||
@factory.post_generation
|
@factory.post_generation
|
||||||
def users(self, create, extracted, **kwargs):
|
def users(self, create, extracted, **kwargs):
|
||||||
@@ -48,6 +53,13 @@ class DocumentFactory(factory.django.DjangoModelFactory):
|
|||||||
else:
|
else:
|
||||||
UserDocumentAccessFactory(document=self, user=item[0], role=item[1])
|
UserDocumentAccessFactory(document=self, user=item[0], role=item[1])
|
||||||
|
|
||||||
|
@factory.post_generation
|
||||||
|
def link_traces(self, create, extracted, **kwargs):
|
||||||
|
"""Add link traces to document from a given list of users."""
|
||||||
|
if create and extracted:
|
||||||
|
for item in extracted:
|
||||||
|
models.LinkTrace.objects.create(document=self, user=item)
|
||||||
|
|
||||||
|
|
||||||
class UserDocumentAccessFactory(factory.django.DjangoModelFactory):
|
class UserDocumentAccessFactory(factory.django.DjangoModelFactory):
|
||||||
"""Create fake document user accesses for testing."""
|
"""Create fake document user accesses for testing."""
|
||||||
|
|||||||
@@ -0,0 +1,52 @@
|
|||||||
|
# Generated by Django 5.1 on 2024-09-08 16:55
|
||||||
|
|
||||||
|
import django.db.models.deletion
|
||||||
|
import uuid
|
||||||
|
from django.conf import settings
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('core', '0002_create_pg_trgm_extension'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='document',
|
||||||
|
name='link_reach',
|
||||||
|
field=models.CharField(choices=[('restricted', 'Restricted'), ('authenticated', 'Authenticated'), ('public', 'Public')], default='authenticated', max_length=20),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='document',
|
||||||
|
name='link_role',
|
||||||
|
field=models.CharField(choices=[('reader', 'Reader'), ('editor', 'Editor')], default='reader', max_length=20),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='document',
|
||||||
|
name='is_public',
|
||||||
|
field=models.BooleanField(null=True),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='user',
|
||||||
|
name='language',
|
||||||
|
field=models.CharField(choices="(('en-us', 'English'), ('fr-fr', 'French'))", default='en-us', help_text='The language in which the user wants to see the interface.', max_length=10, verbose_name='language'),
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='LinkTrace',
|
||||||
|
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')),
|
||||||
|
('document', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='link_traces', to='core.document')),
|
||||||
|
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='link_traces', to=settings.AUTH_USER_MODEL)),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'verbose_name': 'Document/user link trace',
|
||||||
|
'verbose_name_plural': 'Document/user link traces',
|
||||||
|
'db_table': 'impress_link_trace',
|
||||||
|
'constraints': [models.UniqueConstraint(fields=('user', 'document'), name='unique_link_trace_document_user', violation_error_message='A link trace already exists for this document/user.')],
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
# Generated by Django 5.1 on 2024-09-08 17:04
|
||||||
|
from django.db import migrations
|
||||||
|
|
||||||
|
def migrate_is_public_to_link_reach(apps, schema_editor):
|
||||||
|
"""
|
||||||
|
Forward migration: Migrate 'is_public' to 'link_reach'.
|
||||||
|
If is_public == True, set link_reach to 'public'
|
||||||
|
"""
|
||||||
|
Document = apps.get_model('core', 'Document')
|
||||||
|
Document.objects.filter(is_public=True).update(link_reach='public')
|
||||||
|
|
||||||
|
|
||||||
|
def reverse_migrate_link_reach_to_is_public(apps, schema_editor):
|
||||||
|
"""
|
||||||
|
Reverse migration: Migrate 'link_reach' back to 'is_public'.
|
||||||
|
- If link_reach == 'public', set is_public to True
|
||||||
|
- Else set is_public to False
|
||||||
|
"""
|
||||||
|
Document = apps.get_model('core', 'Document')
|
||||||
|
Document.objects.filter(link_reach='public').update(is_public=True)
|
||||||
|
Document.objects.filter(link_reach__in=['restricted', "authenticated"]).update(is_public=False)
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('core', '0003_document_link_reach_document_link_role_and_more'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.RunPython(
|
||||||
|
migrate_is_public_to_link_reach,
|
||||||
|
reverse_migrate_link_reach_to_is_public
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -51,8 +51,15 @@ def get_resource_roles(resource, user):
|
|||||||
return roles
|
return roles
|
||||||
|
|
||||||
|
|
||||||
|
class LinkRoleChoices(models.TextChoices):
|
||||||
|
"""Defines the possible roles a link can offer on a document."""
|
||||||
|
|
||||||
|
READER = "reader", _("Reader") # Can read
|
||||||
|
EDITOR = "editor", _("Editor") # Can read and edit
|
||||||
|
|
||||||
|
|
||||||
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 resource."""
|
||||||
|
|
||||||
READER = "reader", _("Reader") # Can read
|
READER = "reader", _("Reader") # Can read
|
||||||
EDITOR = "editor", _("Editor") # Can read and edit
|
EDITOR = "editor", _("Editor") # Can read and edit
|
||||||
@@ -60,6 +67,20 @@ class RoleChoices(models.TextChoices):
|
|||||||
OWNER = "owner", _("Owner")
|
OWNER = "owner", _("Owner")
|
||||||
|
|
||||||
|
|
||||||
|
class LinkReachChoices(models.TextChoices):
|
||||||
|
"""Defines types of access for links"""
|
||||||
|
|
||||||
|
RESTRICTED = (
|
||||||
|
"restricted",
|
||||||
|
_("Restricted"),
|
||||||
|
) # Only users with a specific access can read/edit the document
|
||||||
|
AUTHENTICATED = (
|
||||||
|
"authenticated",
|
||||||
|
_("Authenticated"),
|
||||||
|
) # Any authenticated user can access the document
|
||||||
|
PUBLIC = "public", _("Public") # Even anonymous users can access the document
|
||||||
|
|
||||||
|
|
||||||
class BaseModel(models.Model):
|
class BaseModel(models.Model):
|
||||||
"""
|
"""
|
||||||
Serves as an abstract base model for other models, ensuring that records are validated
|
Serves as an abstract base model for other models, ensuring that records are validated
|
||||||
@@ -300,10 +321,13 @@ class Document(BaseModel):
|
|||||||
"""Pad document carrying the content."""
|
"""Pad document carrying the content."""
|
||||||
|
|
||||||
title = models.CharField(_("title"), max_length=255)
|
title = models.CharField(_("title"), max_length=255)
|
||||||
is_public = models.BooleanField(
|
link_reach = models.CharField(
|
||||||
_("public"),
|
max_length=20,
|
||||||
default=False,
|
choices=LinkReachChoices.choices,
|
||||||
help_text=_("Whether this document is public for anyone to use."),
|
default=LinkReachChoices.AUTHENTICATED,
|
||||||
|
)
|
||||||
|
link_role = models.CharField(
|
||||||
|
max_length=20, choices=LinkRoleChoices.choices, default=LinkRoleChoices.READER
|
||||||
)
|
)
|
||||||
|
|
||||||
_content = None
|
_content = None
|
||||||
@@ -466,17 +490,28 @@ class Document(BaseModel):
|
|||||||
"""
|
"""
|
||||||
Compute and return abilities for a given user on the document.
|
Compute and return abilities for a given user on the document.
|
||||||
"""
|
"""
|
||||||
roles = get_resource_roles(self, user)
|
roles = set(get_resource_roles(self, user))
|
||||||
is_owner_or_admin = bool(
|
|
||||||
set(roles).intersection({RoleChoices.OWNER, RoleChoices.ADMIN})
|
# Compute version roles before adding link roles because we don't
|
||||||
)
|
# want anonymous users to access versions (we wouldn't know from
|
||||||
is_editor = bool(RoleChoices.EDITOR in roles)
|
# which date to allow them anyway)
|
||||||
can_get = self.is_public or bool(roles)
|
|
||||||
can_get_versions = bool(roles)
|
can_get_versions = bool(roles)
|
||||||
|
|
||||||
|
# Add role provided by the document link
|
||||||
|
if self.link_reach == LinkReachChoices.PUBLIC or (
|
||||||
|
self.link_reach == LinkReachChoices.AUTHENTICATED and user.is_authenticated
|
||||||
|
):
|
||||||
|
roles.add(self.link_role)
|
||||||
|
|
||||||
|
is_owner_or_admin = bool(
|
||||||
|
roles.intersection({RoleChoices.OWNER, RoleChoices.ADMIN})
|
||||||
|
)
|
||||||
|
is_editor = bool(RoleChoices.EDITOR in roles)
|
||||||
|
can_get = bool(roles)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"destroy": RoleChoices.OWNER in roles,
|
|
||||||
"attachment_upload": is_owner_or_admin or is_editor,
|
"attachment_upload": is_owner_or_admin or is_editor,
|
||||||
|
"destroy": RoleChoices.OWNER in roles,
|
||||||
"manage_accesses": is_owner_or_admin,
|
"manage_accesses": is_owner_or_admin,
|
||||||
"partial_update": is_owner_or_admin or is_editor,
|
"partial_update": is_owner_or_admin or is_editor,
|
||||||
"retrieve": can_get,
|
"retrieve": can_get,
|
||||||
@@ -487,6 +522,38 @@ class Document(BaseModel):
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class LinkTrace(BaseModel):
|
||||||
|
"""
|
||||||
|
Relation model to trace accesses to a document via a link by a logged-in user.
|
||||||
|
This is necessary to show the document in the user's list of documents even
|
||||||
|
though the user does not have a role on the document.
|
||||||
|
"""
|
||||||
|
|
||||||
|
document = models.ForeignKey(
|
||||||
|
Document,
|
||||||
|
on_delete=models.CASCADE,
|
||||||
|
related_name="link_traces",
|
||||||
|
)
|
||||||
|
user = models.ForeignKey(User, on_delete=models.CASCADE, related_name="link_traces")
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
db_table = "impress_link_trace"
|
||||||
|
verbose_name = _("Document/user link trace")
|
||||||
|
verbose_name_plural = _("Document/user link traces")
|
||||||
|
constraints = [
|
||||||
|
models.UniqueConstraint(
|
||||||
|
fields=["user", "document"],
|
||||||
|
name="unique_link_trace_document_user",
|
||||||
|
violation_error_message=_(
|
||||||
|
"A link trace already exists for this document/user."
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return f"{self.user!s} trace on document {self.document!s}"
|
||||||
|
|
||||||
|
|
||||||
class DocumentAccess(BaseAccess):
|
class DocumentAccess(BaseAccess):
|
||||||
"""Relation model to give access to a document for a user or a team with a role."""
|
"""Relation model to give access to a document for a user or a team with a role."""
|
||||||
|
|
||||||
|
|||||||
@@ -14,62 +14,29 @@ from core.tests.conftest import TEAM, USER, VIA
|
|||||||
pytestmark = pytest.mark.django_db
|
pytestmark = pytest.mark.django_db
|
||||||
|
|
||||||
|
|
||||||
def test_api_document_versions_list_anonymous_public():
|
@pytest.mark.parametrize("reach", models.LinkReachChoices.values)
|
||||||
|
@pytest.mark.parametrize("role", models.LinkRoleChoices.values)
|
||||||
|
def test_api_document_versions_list_anonymous(role, reach):
|
||||||
"""
|
"""
|
||||||
Anonymous users should not be allowed to list document versions for a public document.
|
Anonymous users should not be allowed to list document versions for a document
|
||||||
|
whatever the reach and role.
|
||||||
"""
|
"""
|
||||||
document = factories.DocumentFactory(is_public=True)
|
document = factories.DocumentFactory(link_role=role, link_reach=reach)
|
||||||
factories.UserDocumentAccessFactory.create_batch(2, document=document)
|
|
||||||
|
# Accesses and traces for other users should not interfere
|
||||||
|
factories.UserDocumentAccessFactory(document=document)
|
||||||
|
models.LinkTrace.objects.create(document=document, user=factories.UserFactory())
|
||||||
|
|
||||||
response = APIClient().get(f"/api/v1.0/documents/{document.id!s}/versions/")
|
response = APIClient().get(f"/api/v1.0/documents/{document.id!s}/versions/")
|
||||||
|
|
||||||
assert response.status_code == 401
|
|
||||||
assert response.json() == {
|
|
||||||
"detail": "Authentication credentials were not provided."
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def test_api_document_versions_list_anonymous_private():
|
|
||||||
"""
|
|
||||||
Anonymous users should not be allowed to find document versions for a private document.
|
|
||||||
"""
|
|
||||||
document = factories.DocumentFactory(is_public=False)
|
|
||||||
factories.UserDocumentAccessFactory.create_batch(2, document=document)
|
|
||||||
|
|
||||||
response = APIClient().get(f"/api/v1.0/documents/{document.id!s}/versions/")
|
|
||||||
|
|
||||||
assert response.status_code == 404
|
|
||||||
assert response.json() == {"detail": "No Document matches the given query."}
|
|
||||||
|
|
||||||
|
|
||||||
def test_api_document_versions_list_authenticated_unrelated_public():
|
|
||||||
"""
|
|
||||||
Authenticated users should not be allowed to list document versions for a public document
|
|
||||||
to which they are not related.
|
|
||||||
"""
|
|
||||||
user = factories.UserFactory()
|
|
||||||
|
|
||||||
client = APIClient()
|
|
||||||
client.force_login(user)
|
|
||||||
|
|
||||||
document = factories.DocumentFactory(is_public=True)
|
|
||||||
factories.UserDocumentAccessFactory.create_batch(3, document=document)
|
|
||||||
|
|
||||||
# The versions of another document to which the user is related should not be listed either
|
|
||||||
factories.UserDocumentAccessFactory(user=user)
|
|
||||||
|
|
||||||
response = client.get(
|
|
||||||
f"/api/v1.0/documents/{document.id!s}/versions/",
|
|
||||||
)
|
|
||||||
assert response.status_code == 403
|
assert response.status_code == 403
|
||||||
assert response.json() == {
|
assert response.json() == {'detail': 'Authentication required.'}
|
||||||
"detail": "You do not have permission to perform this action."
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def test_api_document_versions_list_authenticated_unrelated_private():
|
@pytest.mark.parametrize("reach", models.LinkReachChoices.values)
|
||||||
|
def test_api_document_versions_list_authenticated_unrelated(reach):
|
||||||
"""
|
"""
|
||||||
Authenticated users should not be allowed to find document versions for a private document
|
Authenticated users should not be allowed to list document versions for a document
|
||||||
to which they are not related.
|
to which they are not related.
|
||||||
"""
|
"""
|
||||||
user = factories.UserFactory()
|
user = factories.UserFactory()
|
||||||
@@ -77,7 +44,7 @@ def test_api_document_versions_list_authenticated_unrelated_private():
|
|||||||
client = APIClient()
|
client = APIClient()
|
||||||
client.force_login(user)
|
client.force_login(user)
|
||||||
|
|
||||||
document = factories.DocumentFactory(is_public=False)
|
document = factories.DocumentFactory(link_reach=reach)
|
||||||
factories.UserDocumentAccessFactory.create_batch(3, document=document)
|
factories.UserDocumentAccessFactory.create_batch(3, document=document)
|
||||||
|
|
||||||
# The versions of another document to which the user is related should not be listed either
|
# The versions of another document to which the user is related should not be listed either
|
||||||
@@ -145,11 +112,13 @@ def test_api_document_versions_list_authenticated_related(via, mock_user_teams):
|
|||||||
assert content["count"] == 1
|
assert content["count"] == 1
|
||||||
|
|
||||||
|
|
||||||
def test_api_document_versions_retrieve_anonymous_public():
|
@pytest.mark.parametrize("reach", models.LinkReachChoices.values)
|
||||||
|
def test_api_document_versions_retrieve_anonymous(reach):
|
||||||
"""
|
"""
|
||||||
Anonymous users should not be allowed to retrieve specific versions for a public document.
|
Anonymous users should not be allowed to find specific versions for a document with
|
||||||
|
restricted or authenticated link reach.
|
||||||
"""
|
"""
|
||||||
document = factories.DocumentFactory(is_public=True)
|
document = factories.DocumentFactory(link_reach=reach)
|
||||||
version_id = document.get_versions_slice()["versions"][0]["version_id"]
|
version_id = document.get_versions_slice()["versions"][0]["version_id"]
|
||||||
|
|
||||||
url = f"/api/v1.0/documents/{document.id!s}/versions/{version_id:s}/"
|
url = f"/api/v1.0/documents/{document.id!s}/versions/{version_id:s}/"
|
||||||
@@ -161,23 +130,10 @@ def test_api_document_versions_retrieve_anonymous_public():
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def test_api_document_versions_retrieve_anonymous_private():
|
@pytest.mark.parametrize("reach", models.LinkReachChoices.values)
|
||||||
|
def test_api_document_versions_retrieve_authenticated_unrelated(reach):
|
||||||
"""
|
"""
|
||||||
Anonymous users should not be allowed to find specific versions for a private document.
|
Authenticated users should not be allowed to retrieve specific versions for a
|
||||||
"""
|
|
||||||
document = factories.DocumentFactory(is_public=False)
|
|
||||||
version_id = document.get_versions_slice()["versions"][0]["version_id"]
|
|
||||||
|
|
||||||
url = f"/api/v1.0/documents/{document.id!s}/versions/{version_id:s}/"
|
|
||||||
response = APIClient().get(url)
|
|
||||||
|
|
||||||
assert response.status_code == 404
|
|
||||||
assert response.json() == {"detail": "No Document matches the given query."}
|
|
||||||
|
|
||||||
|
|
||||||
def test_api_document_versions_retrieve_authenticated_unrelated_public():
|
|
||||||
"""
|
|
||||||
Authenticated users should not be allowed to retrieve specific versions for a public
|
|
||||||
document to which they are not related.
|
document to which they are not related.
|
||||||
"""
|
"""
|
||||||
user = factories.UserFactory()
|
user = factories.UserFactory()
|
||||||
@@ -185,29 +141,7 @@ def test_api_document_versions_retrieve_authenticated_unrelated_public():
|
|||||||
client = APIClient()
|
client = APIClient()
|
||||||
client.force_login(user)
|
client.force_login(user)
|
||||||
|
|
||||||
document = factories.DocumentFactory(is_public=True)
|
document = factories.DocumentFactory(link_reach=reach)
|
||||||
version_id = document.get_versions_slice()["versions"][0]["version_id"]
|
|
||||||
|
|
||||||
response = client.get(
|
|
||||||
f"/api/v1.0/documents/{document.id!s}/versions/{version_id:s}/",
|
|
||||||
)
|
|
||||||
assert response.status_code == 403
|
|
||||||
assert response.json() == {
|
|
||||||
"detail": "You do not have permission to perform this action."
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def test_api_document_versions_retrieve_authenticated_unrelated_private():
|
|
||||||
"""
|
|
||||||
Authenticated users should not be allowed to find specific versions for a private document
|
|
||||||
to which they are not related.
|
|
||||||
"""
|
|
||||||
user = factories.UserFactory()
|
|
||||||
|
|
||||||
client = APIClient()
|
|
||||||
client.force_login(user)
|
|
||||||
|
|
||||||
document = factories.DocumentFactory(is_public=False)
|
|
||||||
version_id = document.get_versions_slice()["versions"][0]["version_id"]
|
version_id = document.get_versions_slice()["versions"][0]["version_id"]
|
||||||
|
|
||||||
response = client.get(
|
response = client.get(
|
||||||
@@ -271,10 +205,8 @@ def test_api_document_versions_create_anonymous():
|
|||||||
format="json",
|
format="json",
|
||||||
)
|
)
|
||||||
|
|
||||||
assert response.status_code == 401
|
assert response.status_code == 405
|
||||||
assert response.json() == {
|
assert response.json() == {"detail": 'Method "POST" not allowed.'}
|
||||||
"detail": "Authentication credentials were not provided."
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def test_api_document_versions_create_authenticated_unrelated():
|
def test_api_document_versions_create_authenticated_unrelated():
|
||||||
@@ -335,7 +267,7 @@ def test_api_document_versions_update_anonymous():
|
|||||||
{"foo": "bar"},
|
{"foo": "bar"},
|
||||||
format="json",
|
format="json",
|
||||||
)
|
)
|
||||||
assert response.status_code == 401
|
assert response.status_code == 405
|
||||||
|
|
||||||
|
|
||||||
def test_api_document_versions_update_authenticated_unrelated():
|
def test_api_document_versions_update_authenticated_unrelated():
|
||||||
@@ -401,7 +333,8 @@ def test_api_document_versions_delete_anonymous():
|
|||||||
assert response.status_code == 401
|
assert response.status_code == 401
|
||||||
|
|
||||||
|
|
||||||
def test_api_document_versions_delete_authenticated_public():
|
@pytest.mark.parametrize("reach", models.LinkReachChoices.values)
|
||||||
|
def test_api_document_versions_delete_authenticated(reach):
|
||||||
"""
|
"""
|
||||||
Authenticated users should not be allowed to delete a document version for a
|
Authenticated users should not be allowed to delete a document version for a
|
||||||
public document to which they are not related.
|
public document to which they are not related.
|
||||||
@@ -411,7 +344,7 @@ def test_api_document_versions_delete_authenticated_public():
|
|||||||
client = APIClient()
|
client = APIClient()
|
||||||
client.force_login(user)
|
client.force_login(user)
|
||||||
|
|
||||||
document = factories.DocumentFactory(is_public=True)
|
document = factories.DocumentFactory(link_reach=reach)
|
||||||
version_id = document.get_versions_slice()["versions"][0]["version_id"]
|
version_id = document.get_versions_slice()["versions"][0]["version_id"]
|
||||||
|
|
||||||
response = client.delete(
|
response = client.delete(
|
||||||
@@ -421,29 +354,6 @@ def test_api_document_versions_delete_authenticated_public():
|
|||||||
assert response.status_code == 403
|
assert response.status_code == 403
|
||||||
|
|
||||||
|
|
||||||
def test_api_document_versions_delete_authenticated_private():
|
|
||||||
"""
|
|
||||||
Authenticated users should not be allowed to find a document version to delete it
|
|
||||||
for a private document to which they are not related.
|
|
||||||
"""
|
|
||||||
user = factories.UserFactory()
|
|
||||||
|
|
||||||
client = APIClient()
|
|
||||||
client.force_login(user)
|
|
||||||
|
|
||||||
document = factories.DocumentFactory(is_public=False)
|
|
||||||
version_id = document.get_versions_slice()["versions"][0]["version_id"]
|
|
||||||
|
|
||||||
response = client.delete(
|
|
||||||
f"/api/v1.0/documents/{document.id!s}/versions/{version_id:s}/",
|
|
||||||
)
|
|
||||||
|
|
||||||
assert response.status_code == 403
|
|
||||||
assert response.json() == {
|
|
||||||
"detail": "You do not have permission to perform this action."
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("role", ["reader", "editor"])
|
@pytest.mark.parametrize("role", ["reader", "editor"])
|
||||||
@pytest.mark.parametrize("via", VIA)
|
@pytest.mark.parametrize("via", VIA)
|
||||||
def test_api_document_versions_delete_reader_or_editor(via, role, mock_user_teams):
|
def test_api_document_versions_delete_reader_or_editor(via, role, mock_user_teams):
|
||||||
|
|||||||
@@ -17,9 +17,22 @@ from core.tests.conftest import TEAM, USER, VIA
|
|||||||
pytestmark = pytest.mark.django_db
|
pytestmark = pytest.mark.django_db
|
||||||
|
|
||||||
|
|
||||||
def test_api_documents_attachment_upload_anonymous():
|
@pytest.mark.parametrize(
|
||||||
"""Anonymous users can't upload attachments to a document."""
|
"reach, role",
|
||||||
document = factories.DocumentFactory()
|
[
|
||||||
|
("restricted", "reader"),
|
||||||
|
("restricted", "editor"),
|
||||||
|
("authenticated", "reader"),
|
||||||
|
("authenticated", "editor"),
|
||||||
|
("public", "reader"),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_api_documents_attachment_upload_anonymous_forbidden(reach, role):
|
||||||
|
"""
|
||||||
|
Anonymous users should not be able to upload attachments if the link reach
|
||||||
|
and role don't allow it.
|
||||||
|
"""
|
||||||
|
document = factories.DocumentFactory(link_reach=reach, link_role=role)
|
||||||
file = SimpleUploadedFile("test_file.jpg", b"Dummy content")
|
file = SimpleUploadedFile("test_file.jpg", b"Dummy content")
|
||||||
|
|
||||||
url = f"/api/v1.0/documents/{document.id!s}/attachment-upload/"
|
url = f"/api/v1.0/documents/{document.id!s}/attachment-upload/"
|
||||||
@@ -31,16 +44,47 @@ def test_api_documents_attachment_upload_anonymous():
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def test_api_documents_attachment_upload_authenticated_public():
|
def test_api_documents_attachment_upload_anonymous_success():
|
||||||
"""
|
"""
|
||||||
Users who are not related to a public document should not be allowed to upload an attachment.
|
Anonymous users should be able to upload attachments to a document
|
||||||
|
if the link reach and role permit it.
|
||||||
|
"""
|
||||||
|
document = factories.DocumentFactory(link_reach="public", link_role="editor")
|
||||||
|
file = SimpleUploadedFile("test_file.jpg", b"Dummy content")
|
||||||
|
|
||||||
|
url = f"/api/v1.0/documents/{document.id!s}/attachment-upload/"
|
||||||
|
response = APIClient().post(url, {"file": file}, format="multipart")
|
||||||
|
|
||||||
|
assert response.status_code == 201
|
||||||
|
|
||||||
|
pattern = re.compile(rf"^/media/{document.id!s}/attachments/(.*)\.jpg")
|
||||||
|
match = pattern.search(response.json()["file"])
|
||||||
|
file_id = match.group(1)
|
||||||
|
|
||||||
|
# Validate that file_id is a valid UUID
|
||||||
|
uuid.UUID(file_id)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"reach, role",
|
||||||
|
[
|
||||||
|
("restricted", "reader"),
|
||||||
|
("restricted", "editor"),
|
||||||
|
("authenticated", "reader"),
|
||||||
|
("public", "reader"),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_api_documents_attachment_upload_authenticated_forbidden(reach, role):
|
||||||
|
"""
|
||||||
|
Users who are not related to a document can't upload attachments if the
|
||||||
|
link reach and role don't allow it.
|
||||||
"""
|
"""
|
||||||
user = factories.UserFactory()
|
user = factories.UserFactory()
|
||||||
|
|
||||||
client = APIClient()
|
client = APIClient()
|
||||||
client.force_login(user)
|
client.force_login(user)
|
||||||
|
|
||||||
document = factories.DocumentFactory(is_public=True)
|
document = factories.DocumentFactory(link_reach=reach, link_role=role)
|
||||||
file = SimpleUploadedFile("test_file.jpg", b"Dummy content")
|
file = SimpleUploadedFile("test_file.jpg", b"Dummy content")
|
||||||
|
|
||||||
url = f"/api/v1.0/documents/{document.id!s}/attachment-upload/"
|
url = f"/api/v1.0/documents/{document.id!s}/attachment-upload/"
|
||||||
@@ -52,25 +96,37 @@ def test_api_documents_attachment_upload_authenticated_public():
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def test_api_documents_attachment_upload_authenticated_private():
|
@pytest.mark.parametrize(
|
||||||
|
"reach, role",
|
||||||
|
[
|
||||||
|
("authenticated", "editor"),
|
||||||
|
("public", "editor"),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_api_documents_attachment_upload_authenticated_success(reach, role):
|
||||||
"""
|
"""
|
||||||
Users who are not related to a private document should not be able to upload an attachment.
|
Autenticated who are not related to a document should be able to upload a file
|
||||||
|
if the link reach and role permit it.
|
||||||
"""
|
"""
|
||||||
user = factories.UserFactory()
|
user = factories.UserFactory()
|
||||||
|
|
||||||
client = APIClient()
|
client = APIClient()
|
||||||
client.force_login(user)
|
client.force_login(user)
|
||||||
|
|
||||||
document = factories.DocumentFactory(is_public=False)
|
document = factories.DocumentFactory(link_reach=reach, link_role=role)
|
||||||
file = SimpleUploadedFile("test_file.jpg", b"Dummy content")
|
file = SimpleUploadedFile("test_file.jpg", b"Dummy content")
|
||||||
|
|
||||||
url = f"/api/v1.0/documents/{document.id!s}/attachment-upload/"
|
url = f"/api/v1.0/documents/{document.id!s}/attachment-upload/"
|
||||||
response = client.post(url, {"file": file}, format="multipart")
|
response = client.post(url, {"file": file}, format="multipart")
|
||||||
|
|
||||||
assert response.status_code == 403
|
assert response.status_code == 201
|
||||||
assert response.json() == {
|
|
||||||
"detail": "You do not have permission to perform this action."
|
pattern = re.compile(rf"^/media/{document.id!s}/attachments/(.*)\.jpg")
|
||||||
}
|
match = pattern.search(response.json()["file"])
|
||||||
|
file_id = match.group(1)
|
||||||
|
|
||||||
|
# Validate that file_id is a valid UUID
|
||||||
|
uuid.UUID(file_id)
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("via", VIA)
|
@pytest.mark.parametrize("via", VIA)
|
||||||
@@ -83,7 +139,7 @@ def test_api_documents_attachment_upload_reader(via, mock_user_teams):
|
|||||||
client = APIClient()
|
client = APIClient()
|
||||||
client.force_login(user)
|
client.force_login(user)
|
||||||
|
|
||||||
document = factories.DocumentFactory()
|
document = factories.DocumentFactory(link_role="reader")
|
||||||
if via == USER:
|
if via == USER:
|
||||||
factories.UserDocumentAccessFactory(document=document, user=user, role="reader")
|
factories.UserDocumentAccessFactory(document=document, user=user, role="reader")
|
||||||
elif via == TEAM:
|
elif via == TEAM:
|
||||||
|
|||||||
@@ -2,8 +2,6 @@
|
|||||||
Tests for Documents API endpoint in impress's core app: delete
|
Tests for Documents API endpoint in impress's core app: delete
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import random
|
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
from rest_framework.test import APIClient
|
from rest_framework.test import APIClient
|
||||||
|
|
||||||
@@ -25,24 +23,25 @@ def test_api_documents_delete_anonymous():
|
|||||||
assert models.Document.objects.count() == 1
|
assert models.Document.objects.count() == 1
|
||||||
|
|
||||||
|
|
||||||
def test_api_documents_delete_authenticated_unrelated():
|
@pytest.mark.parametrize("reach", models.LinkReachChoices.values)
|
||||||
|
@pytest.mark.parametrize("role", models.LinkRoleChoices.values)
|
||||||
|
def test_api_documents_delete_authenticated_unrelated(reach, role):
|
||||||
"""
|
"""
|
||||||
Authenticated users should not be allowed to delete a document to which they are not
|
Authenticated users should not be allowed to delete a document to which
|
||||||
related.
|
they are not related.
|
||||||
"""
|
"""
|
||||||
user = factories.UserFactory()
|
user = factories.UserFactory()
|
||||||
|
|
||||||
client = APIClient()
|
client = APIClient()
|
||||||
client.force_login(user)
|
client.force_login(user)
|
||||||
|
|
||||||
is_public = random.choice([True, False])
|
document = factories.DocumentFactory(link_reach=reach, link_role=role)
|
||||||
document = factories.DocumentFactory(is_public=is_public)
|
|
||||||
|
|
||||||
response = client.delete(
|
response = client.delete(
|
||||||
f"/api/v1.0/documents/{document.id!s}/",
|
f"/api/v1.0/documents/{document.id!s}/",
|
||||||
)
|
)
|
||||||
|
|
||||||
assert response.status_code == 403 if is_public else 404
|
assert response.status_code == 403
|
||||||
assert models.Document.objects.count() == 1
|
assert models.Document.objects.count() == 1
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
Tests for Documents API endpoint in impress's core app: list
|
Tests for Documents API endpoint in impress's core app: list
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
import operator
|
||||||
from unittest import mock
|
from unittest import mock
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
@@ -9,46 +10,50 @@ from faker import Faker
|
|||||||
from rest_framework.pagination import PageNumberPagination
|
from rest_framework.pagination import PageNumberPagination
|
||||||
from rest_framework.test import APIClient
|
from rest_framework.test import APIClient
|
||||||
|
|
||||||
from core import factories
|
from core import factories, models
|
||||||
|
|
||||||
fake = Faker()
|
fake = Faker()
|
||||||
pytestmark = pytest.mark.django_db
|
pytestmark = pytest.mark.django_db
|
||||||
|
|
||||||
|
|
||||||
def test_api_documents_list_anonymous():
|
@pytest.mark.parametrize("role", models.LinkRoleChoices.values)
|
||||||
"""Anonymous users should only be able to list documents public or not."""
|
@pytest.mark.parametrize("reach", models.LinkReachChoices.values)
|
||||||
factories.DocumentFactory.create_batch(2, is_public=False)
|
def test_api_documents_list_anonymous(reach, role):
|
||||||
factories.DocumentFactory.create_batch(2, is_public=True)
|
"""
|
||||||
|
Anonymous users should not be allowed to list documents whatever the
|
||||||
|
link reach and the role
|
||||||
|
"""
|
||||||
|
factories.DocumentFactory(link_reach=reach, link_role=role)
|
||||||
|
|
||||||
response = APIClient().get("/api/v1.0/documents/")
|
response = APIClient().get("/api/v1.0/documents/")
|
||||||
|
|
||||||
assert response.status_code == 200
|
assert response.status_code == 200
|
||||||
assert response.json() == {
|
results = response.json()["results"]
|
||||||
"count": 0,
|
assert len(results) == 0
|
||||||
"next": None,
|
|
||||||
"previous": None,
|
|
||||||
"results": [],
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def test_api_documents_list_authenticated_direct():
|
def test_api_documents_list_authenticated_direct():
|
||||||
"""
|
"""
|
||||||
Authenticated users should be able to list documents they are a direct
|
Authenticated users should be able to list documents they are a direct
|
||||||
owner/administrator/member of.
|
owner/administrator/member of or documents that have a link reach other
|
||||||
|
than restricted.
|
||||||
"""
|
"""
|
||||||
user = factories.UserFactory()
|
user = factories.UserFactory()
|
||||||
|
|
||||||
client = APIClient()
|
client = APIClient()
|
||||||
client.force_login(user)
|
client.force_login(user)
|
||||||
|
|
||||||
related_documents = [
|
documents = [
|
||||||
access.document
|
access.document
|
||||||
for access in factories.UserDocumentAccessFactory.create_batch(5, user=user)
|
for access in factories.UserDocumentAccessFactory.create_batch(2, user=user)
|
||||||
]
|
]
|
||||||
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}
|
# Unrelated and untraced documents
|
||||||
|
for reach in models.LinkReachChoices:
|
||||||
|
for role in models.LinkRoleChoices:
|
||||||
|
factories.DocumentFactory(link_reach=reach, link_role=role)
|
||||||
|
|
||||||
|
expected_ids = {str(document.id) for document in documents}
|
||||||
|
|
||||||
response = client.get(
|
response = client.get(
|
||||||
"/api/v1.0/documents/",
|
"/api/v1.0/documents/",
|
||||||
@@ -56,7 +61,7 @@ def test_api_documents_list_authenticated_direct():
|
|||||||
|
|
||||||
assert response.status_code == 200
|
assert response.status_code == 200
|
||||||
results = response.json()["results"]
|
results = response.json()["results"]
|
||||||
assert len(results) == 5
|
assert len(results) == 2
|
||||||
results_id = {result["id"] for result in results}
|
results_id = {result["id"] for result in results}
|
||||||
assert expected_ids == results_id
|
assert expected_ids == results_id
|
||||||
|
|
||||||
@@ -81,8 +86,6 @@ def test_api_documents_list_authenticated_via_team(mock_user_teams):
|
|||||||
access.document
|
access.document
|
||||||
for access in factories.TeamDocumentAccessFactory.create_batch(3, team="team2")
|
for access in factories.TeamDocumentAccessFactory.create_batch(3, team="team2")
|
||||||
]
|
]
|
||||||
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}
|
expected_ids = {str(document.id) for document in documents_team1 + documents_team2}
|
||||||
|
|
||||||
@@ -95,6 +98,63 @@ def test_api_documents_list_authenticated_via_team(mock_user_teams):
|
|||||||
assert expected_ids == results_id
|
assert expected_ids == results_id
|
||||||
|
|
||||||
|
|
||||||
|
def test_api_documents_list_authenticated_link_reach_restricted():
|
||||||
|
"""
|
||||||
|
An authenticated user who has link traces to a document that is restricted should not
|
||||||
|
see it on the list view
|
||||||
|
"""
|
||||||
|
user = factories.UserFactory()
|
||||||
|
|
||||||
|
client = APIClient()
|
||||||
|
client.force_login(user)
|
||||||
|
|
||||||
|
document = factories.DocumentFactory(link_traces=[user], link_reach="restricted")
|
||||||
|
|
||||||
|
# Link traces for other documents or other users should not interfere
|
||||||
|
models.LinkTrace.objects.create(document=document, user=factories.UserFactory())
|
||||||
|
other_document = factories.DocumentFactory(link_reach="public")
|
||||||
|
models.LinkTrace.objects.create(document=other_document, user=user)
|
||||||
|
|
||||||
|
response = client.get(
|
||||||
|
"/api/v1.0/documents/",
|
||||||
|
)
|
||||||
|
|
||||||
|
assert response.status_code == 200
|
||||||
|
results = response.json()["results"]
|
||||||
|
# Only the other document is returned but not the restricted document even though the user
|
||||||
|
# visited it earlier (probably b/c it previously had public or authenticated reach...)
|
||||||
|
assert len(results) == 1
|
||||||
|
assert results[0]["id"] == str(other_document.id)
|
||||||
|
|
||||||
|
|
||||||
|
def test_api_documents_list_authenticated_link_reach_public_or_authenticated():
|
||||||
|
"""
|
||||||
|
An authenticated user who has link traces to a document with public or authenticated
|
||||||
|
link reach should see it on the list view.
|
||||||
|
"""
|
||||||
|
user = factories.UserFactory()
|
||||||
|
|
||||||
|
client = APIClient()
|
||||||
|
client.force_login(user)
|
||||||
|
|
||||||
|
documents = [
|
||||||
|
factories.DocumentFactory(link_traces=[user], link_reach=reach)
|
||||||
|
for reach in models.LinkReachChoices
|
||||||
|
if reach != "restricted"
|
||||||
|
]
|
||||||
|
expected_ids = {str(document.id) for document in documents}
|
||||||
|
|
||||||
|
response = client.get(
|
||||||
|
"/api/v1.0/documents/",
|
||||||
|
)
|
||||||
|
|
||||||
|
assert response.status_code == 200
|
||||||
|
results = response.json()["results"]
|
||||||
|
assert len(results) == 2
|
||||||
|
results_id = {result["id"] for result in results}
|
||||||
|
assert expected_ids == results_id
|
||||||
|
|
||||||
|
|
||||||
@mock.patch.object(PageNumberPagination, "get_page_size", return_value=2)
|
@mock.patch.object(PageNumberPagination, "get_page_size", return_value=2)
|
||||||
def test_api_documents_list_pagination(
|
def test_api_documents_list_pagination(
|
||||||
_mock_page_size,
|
_mock_page_size,
|
||||||
@@ -152,7 +212,7 @@ def test_api_documents_list_authenticated_distinct():
|
|||||||
|
|
||||||
other_user = factories.UserFactory()
|
other_user = factories.UserFactory()
|
||||||
|
|
||||||
document = factories.DocumentFactory(users=[user, other_user], is_public=True)
|
document = factories.DocumentFactory(users=[user, other_user])
|
||||||
|
|
||||||
response = client.get(
|
response = client.get(
|
||||||
"/api/v1.0/documents/",
|
"/api/v1.0/documents/",
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ Tests for Documents API endpoint in impress's core app: retrieve
|
|||||||
import pytest
|
import pytest
|
||||||
from rest_framework.test import APIClient
|
from rest_framework.test import APIClient
|
||||||
|
|
||||||
from core import factories
|
from core import factories, models
|
||||||
from core.api import serializers
|
from core.api import serializers
|
||||||
|
|
||||||
pytestmark = pytest.mark.django_db
|
pytestmark = pytest.mark.django_db
|
||||||
@@ -13,7 +13,7 @@ pytestmark = pytest.mark.django_db
|
|||||||
|
|
||||||
def test_api_documents_retrieve_anonymous_public():
|
def test_api_documents_retrieve_anonymous_public():
|
||||||
"""Anonymous users should be allowed to retrieve public documents."""
|
"""Anonymous users should be allowed to retrieve public documents."""
|
||||||
document = factories.DocumentFactory(is_public=True)
|
document = factories.DocumentFactory(link_reach="public")
|
||||||
|
|
||||||
response = APIClient().get(f"/api/v1.0/documents/{document.id!s}/")
|
response = APIClient().get(f"/api/v1.0/documents/{document.id!s}/")
|
||||||
|
|
||||||
@@ -21,36 +21,41 @@ def test_api_documents_retrieve_anonymous_public():
|
|||||||
assert response.json() == {
|
assert response.json() == {
|
||||||
"id": str(document.id),
|
"id": str(document.id),
|
||||||
"abilities": {
|
"abilities": {
|
||||||
|
"attachment_upload": document.link_role == "editor",
|
||||||
"destroy": False,
|
"destroy": False,
|
||||||
"attachment_upload": False,
|
|
||||||
"manage_accesses": False,
|
"manage_accesses": False,
|
||||||
"partial_update": False,
|
"partial_update": document.link_role == "editor",
|
||||||
"retrieve": True,
|
"retrieve": True,
|
||||||
"update": False,
|
"update": document.link_role == "editor",
|
||||||
"versions_destroy": False,
|
"versions_destroy": False,
|
||||||
"versions_list": False,
|
"versions_list": False,
|
||||||
"versions_retrieve": False,
|
"versions_retrieve": False,
|
||||||
},
|
},
|
||||||
"accesses": [],
|
"accesses": [],
|
||||||
|
"link_reach": "public",
|
||||||
|
"link_role": document.link_role,
|
||||||
"title": document.title,
|
"title": document.title,
|
||||||
"is_public": True,
|
|
||||||
"content": document.content,
|
"content": document.content,
|
||||||
"created_at": document.created_at.isoformat().replace("+00:00", "Z"),
|
"created_at": document.created_at.isoformat().replace("+00:00", "Z"),
|
||||||
"updated_at": document.updated_at.isoformat().replace("+00:00", "Z"),
|
"updated_at": document.updated_at.isoformat().replace("+00:00", "Z"),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def test_api_documents_retrieve_anonymous_not_public():
|
@pytest.mark.parametrize("reach", ["restricted", "authenticated"])
|
||||||
|
def test_api_documents_retrieve_anonymous_restricted_or_authenticated(reach):
|
||||||
"""Anonymous users should not be able to retrieve a document that is not public."""
|
"""Anonymous users should not be able to retrieve a document that is not public."""
|
||||||
document = factories.DocumentFactory(is_public=False)
|
document = factories.DocumentFactory(link_reach=reach)
|
||||||
|
|
||||||
response = APIClient().get(f"/api/v1.0/documents/{document.id!s}/")
|
response = APIClient().get(f"/api/v1.0/documents/{document.id!s}/")
|
||||||
|
|
||||||
assert response.status_code == 404
|
assert response.status_code == 401
|
||||||
assert response.json() == {"detail": "No Document matches the given query."}
|
assert response.json() == {
|
||||||
|
"detail": "Authentication credentials were not provided."
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
def test_api_documents_retrieve_authenticated_unrelated_public():
|
@pytest.mark.parametrize("reach", ["public", "authenticated"])
|
||||||
|
def test_api_documents_retrieve_authenticated_unrelated_public_or_authenticated(reach):
|
||||||
"""
|
"""
|
||||||
Authenticated users should be able to retrieve a public document to which they are
|
Authenticated users should be able to retrieve a public document to which they are
|
||||||
not related.
|
not related.
|
||||||
@@ -60,7 +65,7 @@ def test_api_documents_retrieve_authenticated_unrelated_public():
|
|||||||
client = APIClient()
|
client = APIClient()
|
||||||
client.force_login(user)
|
client.force_login(user)
|
||||||
|
|
||||||
document = factories.DocumentFactory(is_public=True)
|
document = factories.DocumentFactory(link_reach=reach)
|
||||||
|
|
||||||
response = client.get(
|
response = client.get(
|
||||||
f"/api/v1.0/documents/{document.id!s}/",
|
f"/api/v1.0/documents/{document.id!s}/",
|
||||||
@@ -69,28 +74,61 @@ def test_api_documents_retrieve_authenticated_unrelated_public():
|
|||||||
assert response.json() == {
|
assert response.json() == {
|
||||||
"id": str(document.id),
|
"id": str(document.id),
|
||||||
"abilities": {
|
"abilities": {
|
||||||
|
"attachment_upload": document.link_role == "editor",
|
||||||
"destroy": False,
|
"destroy": False,
|
||||||
"attachment_upload": False,
|
|
||||||
"manage_accesses": False,
|
"manage_accesses": False,
|
||||||
"partial_update": False,
|
"partial_update": document.link_role == "editor",
|
||||||
"retrieve": True,
|
"retrieve": True,
|
||||||
"update": False,
|
"update": document.link_role == "editor",
|
||||||
"versions_destroy": False,
|
"versions_destroy": False,
|
||||||
"versions_list": False,
|
"versions_list": False,
|
||||||
"versions_retrieve": False,
|
"versions_retrieve": False,
|
||||||
},
|
},
|
||||||
"accesses": [],
|
"accesses": [],
|
||||||
|
"link_reach": reach,
|
||||||
|
"link_role": document.link_role,
|
||||||
"title": document.title,
|
"title": document.title,
|
||||||
"is_public": True,
|
|
||||||
"content": document.content,
|
"content": document.content,
|
||||||
"created_at": document.created_at.isoformat().replace("+00:00", "Z"),
|
"created_at": document.created_at.isoformat().replace("+00:00", "Z"),
|
||||||
"updated_at": document.updated_at.isoformat().replace("+00:00", "Z"),
|
"updated_at": document.updated_at.isoformat().replace("+00:00", "Z"),
|
||||||
}
|
}
|
||||||
|
assert (
|
||||||
|
models.LinkTrace.objects.filter(document=document, user=user).exists() is True
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def test_api_documents_retrieve_authenticated_unrelated_not_public():
|
@pytest.mark.parametrize("reach", ["public", "authenticated"])
|
||||||
|
def test_api_documents_retrieve_authenticated_trace_twice(reach):
|
||||||
"""
|
"""
|
||||||
Authenticated users should not be allowed to retrieve a document that is not public and
|
Accessing a document several times should not raise any error even though the
|
||||||
|
trace already exists for this document and user.
|
||||||
|
"""
|
||||||
|
user = factories.UserFactory()
|
||||||
|
|
||||||
|
client = APIClient()
|
||||||
|
client.force_login(user)
|
||||||
|
|
||||||
|
document = factories.DocumentFactory(link_reach=reach)
|
||||||
|
assert (
|
||||||
|
models.LinkTrace.objects.filter(document=document, user=user).exists() is False
|
||||||
|
)
|
||||||
|
|
||||||
|
client.get(
|
||||||
|
f"/api/v1.0/documents/{document.id!s}/",
|
||||||
|
)
|
||||||
|
assert (
|
||||||
|
models.LinkTrace.objects.filter(document=document, user=user).exists() is True
|
||||||
|
)
|
||||||
|
|
||||||
|
# A second visit should not raise any error
|
||||||
|
response = client.get(f"/api/v1.0/documents/{document.id!s}/")
|
||||||
|
|
||||||
|
assert response.status_code == 200
|
||||||
|
|
||||||
|
|
||||||
|
def test_api_documents_retrieve_authenticated_unrelated_restricted():
|
||||||
|
"""
|
||||||
|
Authenticated users should not be allowed to retrieve a document that is restricted and
|
||||||
to which they are not related.
|
to which they are not related.
|
||||||
"""
|
"""
|
||||||
user = factories.UserFactory()
|
user = factories.UserFactory()
|
||||||
@@ -98,7 +136,7 @@ def test_api_documents_retrieve_authenticated_unrelated_not_public():
|
|||||||
client = APIClient()
|
client = APIClient()
|
||||||
client.force_login(user)
|
client.force_login(user)
|
||||||
|
|
||||||
document = factories.DocumentFactory(is_public=False)
|
document = factories.DocumentFactory(link_reach="restricted")
|
||||||
|
|
||||||
response = client.get(
|
response = client.get(
|
||||||
f"/api/v1.0/documents/{document.id!s}/",
|
f"/api/v1.0/documents/{document.id!s}/",
|
||||||
@@ -154,7 +192,8 @@ def test_api_documents_retrieve_authenticated_related_direct():
|
|||||||
"title": document.title,
|
"title": document.title,
|
||||||
"content": document.content,
|
"content": document.content,
|
||||||
"abilities": document.get_abilities(user),
|
"abilities": document.get_abilities(user),
|
||||||
"is_public": document.is_public,
|
"link_reach": document.link_reach,
|
||||||
|
"link_role": document.link_role,
|
||||||
"created_at": document.created_at.isoformat().replace("+00:00", "Z"),
|
"created_at": document.created_at.isoformat().replace("+00:00", "Z"),
|
||||||
"updated_at": document.updated_at.isoformat().replace("+00:00", "Z"),
|
"updated_at": document.updated_at.isoformat().replace("+00:00", "Z"),
|
||||||
}
|
}
|
||||||
@@ -162,8 +201,8 @@ def test_api_documents_retrieve_authenticated_related_direct():
|
|||||||
|
|
||||||
def test_api_documents_retrieve_authenticated_related_team_none(mock_user_teams):
|
def test_api_documents_retrieve_authenticated_related_team_none(mock_user_teams):
|
||||||
"""
|
"""
|
||||||
Authenticated users should not be able to retrieve a document related to teams in
|
Authenticated users should not be able to retrieve a restricted document related to
|
||||||
which the user is not.
|
teams in which the user is not.
|
||||||
"""
|
"""
|
||||||
mock_user_teams.return_value = []
|
mock_user_teams.return_value = []
|
||||||
|
|
||||||
@@ -172,7 +211,7 @@ def test_api_documents_retrieve_authenticated_related_team_none(mock_user_teams)
|
|||||||
client = APIClient()
|
client = APIClient()
|
||||||
client.force_login(user)
|
client.force_login(user)
|
||||||
|
|
||||||
document = factories.DocumentFactory(is_public=False)
|
document = factories.DocumentFactory(link_reach="restricted")
|
||||||
|
|
||||||
factories.TeamDocumentAccessFactory(
|
factories.TeamDocumentAccessFactory(
|
||||||
document=document, team="readers", role="reader"
|
document=document, team="readers", role="reader"
|
||||||
@@ -217,7 +256,7 @@ def test_api_documents_retrieve_authenticated_related_team_members(
|
|||||||
client = APIClient()
|
client = APIClient()
|
||||||
client.force_login(user)
|
client.force_login(user)
|
||||||
|
|
||||||
document = factories.DocumentFactory(is_public=False)
|
document = factories.DocumentFactory(link_reach="restricted")
|
||||||
|
|
||||||
access_reader = factories.TeamDocumentAccessFactory(
|
access_reader = factories.TeamDocumentAccessFactory(
|
||||||
document=document, team="readers", role="reader"
|
document=document, team="readers", role="reader"
|
||||||
@@ -291,7 +330,8 @@ def test_api_documents_retrieve_authenticated_related_team_members(
|
|||||||
"title": document.title,
|
"title": document.title,
|
||||||
"content": document.content,
|
"content": document.content,
|
||||||
"abilities": document.get_abilities(user),
|
"abilities": document.get_abilities(user),
|
||||||
"is_public": False,
|
"link_reach": "restricted",
|
||||||
|
"link_role": document.link_role,
|
||||||
"created_at": document.created_at.isoformat().replace("+00:00", "Z"),
|
"created_at": document.created_at.isoformat().replace("+00:00", "Z"),
|
||||||
"updated_at": document.updated_at.isoformat().replace("+00:00", "Z"),
|
"updated_at": document.updated_at.isoformat().replace("+00:00", "Z"),
|
||||||
}
|
}
|
||||||
@@ -319,7 +359,7 @@ def test_api_documents_retrieve_authenticated_related_team_administrators(
|
|||||||
client = APIClient()
|
client = APIClient()
|
||||||
client.force_login(user)
|
client.force_login(user)
|
||||||
|
|
||||||
document = factories.DocumentFactory(is_public=False)
|
document = factories.DocumentFactory(link_reach="restricted")
|
||||||
|
|
||||||
access_reader = factories.TeamDocumentAccessFactory(
|
access_reader = factories.TeamDocumentAccessFactory(
|
||||||
document=document, team="readers", role="reader"
|
document=document, team="readers", role="reader"
|
||||||
@@ -410,7 +450,8 @@ def test_api_documents_retrieve_authenticated_related_team_administrators(
|
|||||||
"title": document.title,
|
"title": document.title,
|
||||||
"content": document.content,
|
"content": document.content,
|
||||||
"abilities": document.get_abilities(user),
|
"abilities": document.get_abilities(user),
|
||||||
"is_public": False,
|
"link_reach": "restricted",
|
||||||
|
"link_role": document.link_role,
|
||||||
"created_at": document.created_at.isoformat().replace("+00:00", "Z"),
|
"created_at": document.created_at.isoformat().replace("+00:00", "Z"),
|
||||||
"updated_at": document.updated_at.isoformat().replace("+00:00", "Z"),
|
"updated_at": document.updated_at.isoformat().replace("+00:00", "Z"),
|
||||||
}
|
}
|
||||||
@@ -429,8 +470,8 @@ def test_api_documents_retrieve_authenticated_related_team_owners(
|
|||||||
teams, mock_user_teams
|
teams, mock_user_teams
|
||||||
):
|
):
|
||||||
"""
|
"""
|
||||||
Authenticated users should be allowed to retrieve a document to which they
|
Authenticated users should be allowed to retrieve a restricted document to which
|
||||||
are related via a team whatever the role and see all its accesses.
|
they are related via a team whatever the role and see all its accesses.
|
||||||
"""
|
"""
|
||||||
mock_user_teams.return_value = teams
|
mock_user_teams.return_value = teams
|
||||||
|
|
||||||
@@ -439,7 +480,7 @@ def test_api_documents_retrieve_authenticated_related_team_owners(
|
|||||||
client = APIClient()
|
client = APIClient()
|
||||||
client.force_login(user)
|
client.force_login(user)
|
||||||
|
|
||||||
document = factories.DocumentFactory(is_public=False)
|
document = factories.DocumentFactory(link_reach="restricted")
|
||||||
|
|
||||||
access_reader = factories.TeamDocumentAccessFactory(
|
access_reader = factories.TeamDocumentAccessFactory(
|
||||||
document=document, team="readers", role="reader"
|
document=document, team="readers", role="reader"
|
||||||
@@ -533,7 +574,8 @@ def test_api_documents_retrieve_authenticated_related_team_owners(
|
|||||||
"title": document.title,
|
"title": document.title,
|
||||||
"content": document.content,
|
"content": document.content,
|
||||||
"abilities": document.get_abilities(user),
|
"abilities": document.get_abilities(user),
|
||||||
"is_public": False,
|
"link_reach": "restricted",
|
||||||
|
"link_role": document.link_role,
|
||||||
"created_at": document.created_at.isoformat().replace("+00:00", "Z"),
|
"created_at": document.created_at.isoformat().replace("+00:00", "Z"),
|
||||||
"updated_at": document.updated_at.isoformat().replace("+00:00", "Z"),
|
"updated_at": document.updated_at.isoformat().replace("+00:00", "Z"),
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ pytestmark = pytest.mark.django_db
|
|||||||
|
|
||||||
def test_api_documents_retrieve_auth_anonymous_public():
|
def test_api_documents_retrieve_auth_anonymous_public():
|
||||||
"""Anonymous users should be able to retrieve attachments linked to a public document"""
|
"""Anonymous users should be able to retrieve attachments linked to a public document"""
|
||||||
document = factories.DocumentFactory(is_public=True)
|
document = factories.DocumentFactory(link_reach="public")
|
||||||
|
|
||||||
filename = f"{uuid.uuid4()!s}.jpg"
|
filename = f"{uuid.uuid4()!s}.jpg"
|
||||||
key = f"{document.pk!s}/attachments/{filename:s}"
|
key = f"{document.pk!s}/attachments/{filename:s}"
|
||||||
@@ -64,12 +64,13 @@ def test_api_documents_retrieve_auth_anonymous_public():
|
|||||||
assert response.content.decode("utf-8") == "my prose"
|
assert response.content.decode("utf-8") == "my prose"
|
||||||
|
|
||||||
|
|
||||||
def test_api_documents_retrieve_auth_anonymous_not_public():
|
@pytest.mark.parametrize("reach", ["authenticated", "restricted"])
|
||||||
|
def test_api_documents_retrieve_auth_anonymous_authenticated_or_restricted(reach):
|
||||||
"""
|
"""
|
||||||
Anonymous users should not be allowed to retrieve attachments linked to a document
|
Anonymous users should not be allowed to retrieve attachments linked to a document
|
||||||
that is not public.
|
with link reach set to authenticated or restricted.
|
||||||
"""
|
"""
|
||||||
document = factories.DocumentFactory(is_public=False)
|
document = factories.DocumentFactory(link_reach=reach)
|
||||||
|
|
||||||
filename = f"{uuid.uuid4()!s}.jpg"
|
filename = f"{uuid.uuid4()!s}.jpg"
|
||||||
media_url = f"http://localhost/media/{document.pk!s}/attachments/{filename:s}"
|
media_url = f"http://localhost/media/{document.pk!s}/attachments/{filename:s}"
|
||||||
@@ -82,12 +83,13 @@ def test_api_documents_retrieve_auth_anonymous_not_public():
|
|||||||
assert "Authorization" not in response
|
assert "Authorization" not in response
|
||||||
|
|
||||||
|
|
||||||
def test_api_documents_retrieve_auth_authenticated_public():
|
@pytest.mark.parametrize("reach", ["public", "authenticated"])
|
||||||
|
def test_api_documents_retrieve_auth_authenticated_public_or_authenticated(reach):
|
||||||
"""
|
"""
|
||||||
Authenticated users who are not related to a document should be able to
|
Authenticated users who are not related to a document should be able to retrieve
|
||||||
retrieve attachments linked to a public document.
|
attachments related to a document with public or authenticated link reach.
|
||||||
"""
|
"""
|
||||||
document = factories.DocumentFactory(is_public=True)
|
document = factories.DocumentFactory(link_reach=reach)
|
||||||
|
|
||||||
user = factories.UserFactory()
|
user = factories.UserFactory()
|
||||||
client = APIClient()
|
client = APIClient()
|
||||||
@@ -104,7 +106,7 @@ def test_api_documents_retrieve_auth_authenticated_public():
|
|||||||
)
|
)
|
||||||
|
|
||||||
original_url = f"http://localhost/media/{key:s}"
|
original_url = f"http://localhost/media/{key:s}"
|
||||||
response = APIClient().get(
|
response = client.get(
|
||||||
"/api/v1.0/documents/retrieve-auth/", HTTP_X_ORIGINAL_URL=original_url
|
"/api/v1.0/documents/retrieve-auth/", HTTP_X_ORIGINAL_URL=original_url
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -133,12 +135,12 @@ def test_api_documents_retrieve_auth_authenticated_public():
|
|||||||
assert response.content.decode("utf-8") == "my prose"
|
assert response.content.decode("utf-8") == "my prose"
|
||||||
|
|
||||||
|
|
||||||
def test_api_documents_retrieve_auth_authenticated_not_public():
|
def test_api_documents_retrieve_auth_authenticated_restricted():
|
||||||
"""
|
"""
|
||||||
Authenticated users who are not related to a document should not be allowed to
|
Authenticated users who are not related to a document should not be allowed to
|
||||||
retrieve attachments linked to a document that is not public.
|
retrieve attachments linked to a document that is restricted.
|
||||||
"""
|
"""
|
||||||
document = factories.DocumentFactory(is_public=False)
|
document = factories.DocumentFactory(link_reach="restricted")
|
||||||
|
|
||||||
user = factories.UserFactory()
|
user = factories.UserFactory()
|
||||||
client = APIClient()
|
client = APIClient()
|
||||||
@@ -147,7 +149,7 @@ def test_api_documents_retrieve_auth_authenticated_not_public():
|
|||||||
filename = f"{uuid.uuid4()!s}.jpg"
|
filename = f"{uuid.uuid4()!s}.jpg"
|
||||||
media_url = f"http://localhost/media/{document.pk!s}/attachments/{filename:s}"
|
media_url = f"http://localhost/media/{document.pk!s}/attachments/{filename:s}"
|
||||||
|
|
||||||
response = APIClient().get(
|
response = client.get(
|
||||||
"/api/v1.0/documents/retrieve-auth/", HTTP_X_ORIGINAL_URL=media_url
|
"/api/v1.0/documents/retrieve-auth/", HTTP_X_ORIGINAL_URL=media_url
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -155,18 +157,17 @@ def test_api_documents_retrieve_auth_authenticated_not_public():
|
|||||||
assert "Authorization" not in response
|
assert "Authorization" not in response
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("is_public", [True, False])
|
|
||||||
@pytest.mark.parametrize("via", VIA)
|
@pytest.mark.parametrize("via", VIA)
|
||||||
def test_api_documents_retrieve_auth_related(via, is_public, mock_user_teams):
|
def test_api_documents_retrieve_auth_related(via, mock_user_teams):
|
||||||
"""
|
"""
|
||||||
Users who have a role on a document, whatever the role, should be able to
|
Users who have a specific access to a document, whatever the role, should be able to
|
||||||
retrieve related attachments.
|
retrieve related attachments.
|
||||||
"""
|
"""
|
||||||
user = factories.UserFactory()
|
user = factories.UserFactory()
|
||||||
client = APIClient()
|
client = APIClient()
|
||||||
client.force_login(user)
|
client.force_login(user)
|
||||||
|
|
||||||
document = factories.DocumentFactory(is_public=is_public)
|
document = factories.DocumentFactory()
|
||||||
if via == USER:
|
if via == USER:
|
||||||
factories.UserDocumentAccessFactory(document=document, user=user)
|
factories.UserDocumentAccessFactory(document=document, user=user)
|
||||||
elif via == TEAM:
|
elif via == TEAM:
|
||||||
|
|||||||
@@ -4,6 +4,8 @@ Tests for Documents API endpoint in impress's core app: update
|
|||||||
|
|
||||||
import random
|
import random
|
||||||
|
|
||||||
|
from django.contrib.auth.models import AnonymousUser
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
from rest_framework.test import APIClient
|
from rest_framework.test import APIClient
|
||||||
|
|
||||||
@@ -14,9 +16,22 @@ from core.tests.conftest import TEAM, USER, VIA
|
|||||||
pytestmark = pytest.mark.django_db
|
pytestmark = pytest.mark.django_db
|
||||||
|
|
||||||
|
|
||||||
def test_api_documents_update_anonymous():
|
@pytest.mark.parametrize(
|
||||||
"""Anonymous users should not be allowed to update a document."""
|
"reach, role",
|
||||||
document = factories.DocumentFactory()
|
[
|
||||||
|
("restricted", "reader"),
|
||||||
|
("restricted", "editor"),
|
||||||
|
("authenticated", "reader"),
|
||||||
|
("authenticated", "editor"),
|
||||||
|
("public", "reader"),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_api_documents_update_anonymous_forbidden(reach, role):
|
||||||
|
"""
|
||||||
|
Anonymous users should not be allowed to update a document when link
|
||||||
|
configuration does not allow it.
|
||||||
|
"""
|
||||||
|
document = factories.DocumentFactory(link_reach=reach, link_role=role)
|
||||||
old_document_values = serializers.DocumentSerializer(instance=document).data
|
old_document_values = serializers.DocumentSerializer(instance=document).data
|
||||||
|
|
||||||
new_document_values = serializers.DocumentSerializer(
|
new_document_values = serializers.DocumentSerializer(
|
||||||
@@ -37,16 +52,26 @@ def test_api_documents_update_anonymous():
|
|||||||
assert document_values == old_document_values
|
assert document_values == old_document_values
|
||||||
|
|
||||||
|
|
||||||
def test_api_documents_update_authenticated_unrelated():
|
@pytest.mark.parametrize(
|
||||||
|
"reach,role",
|
||||||
|
[
|
||||||
|
("public", "reader"),
|
||||||
|
("authenticated", "reader"),
|
||||||
|
("restricted", "reader"),
|
||||||
|
("restricted", "editor"),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_api_documents_update_authenticated_unrelated_forbidden(reach, role):
|
||||||
"""
|
"""
|
||||||
Authenticated users should not be allowed to update a document to which they are not related.
|
Authenticated users should not be allowed to update a document to which
|
||||||
|
they are not related if the link configuration does not allow it.
|
||||||
"""
|
"""
|
||||||
user = factories.UserFactory()
|
user = factories.UserFactory()
|
||||||
|
|
||||||
client = APIClient()
|
client = APIClient()
|
||||||
client.force_login(user)
|
client.force_login(user)
|
||||||
|
|
||||||
document = factories.DocumentFactory(is_public=False)
|
document = factories.DocumentFactory(link_reach=reach, link_role=role)
|
||||||
old_document_values = serializers.DocumentSerializer(instance=document).data
|
old_document_values = serializers.DocumentSerializer(instance=document).data
|
||||||
|
|
||||||
new_document_values = serializers.DocumentSerializer(
|
new_document_values = serializers.DocumentSerializer(
|
||||||
@@ -68,10 +93,57 @@ def test_api_documents_update_authenticated_unrelated():
|
|||||||
assert document_values == old_document_values
|
assert document_values == old_document_values
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"is_authenticated,reach,role",
|
||||||
|
[
|
||||||
|
(False, "public", "editor"),
|
||||||
|
(True, "public", "editor"),
|
||||||
|
(True, "authenticated", "editor"),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_api_documents_update_anonymous_or_authenticated_unrelated(
|
||||||
|
is_authenticated, reach, role
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Authenticated users should be able to update a document to which
|
||||||
|
they are not related if the link configuration allows it.
|
||||||
|
"""
|
||||||
|
client = APIClient()
|
||||||
|
|
||||||
|
if is_authenticated:
|
||||||
|
user = factories.UserFactory()
|
||||||
|
client.force_login(user)
|
||||||
|
else:
|
||||||
|
user = AnonymousUser()
|
||||||
|
|
||||||
|
document = factories.DocumentFactory(link_reach=reach, link_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 = models.Document.objects.get(pk=document.pk)
|
||||||
|
document_values = serializers.DocumentSerializer(instance=document).data
|
||||||
|
for key, value in document_values.items():
|
||||||
|
if key in ["id", "accesses", "created_at", "link_reach", "link_role"]:
|
||||||
|
assert value == old_document_values[key]
|
||||||
|
elif key == "updated_at":
|
||||||
|
assert value > old_document_values[key]
|
||||||
|
else:
|
||||||
|
assert value == new_document_values[key]
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("via", VIA)
|
@pytest.mark.parametrize("via", VIA)
|
||||||
def test_api_documents_update_authenticated_reader(via, mock_user_teams):
|
def test_api_documents_update_authenticated_reader(via, mock_user_teams):
|
||||||
"""
|
"""
|
||||||
Users who are editors or reader of a document but not administrators should
|
Users who are reader of a document but not administrators should
|
||||||
not be allowed to update it.
|
not be allowed to update it.
|
||||||
"""
|
"""
|
||||||
user = factories.UserFactory()
|
user = factories.UserFactory()
|
||||||
@@ -79,7 +151,7 @@ def test_api_documents_update_authenticated_reader(via, mock_user_teams):
|
|||||||
client = APIClient()
|
client = APIClient()
|
||||||
client.force_login(user)
|
client.force_login(user)
|
||||||
|
|
||||||
document = factories.DocumentFactory()
|
document = factories.DocumentFactory(link_role="reader")
|
||||||
if via == USER:
|
if via == USER:
|
||||||
factories.UserDocumentAccessFactory(document=document, user=user, role="reader")
|
factories.UserDocumentAccessFactory(document=document, user=user, role="reader")
|
||||||
elif via == TEAM:
|
elif via == TEAM:
|
||||||
@@ -144,7 +216,7 @@ def test_api_documents_update_authenticated_editor_administrator_or_owner(
|
|||||||
document = models.Document.objects.get(pk=document.pk)
|
document = models.Document.objects.get(pk=document.pk)
|
||||||
document_values = serializers.DocumentSerializer(instance=document).data
|
document_values = serializers.DocumentSerializer(instance=document).data
|
||||||
for key, value in document_values.items():
|
for key, value in document_values.items():
|
||||||
if key in ["id", "accesses", "created_at"]:
|
if key in ["id", "accesses", "created_at", "link_reach", "link_role"]:
|
||||||
assert value == old_document_values[key]
|
assert value == old_document_values[key]
|
||||||
elif key == "updated_at":
|
elif key == "updated_at":
|
||||||
assert value > old_document_values[key]
|
assert value > old_document_values[key]
|
||||||
@@ -183,7 +255,7 @@ def test_api_documents_update_authenticated_owners(via, mock_user_teams):
|
|||||||
document = models.Document.objects.get(pk=document.pk)
|
document = models.Document.objects.get(pk=document.pk)
|
||||||
document_values = serializers.DocumentSerializer(instance=document).data
|
document_values = serializers.DocumentSerializer(instance=document).data
|
||||||
for key, value in document_values.items():
|
for key, value in document_values.items():
|
||||||
if key in ["id", "accesses", "created_at"]:
|
if key in ["id", "accesses", "created_at", "link_reach", "link_role"]:
|
||||||
assert value == old_document_values[key]
|
assert value == old_document_values[key]
|
||||||
elif key == "updated_at":
|
elif key == "updated_at":
|
||||||
assert value > old_document_values[key]
|
assert value > old_document_values[key]
|
||||||
@@ -215,21 +287,20 @@ def test_api_documents_update_administrator_or_owner_of_another(via, mock_user_t
|
|||||||
role=random.choice(["administrator", "owner"]),
|
role=random.choice(["administrator", "owner"]),
|
||||||
)
|
)
|
||||||
|
|
||||||
is_public = random.choice([True, False])
|
other_document = factories.DocumentFactory(title="Old title", link_role="reader")
|
||||||
document = factories.DocumentFactory(title="Old title", is_public=is_public)
|
old_document_values = serializers.DocumentSerializer(instance=other_document).data
|
||||||
old_document_values = serializers.DocumentSerializer(instance=document).data
|
|
||||||
|
|
||||||
new_document_values = serializers.DocumentSerializer(
|
new_document_values = serializers.DocumentSerializer(
|
||||||
instance=factories.DocumentFactory()
|
instance=factories.DocumentFactory()
|
||||||
).data
|
).data
|
||||||
response = client.put(
|
response = client.put(
|
||||||
f"/api/v1.0/documents/{document.id!s}/",
|
f"/api/v1.0/documents/{other_document.id!s}/",
|
||||||
new_document_values,
|
new_document_values,
|
||||||
format="json",
|
format="json",
|
||||||
)
|
)
|
||||||
|
|
||||||
assert response.status_code == 403 if is_public else 404
|
assert response.status_code == 403
|
||||||
|
|
||||||
document.refresh_from_db()
|
other_document.refresh_from_db()
|
||||||
document_values = serializers.DocumentSerializer(instance=document).data
|
other_document_values = serializers.DocumentSerializer(instance=other_document).data
|
||||||
assert document_values == old_document_values
|
assert other_document_values == old_document_values
|
||||||
|
|||||||
@@ -44,8 +44,10 @@ def test_api_templates_generate_document_anonymous_not_public():
|
|||||||
format="json",
|
format="json",
|
||||||
)
|
)
|
||||||
|
|
||||||
assert response.status_code == 404
|
assert response.status_code == 401
|
||||||
assert response.json() == {"detail": "No Template matches the given query."}
|
assert response.json() == {
|
||||||
|
"detail": "Authentication credentials were not provided."
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
def test_api_templates_generate_document_authenticated_public():
|
def test_api_templates_generate_document_authenticated_public():
|
||||||
|
|||||||
@@ -14,21 +14,24 @@ pytestmark = pytest.mark.django_db
|
|||||||
|
|
||||||
|
|
||||||
def test_api_templates_list_anonymous():
|
def test_api_templates_list_anonymous():
|
||||||
"""Anonymous users should not be able to list templates, public or not."""
|
"""Anonymous users should only be able to list public templates."""
|
||||||
factories.TemplateFactory.create_batch(2, is_public=False)
|
factories.TemplateFactory.create_batch(2, is_public=False)
|
||||||
factories.TemplateFactory.create_batch(2, is_public=True)
|
public_templates = factories.TemplateFactory.create_batch(2, is_public=True)
|
||||||
|
expected_ids = {str(template.id) for template in public_templates}
|
||||||
|
|
||||||
response = APIClient().get("/api/v1.0/templates/")
|
response = APIClient().get("/api/v1.0/templates/")
|
||||||
|
|
||||||
assert response.status_code == 200
|
assert response.status_code == 200
|
||||||
results = response.json()["results"]
|
results = response.json()["results"]
|
||||||
assert len(results) == 0
|
assert len(results) == 2
|
||||||
|
results_id = {result["id"] for result in results}
|
||||||
|
assert expected_ids == results_id
|
||||||
|
|
||||||
|
|
||||||
def test_api_templates_list_authenticated_direct():
|
def test_api_templates_list_authenticated_direct():
|
||||||
"""
|
"""
|
||||||
Authenticated users should be able to list templates they are a direct
|
Authenticated users should be able to list templates they are a direct
|
||||||
owner/administrator/member of.
|
owner/administrator/member of or that are public.
|
||||||
"""
|
"""
|
||||||
user = factories.UserFactory()
|
user = factories.UserFactory()
|
||||||
|
|
||||||
@@ -39,10 +42,12 @@ def test_api_templates_list_authenticated_direct():
|
|||||||
access.template
|
access.template
|
||||||
for access in factories.UserTemplateAccessFactory.create_batch(5, user=user)
|
for access in factories.UserTemplateAccessFactory.create_batch(5, user=user)
|
||||||
]
|
]
|
||||||
factories.TemplateFactory.create_batch(2, is_public=True)
|
public_templates = factories.TemplateFactory.create_batch(2, is_public=True)
|
||||||
factories.TemplateFactory.create_batch(2, is_public=False)
|
factories.TemplateFactory.create_batch(2, is_public=False)
|
||||||
|
|
||||||
expected_ids = {str(template.id) for template in related_templates}
|
expected_ids = {
|
||||||
|
str(template.id) for template in related_templates + public_templates
|
||||||
|
}
|
||||||
|
|
||||||
response = client.get(
|
response = client.get(
|
||||||
"/api/v1.0/templates/",
|
"/api/v1.0/templates/",
|
||||||
@@ -50,7 +55,7 @@ def test_api_templates_list_authenticated_direct():
|
|||||||
|
|
||||||
assert response.status_code == 200
|
assert response.status_code == 200
|
||||||
results = response.json()["results"]
|
results = response.json()["results"]
|
||||||
assert len(results) == 5
|
assert len(results) == 7
|
||||||
results_id = {result["id"] for result in results}
|
results_id = {result["id"] for result in results}
|
||||||
assert expected_ids == results_id
|
assert expected_ids == results_id
|
||||||
|
|
||||||
@@ -58,7 +63,7 @@ def test_api_templates_list_authenticated_direct():
|
|||||||
def test_api_templates_list_authenticated_via_team(mock_user_teams):
|
def test_api_templates_list_authenticated_via_team(mock_user_teams):
|
||||||
"""
|
"""
|
||||||
Authenticated users should be able to list templates they are a
|
Authenticated users should be able to list templates they are a
|
||||||
owner/administrator/member of via a team.
|
owner/administrator/member of via a team or that are public.
|
||||||
"""
|
"""
|
||||||
user = factories.UserFactory()
|
user = factories.UserFactory()
|
||||||
|
|
||||||
@@ -75,16 +80,19 @@ def test_api_templates_list_authenticated_via_team(mock_user_teams):
|
|||||||
access.template
|
access.template
|
||||||
for access in factories.TeamTemplateAccessFactory.create_batch(3, team="team2")
|
for access in factories.TeamTemplateAccessFactory.create_batch(3, team="team2")
|
||||||
]
|
]
|
||||||
factories.TemplateFactory.create_batch(2, is_public=True)
|
public_templates = factories.TemplateFactory.create_batch(2, is_public=True)
|
||||||
factories.TemplateFactory.create_batch(2, is_public=False)
|
factories.TemplateFactory.create_batch(2, is_public=False)
|
||||||
|
|
||||||
expected_ids = {str(template.id) for template in templates_team1 + templates_team2}
|
expected_ids = {
|
||||||
|
str(template.id)
|
||||||
|
for template in templates_team1 + templates_team2 + public_templates
|
||||||
|
}
|
||||||
|
|
||||||
response = client.get("/api/v1.0/templates/")
|
response = client.get("/api/v1.0/templates/")
|
||||||
|
|
||||||
assert response.status_code == 200
|
assert response.status_code == 200
|
||||||
results = response.json()["results"]
|
results = response.json()["results"]
|
||||||
assert len(results) == 5
|
assert len(results) == 7
|
||||||
results_id = {result["id"] for result in results}
|
results_id = {result["id"] for result in results}
|
||||||
assert expected_ids == results_id
|
assert expected_ids == results_id
|
||||||
|
|
||||||
|
|||||||
@@ -41,8 +41,10 @@ def test_api_templates_retrieve_anonymous_not_public():
|
|||||||
|
|
||||||
response = APIClient().get(f"/api/v1.0/templates/{template.id!s}/")
|
response = APIClient().get(f"/api/v1.0/templates/{template.id!s}/")
|
||||||
|
|
||||||
assert response.status_code == 404
|
assert response.status_code == 401
|
||||||
assert response.json() == {"detail": "No Template matches the given query."}
|
assert response.json() == {
|
||||||
|
"detail": "Authentication credentials were not provided."
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
def test_api_templates_retrieve_authenticated_unrelated_public():
|
def test_api_templates_retrieve_authenticated_unrelated_public():
|
||||||
|
|||||||
@@ -57,30 +57,28 @@ def test_models_documents_file_key():
|
|||||||
# get_abilities
|
# get_abilities
|
||||||
|
|
||||||
|
|
||||||
def test_models_documents_get_abilities_anonymous_public():
|
@pytest.mark.parametrize(
|
||||||
"""Check abilities returned for an anonymous user if the document is public."""
|
"is_authenticated,reach,role",
|
||||||
document = factories.DocumentFactory(is_public=True)
|
[
|
||||||
abilities = document.get_abilities(AnonymousUser())
|
(True, "restricted", "reader"),
|
||||||
|
(True, "restricted", "editor"),
|
||||||
|
(False, "restricted", "reader"),
|
||||||
|
(False, "restricted", "editor"),
|
||||||
|
(False, "authenticated", "reader"),
|
||||||
|
(False, "authenticated", "editor"),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_models_documents_get_abilities_forbidden(is_authenticated, reach, role):
|
||||||
|
"""
|
||||||
|
Check abilities returned for a document giving insufficient roles to link holders
|
||||||
|
i.e anonymous users or authenticated users who have no specific role on the document.
|
||||||
|
"""
|
||||||
|
document = factories.DocumentFactory(link_reach=reach, link_role=role)
|
||||||
|
user = factories.UserFactory() if is_authenticated else AnonymousUser()
|
||||||
|
abilities = document.get_abilities(user)
|
||||||
assert abilities == {
|
assert abilities == {
|
||||||
"destroy": False,
|
|
||||||
"attachment_upload": False,
|
"attachment_upload": False,
|
||||||
"manage_accesses": False,
|
|
||||||
"partial_update": False,
|
|
||||||
"retrieve": True,
|
|
||||||
"update": False,
|
|
||||||
"versions_destroy": False,
|
|
||||||
"versions_list": False,
|
|
||||||
"versions_retrieve": 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,
|
"destroy": False,
|
||||||
"attachment_upload": False,
|
|
||||||
"manage_accesses": False,
|
"manage_accesses": False,
|
||||||
"partial_update": False,
|
"partial_update": False,
|
||||||
"retrieve": False,
|
"retrieve": False,
|
||||||
@@ -91,13 +89,25 @@ def test_models_documents_get_abilities_anonymous_not_public():
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def test_models_documents_get_abilities_authenticated_unrelated_public():
|
@pytest.mark.parametrize(
|
||||||
"""Check abilities returned for an authenticated user if the user is public."""
|
"is_authenticated,reach",
|
||||||
document = factories.DocumentFactory(is_public=True)
|
[
|
||||||
abilities = document.get_abilities(factories.UserFactory())
|
(True, "public"),
|
||||||
|
(False, "public"),
|
||||||
|
(True, "authenticated"),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_models_documents_get_abilities_reader(is_authenticated, reach):
|
||||||
|
"""
|
||||||
|
Check abilities returned for a document giving reader role to link holders
|
||||||
|
i.e anonymous users or authenticated users who have no specific role on the document.
|
||||||
|
"""
|
||||||
|
document = factories.DocumentFactory(link_reach=reach, link_role="reader")
|
||||||
|
user = factories.UserFactory() if is_authenticated else AnonymousUser()
|
||||||
|
abilities = document.get_abilities(user)
|
||||||
assert abilities == {
|
assert abilities == {
|
||||||
"destroy": False,
|
|
||||||
"attachment_upload": False,
|
"attachment_upload": False,
|
||||||
|
"destroy": False,
|
||||||
"manage_accesses": False,
|
"manage_accesses": False,
|
||||||
"partial_update": False,
|
"partial_update": False,
|
||||||
"retrieve": True,
|
"retrieve": True,
|
||||||
@@ -108,17 +118,29 @@ def test_models_documents_get_abilities_authenticated_unrelated_public():
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def test_models_documents_get_abilities_authenticated_unrelated_not_public():
|
@pytest.mark.parametrize(
|
||||||
"""Check abilities returned for an authenticated user if the document is private."""
|
"is_authenticated,reach",
|
||||||
document = factories.DocumentFactory(is_public=False)
|
[
|
||||||
abilities = document.get_abilities(factories.UserFactory())
|
(True, "public"),
|
||||||
|
(False, "public"),
|
||||||
|
(True, "authenticated"),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_models_documents_get_abilities_editor(is_authenticated, reach):
|
||||||
|
"""
|
||||||
|
Check abilities returned for a document giving editor role to link holders
|
||||||
|
i.e anonymous users or authenticated users who have no specific role on the document.
|
||||||
|
"""
|
||||||
|
document = factories.DocumentFactory(link_reach=reach, link_role="editor")
|
||||||
|
user = factories.UserFactory() if is_authenticated else AnonymousUser()
|
||||||
|
abilities = document.get_abilities(user)
|
||||||
assert abilities == {
|
assert abilities == {
|
||||||
|
"attachment_upload": True,
|
||||||
"destroy": False,
|
"destroy": False,
|
||||||
"attachment_upload": False,
|
|
||||||
"manage_accesses": False,
|
"manage_accesses": False,
|
||||||
"partial_update": False,
|
"partial_update": True,
|
||||||
"retrieve": False,
|
"retrieve": True,
|
||||||
"update": False,
|
"update": True,
|
||||||
"versions_destroy": False,
|
"versions_destroy": False,
|
||||||
"versions_list": False,
|
"versions_list": False,
|
||||||
"versions_retrieve": False,
|
"versions_retrieve": False,
|
||||||
@@ -131,8 +153,8 @@ def test_models_documents_get_abilities_owner():
|
|||||||
access = factories.UserDocumentAccessFactory(role="owner", user=user)
|
access = factories.UserDocumentAccessFactory(role="owner", user=user)
|
||||||
abilities = access.document.get_abilities(access.user)
|
abilities = access.document.get_abilities(access.user)
|
||||||
assert abilities == {
|
assert abilities == {
|
||||||
"destroy": True,
|
|
||||||
"attachment_upload": True,
|
"attachment_upload": True,
|
||||||
|
"destroy": True,
|
||||||
"manage_accesses": True,
|
"manage_accesses": True,
|
||||||
"partial_update": True,
|
"partial_update": True,
|
||||||
"retrieve": True,
|
"retrieve": True,
|
||||||
@@ -148,8 +170,8 @@ def test_models_documents_get_abilities_administrator():
|
|||||||
access = factories.UserDocumentAccessFactory(role="administrator")
|
access = factories.UserDocumentAccessFactory(role="administrator")
|
||||||
abilities = access.document.get_abilities(access.user)
|
abilities = access.document.get_abilities(access.user)
|
||||||
assert abilities == {
|
assert abilities == {
|
||||||
"destroy": False,
|
|
||||||
"attachment_upload": True,
|
"attachment_upload": True,
|
||||||
|
"destroy": False,
|
||||||
"manage_accesses": True,
|
"manage_accesses": True,
|
||||||
"partial_update": True,
|
"partial_update": True,
|
||||||
"retrieve": True,
|
"retrieve": True,
|
||||||
@@ -168,8 +190,8 @@ def test_models_documents_get_abilities_editor_user(django_assert_num_queries):
|
|||||||
abilities = access.document.get_abilities(access.user)
|
abilities = access.document.get_abilities(access.user)
|
||||||
|
|
||||||
assert abilities == {
|
assert abilities == {
|
||||||
"destroy": False,
|
|
||||||
"attachment_upload": True,
|
"attachment_upload": True,
|
||||||
|
"destroy": False,
|
||||||
"manage_accesses": False,
|
"manage_accesses": False,
|
||||||
"partial_update": True,
|
"partial_update": True,
|
||||||
"retrieve": True,
|
"retrieve": True,
|
||||||
@@ -182,14 +204,16 @@ def test_models_documents_get_abilities_editor_user(django_assert_num_queries):
|
|||||||
|
|
||||||
def test_models_documents_get_abilities_reader_user(django_assert_num_queries):
|
def test_models_documents_get_abilities_reader_user(django_assert_num_queries):
|
||||||
"""Check abilities returned for the reader of a document."""
|
"""Check abilities returned for the reader of a document."""
|
||||||
access = factories.UserDocumentAccessFactory(role="reader")
|
access = factories.UserDocumentAccessFactory(
|
||||||
|
role="reader", document__link_role="reader"
|
||||||
|
)
|
||||||
|
|
||||||
with django_assert_num_queries(1):
|
with django_assert_num_queries(1):
|
||||||
abilities = access.document.get_abilities(access.user)
|
abilities = access.document.get_abilities(access.user)
|
||||||
|
|
||||||
assert abilities == {
|
assert abilities == {
|
||||||
"destroy": False,
|
|
||||||
"attachment_upload": False,
|
"attachment_upload": False,
|
||||||
|
"destroy": False,
|
||||||
"manage_accesses": False,
|
"manage_accesses": False,
|
||||||
"partial_update": False,
|
"partial_update": False,
|
||||||
"retrieve": True,
|
"retrieve": True,
|
||||||
@@ -202,15 +226,17 @@ def test_models_documents_get_abilities_reader_user(django_assert_num_queries):
|
|||||||
|
|
||||||
def test_models_documents_get_abilities_preset_role(django_assert_num_queries):
|
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."""
|
"""No query is done if the role is preset e.g. with query annotation."""
|
||||||
access = factories.UserDocumentAccessFactory(role="reader")
|
access = factories.UserDocumentAccessFactory(
|
||||||
|
role="reader", document__link_role="reader"
|
||||||
|
)
|
||||||
access.document.user_roles = ["reader"]
|
access.document.user_roles = ["reader"]
|
||||||
|
|
||||||
with django_assert_num_queries(0):
|
with django_assert_num_queries(0):
|
||||||
abilities = access.document.get_abilities(access.user)
|
abilities = access.document.get_abilities(access.user)
|
||||||
|
|
||||||
assert abilities == {
|
assert abilities == {
|
||||||
"destroy": False,
|
|
||||||
"attachment_upload": False,
|
"attachment_upload": False,
|
||||||
|
"destroy": False,
|
||||||
"manage_accesses": False,
|
"manage_accesses": False,
|
||||||
"partial_update": False,
|
"partial_update": False,
|
||||||
"retrieve": True,
|
"retrieve": True,
|
||||||
|
|||||||
@@ -130,7 +130,9 @@ def create_demo(stdout):
|
|||||||
queue.push(
|
queue.push(
|
||||||
models.Document(
|
models.Document(
|
||||||
title=fake.sentence(nb_words=4),
|
title=fake.sentence(nb_words=4),
|
||||||
is_public=random_true_with_probability(0.5),
|
link_reach=models.LinkReachChoices.AUTHENTICATED
|
||||||
|
if random_true_with_probability(0.5)
|
||||||
|
else random.choice(models.LinkReachChoices.values),
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user