(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:
Samuel Paccoud - DINUM
2024-09-08 23:37:49 +02:00
committed by Samuel Paccoud
parent 41260de1c3
commit 494638d306
21 changed files with 744 additions and 326 deletions

View File

@@ -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

View File

@@ -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)

View File

@@ -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)

View File

@@ -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

View File

@@ -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"],

View File

@@ -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."""

View File

@@ -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.')],
},
),
]

View File

@@ -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
),
]

View File

@@ -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."""

View File

@@ -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):

View File

@@ -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:

View File

@@ -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

View File

@@ -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/",

View File

@@ -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"),
} }

View File

@@ -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:

View File

@@ -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

View File

@@ -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():

View File

@@ -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

View File

@@ -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():

View File

@@ -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,

View File

@@ -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),
) )
) )