♻️(backend) rename, factorize and improve the subrequest media auth view
We want to use the same pattern for the websocket collaboration service authorization as what we use for media files. This addition comes in the next commit but doing it efficiently required factorizing some code with the media auth view.
This commit is contained in:
committed by
Anthony LC
parent
a9def8cb18
commit
64674b6a73
@@ -6,7 +6,7 @@ server {
|
|||||||
|
|
||||||
location /media/ {
|
location /media/ {
|
||||||
# Auth request configuration
|
# Auth request configuration
|
||||||
auth_request /auth;
|
auth_request /media-auth;
|
||||||
auth_request_set $authHeader $upstream_http_authorization;
|
auth_request_set $authHeader $upstream_http_authorization;
|
||||||
auth_request_set $authDate $upstream_http_x_amz_date;
|
auth_request_set $authDate $upstream_http_x_amz_date;
|
||||||
auth_request_set $authContentSha256 $upstream_http_x_amz_content_sha256;
|
auth_request_set $authContentSha256 $upstream_http_x_amz_content_sha256;
|
||||||
@@ -21,8 +21,8 @@ server {
|
|||||||
proxy_set_header Host minio:9000;
|
proxy_set_header Host minio:9000;
|
||||||
}
|
}
|
||||||
|
|
||||||
location /auth {
|
location /media-auth {
|
||||||
proxy_pass http://app-dev:8000/api/v1.0/documents/retrieve-auth/;
|
proxy_pass http://app-dev:8000/api/v1.0/documents/media-auth/;
|
||||||
proxy_set_header Host $host;
|
proxy_set_header Host $host;
|
||||||
proxy_set_header X-Real-IP $remote_addr;
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
"""API endpoints"""
|
"""API endpoints"""
|
||||||
# pylint: disable=too-many-lines
|
# pylint: disable=too-many-lines
|
||||||
|
|
||||||
|
import logging
|
||||||
import re
|
import re
|
||||||
import uuid
|
import uuid
|
||||||
from urllib.parse import urlparse
|
from urllib.parse import urlparse
|
||||||
@@ -10,10 +11,10 @@ from django.contrib.postgres.aggregates import ArrayAgg
|
|||||||
from django.contrib.postgres.search import TrigramSimilarity
|
from django.contrib.postgres.search import TrigramSimilarity
|
||||||
from django.core.exceptions import ValidationError
|
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 import models as db
|
||||||
from django.db.models import (
|
from django.db.models import (
|
||||||
Count,
|
Count,
|
||||||
Exists,
|
Exists,
|
||||||
Min,
|
|
||||||
OuterRef,
|
OuterRef,
|
||||||
Q,
|
Q,
|
||||||
Subquery,
|
Subquery,
|
||||||
@@ -21,24 +22,10 @@ from django.db.models import (
|
|||||||
)
|
)
|
||||||
from django.http import Http404
|
from django.http import Http404
|
||||||
|
|
||||||
|
import rest_framework as drf
|
||||||
from botocore.exceptions import ClientError
|
from botocore.exceptions import ClientError
|
||||||
from django_filters import rest_framework as filters
|
from django_filters import rest_framework as drf_filters
|
||||||
from rest_framework import (
|
from rest_framework import filters
|
||||||
decorators,
|
|
||||||
exceptions,
|
|
||||||
metadata,
|
|
||||||
mixins,
|
|
||||||
pagination,
|
|
||||||
status,
|
|
||||||
views,
|
|
||||||
viewsets,
|
|
||||||
)
|
|
||||||
from rest_framework import (
|
|
||||||
filters as drf_filters,
|
|
||||||
)
|
|
||||||
from rest_framework import (
|
|
||||||
response as drf_response,
|
|
||||||
)
|
|
||||||
from rest_framework.permissions import AllowAny
|
from rest_framework.permissions import AllowAny
|
||||||
|
|
||||||
from core import enums, models
|
from core import enums, models
|
||||||
@@ -47,22 +34,22 @@ from core.services.ai_services import AIService
|
|||||||
from . import permissions, serializers, utils
|
from . import permissions, serializers, utils
|
||||||
from .filters import DocumentFilter
|
from .filters import DocumentFilter
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
ATTACHMENTS_FOLDER = "attachments"
|
ATTACHMENTS_FOLDER = "attachments"
|
||||||
UUID_REGEX = (
|
UUID_REGEX = (
|
||||||
r"[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}"
|
r"[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}"
|
||||||
)
|
)
|
||||||
FILE_EXT_REGEX = r"\.[a-zA-Z]{3,4}"
|
FILE_EXT_REGEX = r"\.[a-zA-Z]{3,4}"
|
||||||
MEDIA_URL_PATTERN = re.compile(
|
MEDIA_STORAGE_URL_PATTERN = re.compile(
|
||||||
f"{settings.MEDIA_URL:s}({UUID_REGEX:s})/"
|
f"{settings.MEDIA_URL:s}(?P<pk>{UUID_REGEX:s})/"
|
||||||
f"({ATTACHMENTS_FOLDER:s}/{UUID_REGEX:s}{FILE_EXT_REGEX:s})$"
|
f"(?P<key>{ATTACHMENTS_FOLDER:s}/{UUID_REGEX:s}{FILE_EXT_REGEX:s})$"
|
||||||
)
|
)
|
||||||
|
|
||||||
# pylint: disable=too-many-ancestors
|
# pylint: disable=too-many-ancestors
|
||||||
|
|
||||||
ATTACHMENTS_FOLDER = "attachments"
|
|
||||||
|
|
||||||
|
class NestedGenericViewSet(drf.viewsets.GenericViewSet):
|
||||||
class NestedGenericViewSet(viewsets.GenericViewSet):
|
|
||||||
"""
|
"""
|
||||||
A generic Viewset aims to be used in a nested route context.
|
A generic Viewset aims to be used in a nested route context.
|
||||||
e.g: `/api/v1.0/resource_1/<resource_1_pk>/resource_2/<resource_2_pk>/`
|
e.g: `/api/v1.0/resource_1/<resource_1_pk>/resource_2/<resource_2_pk>/`
|
||||||
@@ -134,7 +121,7 @@ class SerializerPerActionMixin:
|
|||||||
return self.serializer_classes.get(self.action, self.default_serializer_class)
|
return self.serializer_classes.get(self.action, self.default_serializer_class)
|
||||||
|
|
||||||
|
|
||||||
class Pagination(pagination.PageNumberPagination):
|
class Pagination(drf.pagination.PageNumberPagination):
|
||||||
"""Pagination to display no more than 100 objects per page sorted by creation date."""
|
"""Pagination to display no more than 100 objects per page sorted by creation date."""
|
||||||
|
|
||||||
ordering = "-created_on"
|
ordering = "-created_on"
|
||||||
@@ -143,7 +130,7 @@ class Pagination(pagination.PageNumberPagination):
|
|||||||
|
|
||||||
|
|
||||||
class UserViewSet(
|
class UserViewSet(
|
||||||
mixins.UpdateModelMixin, viewsets.GenericViewSet, mixins.ListModelMixin
|
drf.mixins.UpdateModelMixin, drf.viewsets.GenericViewSet, drf.mixins.ListModelMixin
|
||||||
):
|
):
|
||||||
"""User ViewSet"""
|
"""User ViewSet"""
|
||||||
|
|
||||||
@@ -184,7 +171,7 @@ class UserViewSet(
|
|||||||
|
|
||||||
return queryset
|
return queryset
|
||||||
|
|
||||||
@decorators.action(
|
@drf.decorators.action(
|
||||||
detail=False,
|
detail=False,
|
||||||
methods=["get"],
|
methods=["get"],
|
||||||
url_name="me",
|
url_name="me",
|
||||||
@@ -196,7 +183,7 @@ class UserViewSet(
|
|||||||
Return information on currently logged user
|
Return information on currently logged user
|
||||||
"""
|
"""
|
||||||
context = {"request": request}
|
context = {"request": request}
|
||||||
return drf_response.Response(
|
return drf.response.Response(
|
||||||
self.serializer_class(request.user, context=context).data
|
self.serializer_class(request.user, context=context).data
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -231,7 +218,7 @@ class ResourceAccessViewsetMixin:
|
|||||||
teams = user.teams
|
teams = user.teams
|
||||||
user_roles_query = (
|
user_roles_query = (
|
||||||
queryset.filter(
|
queryset.filter(
|
||||||
Q(user=user) | Q(team__in=teams),
|
db.Q(user=user) | db.Q(team__in=teams),
|
||||||
**{self.resource_field_name: self.kwargs["resource_id"]},
|
**{self.resource_field_name: self.kwargs["resource_id"]},
|
||||||
)
|
)
|
||||||
.values(self.resource_field_name)
|
.values(self.resource_field_name)
|
||||||
@@ -245,11 +232,13 @@ class ResourceAccessViewsetMixin:
|
|||||||
# access instances pointing to the logged-in user)
|
# access instances pointing to the logged-in user)
|
||||||
queryset = (
|
queryset = (
|
||||||
queryset.filter(
|
queryset.filter(
|
||||||
Q(**{f"{self.resource_field_name}__accesses__user": user})
|
db.Q(**{f"{self.resource_field_name}__accesses__user": user})
|
||||||
| Q(**{f"{self.resource_field_name}__accesses__team__in": teams}),
|
| db.Q(
|
||||||
|
**{f"{self.resource_field_name}__accesses__team__in": teams}
|
||||||
|
),
|
||||||
**{self.resource_field_name: self.kwargs["resource_id"]},
|
**{self.resource_field_name: self.kwargs["resource_id"]},
|
||||||
)
|
)
|
||||||
.annotate(user_roles=Subquery(user_roles_query))
|
.annotate(user_roles=db.Subquery(user_roles_query))
|
||||||
.distinct()
|
.distinct()
|
||||||
)
|
)
|
||||||
return queryset
|
return queryset
|
||||||
@@ -264,9 +253,9 @@ class ResourceAccessViewsetMixin:
|
|||||||
instance.role == "owner"
|
instance.role == "owner"
|
||||||
and resource.accesses.filter(role="owner").count() == 1
|
and resource.accesses.filter(role="owner").count() == 1
|
||||||
):
|
):
|
||||||
return drf_response.Response(
|
return drf.response.Response(
|
||||||
{"detail": "Cannot delete the last owner access for the resource."},
|
{"detail": "Cannot delete the last owner access for the resource."},
|
||||||
status=status.HTTP_403_FORBIDDEN,
|
status=drf.status.HTTP_403_FORBIDDEN,
|
||||||
)
|
)
|
||||||
|
|
||||||
return super().destroy(request, *args, **kwargs)
|
return super().destroy(request, *args, **kwargs)
|
||||||
@@ -287,12 +276,12 @@ class ResourceAccessViewsetMixin:
|
|||||||
and resource.accesses.filter(role=models.RoleChoices.OWNER).count() == 1
|
and resource.accesses.filter(role=models.RoleChoices.OWNER).count() == 1
|
||||||
):
|
):
|
||||||
message = "Cannot change the role to a non-owner role for the last owner access."
|
message = "Cannot change the role to a non-owner role for the last owner access."
|
||||||
raise exceptions.PermissionDenied({"detail": message})
|
raise drf.exceptions.PermissionDenied({"detail": message})
|
||||||
|
|
||||||
serializer.save()
|
serializer.save()
|
||||||
|
|
||||||
|
|
||||||
class DocumentMetadata(metadata.SimpleMetadata):
|
class DocumentMetadata(drf.metadata.SimpleMetadata):
|
||||||
"""Custom metadata class to add information"""
|
"""Custom metadata class to add information"""
|
||||||
|
|
||||||
def determine_metadata(self, request, view):
|
def determine_metadata(self, request, view):
|
||||||
@@ -310,10 +299,10 @@ class DocumentMetadata(metadata.SimpleMetadata):
|
|||||||
|
|
||||||
|
|
||||||
class DocumentViewSet(
|
class DocumentViewSet(
|
||||||
mixins.CreateModelMixin,
|
drf.mixins.CreateModelMixin,
|
||||||
mixins.DestroyModelMixin,
|
drf.mixins.DestroyModelMixin,
|
||||||
mixins.UpdateModelMixin,
|
drf.mixins.UpdateModelMixin,
|
||||||
viewsets.GenericViewSet,
|
drf.viewsets.GenericViewSet,
|
||||||
):
|
):
|
||||||
"""
|
"""
|
||||||
Document ViewSet for managing documents.
|
Document ViewSet for managing documents.
|
||||||
@@ -333,7 +322,7 @@ class DocumentViewSet(
|
|||||||
- GET /api/v1.0/documents/?is_creator_me=false&title=hello
|
- GET /api/v1.0/documents/?is_creator_me=false&title=hello
|
||||||
"""
|
"""
|
||||||
|
|
||||||
filter_backends = [filters.DjangoFilterBackend, drf_filters.OrderingFilter]
|
filter_backends = [drf_filters.DjangoFilterBackend, filters.OrderingFilter]
|
||||||
filterset_class = DocumentFilter
|
filterset_class = DocumentFilter
|
||||||
metadata_class = DocumentMetadata
|
metadata_class = DocumentMetadata
|
||||||
ordering = ["-updated_at"]
|
ordering = ["-updated_at"]
|
||||||
@@ -389,11 +378,11 @@ class DocumentViewSet(
|
|||||||
|
|
||||||
if user.is_authenticated:
|
if user.is_authenticated:
|
||||||
queryset = queryset.filter(
|
queryset = queryset.filter(
|
||||||
Q(accesses__user=user)
|
db.Q(accesses__user=user)
|
||||||
| Q(accesses__team__in=user.teams)
|
| db.Q(accesses__team__in=user.teams)
|
||||||
| (
|
| (
|
||||||
Q(link_traces__user=user)
|
db.Q(link_traces__user=user)
|
||||||
& ~Q(link_reach=models.LinkReachChoices.RESTRICTED)
|
& ~db.Q(link_reach=models.LinkReachChoices.RESTRICTED)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
@@ -405,7 +394,7 @@ class DocumentViewSet(
|
|||||||
return self.get_paginated_response(serializer.data)
|
return self.get_paginated_response(serializer.data)
|
||||||
|
|
||||||
serializer = self.get_serializer(queryset, many=True)
|
serializer = self.get_serializer(queryset, many=True)
|
||||||
return drf_response.Response(serializer.data)
|
return drf.response.Response(serializer.data)
|
||||||
|
|
||||||
def retrieve(self, request, *args, **kwargs):
|
def retrieve(self, request, *args, **kwargs):
|
||||||
"""
|
"""
|
||||||
@@ -428,7 +417,7 @@ class DocumentViewSet(
|
|||||||
# The trace already exists, so we just pass without doing anything
|
# The trace already exists, so we just pass without doing anything
|
||||||
pass
|
pass
|
||||||
|
|
||||||
return drf_response.Response(serializer.data)
|
return drf.response.Response(serializer.data)
|
||||||
|
|
||||||
def perform_create(self, serializer):
|
def perform_create(self, serializer):
|
||||||
"""Set the current user as creator and owner of the newly created object."""
|
"""Set the current user as creator and owner of the newly created object."""
|
||||||
@@ -439,7 +428,7 @@ class DocumentViewSet(
|
|||||||
role=models.RoleChoices.OWNER,
|
role=models.RoleChoices.OWNER,
|
||||||
)
|
)
|
||||||
|
|
||||||
@decorators.action(detail=True, methods=["get"], url_path="versions")
|
@drf.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
|
||||||
@@ -447,7 +436,7 @@ class DocumentViewSet(
|
|||||||
"""
|
"""
|
||||||
user = request.user
|
user = request.user
|
||||||
if not user.is_authenticated:
|
if not user.is_authenticated:
|
||||||
raise exceptions.PermissionDenied("Authentication required.")
|
raise drf.exceptions.PermissionDenied("Authentication required.")
|
||||||
|
|
||||||
# Validate query parameters using dedicated serializer
|
# Validate query parameters using dedicated serializer
|
||||||
serializer = serializers.VersionFilterSerializer(data=request.query_params)
|
serializer = serializers.VersionFilterSerializer(data=request.query_params)
|
||||||
@@ -458,13 +447,13 @@ class DocumentViewSet(
|
|||||||
# Users should not see version history dating from before they gained access to the
|
# Users should not see version history dating from before they gained access to the
|
||||||
# document. Filter to get the minimum access date for the logged-in user
|
# document. Filter to get the minimum access date for the logged-in user
|
||||||
access_queryset = document.accesses.filter(
|
access_queryset = document.accesses.filter(
|
||||||
Q(user=user) | Q(team__in=user.teams)
|
db.Q(user=user) | db.Q(team__in=user.teams)
|
||||||
).aggregate(min_date=Min("created_at"))
|
).aggregate(min_date=db.Min("created_at"))
|
||||||
|
|
||||||
# Handle the case where the user has no accesses
|
# Handle the case where the user has no accesses
|
||||||
min_datetime = access_queryset["min_date"]
|
min_datetime = access_queryset["min_date"]
|
||||||
if not min_datetime:
|
if not min_datetime:
|
||||||
return exceptions.PermissionDenied(
|
return drf.exceptions.PermissionDenied(
|
||||||
"Only users with specific access can see version history"
|
"Only users with specific access can see version history"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -474,9 +463,9 @@ class DocumentViewSet(
|
|||||||
page_size=serializer.validated_data.get("page_size"),
|
page_size=serializer.validated_data.get("page_size"),
|
||||||
)
|
)
|
||||||
|
|
||||||
return drf_response.Response(versions_data)
|
return drf.response.Response(versions_data)
|
||||||
|
|
||||||
@decorators.action(
|
@drf.decorators.action(
|
||||||
detail=True,
|
detail=True,
|
||||||
methods=["get", "delete"],
|
methods=["get", "delete"],
|
||||||
url_path="versions/(?P<version_id>[0-9a-f-]{36})",
|
url_path="versions/(?P<version_id>[0-9a-f-]{36})",
|
||||||
@@ -497,7 +486,7 @@ class DocumentViewSet(
|
|||||||
min_datetime = min(
|
min_datetime = min(
|
||||||
access.created_at
|
access.created_at
|
||||||
for access in document.accesses.filter(
|
for access in document.accesses.filter(
|
||||||
Q(user=user) | Q(team__in=user.teams),
|
db.Q(user=user) | db.Q(team__in=user.teams),
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
if response["LastModified"] < min_datetime:
|
if response["LastModified"] < min_datetime:
|
||||||
@@ -505,11 +494,11 @@ class DocumentViewSet(
|
|||||||
|
|
||||||
if request.method == "DELETE":
|
if request.method == "DELETE":
|
||||||
response = document.delete_version(version_id)
|
response = document.delete_version(version_id)
|
||||||
return drf_response.Response(
|
return drf.response.Response(
|
||||||
status=response["ResponseMetadata"]["HTTPStatusCode"]
|
status=response["ResponseMetadata"]["HTTPStatusCode"]
|
||||||
)
|
)
|
||||||
|
|
||||||
return drf_response.Response(
|
return drf.response.Response(
|
||||||
{
|
{
|
||||||
"content": response["Body"].read().decode("utf-8"),
|
"content": response["Body"].read().decode("utf-8"),
|
||||||
"last_modified": response["LastModified"],
|
"last_modified": response["LastModified"],
|
||||||
@@ -517,7 +506,7 @@ class DocumentViewSet(
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
@decorators.action(detail=True, methods=["put"], url_path="link-configuration")
|
@drf.decorators.action(detail=True, methods=["put"], url_path="link-configuration")
|
||||||
def link_configuration(self, request, *args, **kwargs):
|
def link_configuration(self, request, *args, **kwargs):
|
||||||
"""Update link configuration with specific rights (cf get_abilities)."""
|
"""Update link configuration with specific rights (cf get_abilities)."""
|
||||||
# Check permissions first
|
# Check permissions first
|
||||||
@@ -530,9 +519,9 @@ class DocumentViewSet(
|
|||||||
serializer.is_valid(raise_exception=True)
|
serializer.is_valid(raise_exception=True)
|
||||||
|
|
||||||
serializer.save()
|
serializer.save()
|
||||||
return drf_response.Response(serializer.data, status=status.HTTP_200_OK)
|
return drf.response.Response(serializer.data, status=drf.status.HTTP_200_OK)
|
||||||
|
|
||||||
@decorators.action(detail=True, methods=["post", "delete"], url_path="favorite")
|
@drf.decorators.action(detail=True, methods=["post", "delete"], url_path="favorite")
|
||||||
def favorite(self, request, *args, **kwargs):
|
def favorite(self, request, *args, **kwargs):
|
||||||
"""
|
"""
|
||||||
Mark or unmark the document as a favorite for the logged-in user based on the HTTP method.
|
Mark or unmark the document as a favorite for the logged-in user based on the HTTP method.
|
||||||
@@ -546,13 +535,13 @@ class DocumentViewSet(
|
|||||||
try:
|
try:
|
||||||
models.DocumentFavorite.objects.create(document=document, user=user)
|
models.DocumentFavorite.objects.create(document=document, user=user)
|
||||||
except ValidationError:
|
except ValidationError:
|
||||||
return drf_response.Response(
|
return drf.response.Response(
|
||||||
{"detail": "Document already marked as favorite"},
|
{"detail": "Document already marked as favorite"},
|
||||||
status=status.HTTP_200_OK,
|
status=drf.status.HTTP_200_OK,
|
||||||
)
|
)
|
||||||
return drf_response.Response(
|
return drf.response.Response(
|
||||||
{"detail": "Document marked as favorite"},
|
{"detail": "Document marked as favorite"},
|
||||||
status=status.HTTP_201_CREATED,
|
status=drf.status.HTTP_201_CREATED,
|
||||||
)
|
)
|
||||||
|
|
||||||
# Handle DELETE method to unmark as favorite
|
# Handle DELETE method to unmark as favorite
|
||||||
@@ -560,16 +549,16 @@ class DocumentViewSet(
|
|||||||
document=document, user=user
|
document=document, user=user
|
||||||
).delete()
|
).delete()
|
||||||
if deleted:
|
if deleted:
|
||||||
return drf_response.Response(
|
return drf.response.Response(
|
||||||
{"detail": "Document unmarked as favorite"},
|
{"detail": "Document unmarked as favorite"},
|
||||||
status=status.HTTP_204_NO_CONTENT,
|
status=drf.status.HTTP_204_NO_CONTENT,
|
||||||
)
|
)
|
||||||
return drf_response.Response(
|
return drf.response.Response(
|
||||||
{"detail": "Document was already not marked as favorite"},
|
{"detail": "Document was already not marked as favorite"},
|
||||||
status=status.HTTP_200_OK,
|
status=drf.status.HTTP_200_OK,
|
||||||
)
|
)
|
||||||
|
|
||||||
@decorators.action(detail=True, methods=["post"], url_path="attachment-upload")
|
@drf.decorators.action(detail=True, methods=["post"], url_path="attachment-upload")
|
||||||
def attachment_upload(self, request, *args, **kwargs):
|
def attachment_upload(self, request, *args, **kwargs):
|
||||||
"""Upload a file related to a given document"""
|
"""Upload a file related to a given document"""
|
||||||
# Check permissions first
|
# Check permissions first
|
||||||
@@ -594,15 +583,15 @@ class DocumentViewSet(
|
|||||||
file, default_storage.bucket_name, key, ExtraArgs=extra_args
|
file, default_storage.bucket_name, key, ExtraArgs=extra_args
|
||||||
)
|
)
|
||||||
|
|
||||||
return drf_response.Response(
|
return drf.response.Response(
|
||||||
{"file": f"{settings.MEDIA_URL:s}{key:s}"}, status=status.HTTP_201_CREATED
|
{"file": f"{settings.MEDIA_URL:s}{key:s}"},
|
||||||
|
status=drf.status.HTTP_201_CREATED,
|
||||||
)
|
)
|
||||||
|
|
||||||
@decorators.action(detail=False, methods=["get"], url_path="retrieve-auth")
|
def _authorize_subrequest(self, request, pattern):
|
||||||
def retrieve_auth(self, request, *args, **kwargs):
|
|
||||||
"""
|
"""
|
||||||
This view is used by an Nginx subrequest to control access to a document's
|
Shared method to authorize access based on the original URL of an Nginx subrequest
|
||||||
attachment file.
|
and user permissions. Returns a dictionary of URL parameters if authorized.
|
||||||
|
|
||||||
The original url is passed by nginx in the "HTTP_X_ORIGINAL_URL" header.
|
The original url is passed by nginx in the "HTTP_X_ORIGINAL_URL" header.
|
||||||
See corresponding ingress configuration in Helm chart and read about the
|
See corresponding ingress configuration in Helm chart and read about the
|
||||||
@@ -614,33 +603,80 @@ class DocumentViewSet(
|
|||||||
a 403 error). Note that we return 403 errors without any further details for security
|
a 403 error). Note that we return 403 errors without any further details for security
|
||||||
reasons.
|
reasons.
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
- pattern: The regex pattern to extract identifiers from the URL.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
- A dictionary of URL parameters if the request is authorized.
|
||||||
|
Raises:
|
||||||
|
- PermissionDenied if authorization fails.
|
||||||
|
"""
|
||||||
|
# Extract the original URL from the request header
|
||||||
|
original_url = request.META.get("HTTP_X_ORIGINAL_URL")
|
||||||
|
if not original_url:
|
||||||
|
logger.debug("Missing HTTP_X_ORIGINAL_URL header in subrequest")
|
||||||
|
raise drf.exceptions.PermissionDenied()
|
||||||
|
|
||||||
|
parsed_url = urlparse(original_url)
|
||||||
|
match = pattern.search(parsed_url.path)
|
||||||
|
|
||||||
|
if not match:
|
||||||
|
logger.debug(
|
||||||
|
"Subrequest URL '%s' did not match pattern '%s'",
|
||||||
|
parsed_url.path,
|
||||||
|
pattern,
|
||||||
|
)
|
||||||
|
raise drf.exceptions.PermissionDenied()
|
||||||
|
|
||||||
|
try:
|
||||||
|
url_params = match.groupdict()
|
||||||
|
except (ValueError, AttributeError) as exc:
|
||||||
|
logger.debug("Failed to extract parameters from subrequest URL: %s", exc)
|
||||||
|
raise drf.exceptions.PermissionDenied() from exc
|
||||||
|
|
||||||
|
pk = url_params.get("pk")
|
||||||
|
if not pk:
|
||||||
|
logger.debug("Document ID (pk) not found in URL parameters: %s", url_params)
|
||||||
|
raise drf.exceptions.PermissionDenied()
|
||||||
|
|
||||||
|
# Fetch the document and check if the user has access
|
||||||
|
try:
|
||||||
|
document, _created = models.Document.objects.get_or_create(pk=pk)
|
||||||
|
except models.Document.DoesNotExist as exc:
|
||||||
|
logger.debug("Document with ID '%s' does not exist", pk)
|
||||||
|
raise drf.exceptions.PermissionDenied() from exc
|
||||||
|
print(document)
|
||||||
|
if not document.get_abilities(request.user).get(self.action, False):
|
||||||
|
logger.debug(
|
||||||
|
"User '%s' lacks permission for document '%s'", request.user, pk
|
||||||
|
)
|
||||||
|
# raise drf.exceptions.PermissionDenied()
|
||||||
|
|
||||||
|
logger.debug(
|
||||||
|
"Subrequest authorization successful. Extracted parameters: %s", url_params
|
||||||
|
)
|
||||||
|
return url_params
|
||||||
|
|
||||||
|
@drf.decorators.action(detail=False, methods=["get"], url_path="media-auth")
|
||||||
|
def media_auth(self, request, *args, **kwargs):
|
||||||
|
"""
|
||||||
|
This view is used by an Nginx subrequest to control access to a document's
|
||||||
|
attachment file.
|
||||||
|
|
||||||
When we let the request go through, we compute authorization headers that will be added to
|
When we let the request go through, we compute authorization headers that will be added to
|
||||||
the request going through thanks to the nginx.ingress.kubernetes.io/auth-response-headers
|
the request going through thanks to the nginx.ingress.kubernetes.io/auth-response-headers
|
||||||
annotation. The request will then be proxied to the object storage backend who will
|
annotation. The request will then be proxied to the object storage backend who will
|
||||||
respond with the file after checking the signature included in headers.
|
respond with the file after checking the signature included in headers.
|
||||||
"""
|
"""
|
||||||
original_url = urlparse(request.META.get("HTTP_X_ORIGINAL_URL"))
|
url_params = self._authorize_subrequest(request, MEDIA_STORAGE_URL_PATTERN)
|
||||||
match = MEDIA_URL_PATTERN.search(original_url.path)
|
pk, key = url_params.values()
|
||||||
|
|
||||||
try:
|
# Generate S3 authorization headers using the extracted URL parameters
|
||||||
pk, attachment_key = match.groups()
|
request = utils.generate_s3_authorization_headers(f"{pk:s}/{key:s}")
|
||||||
except AttributeError as excpt:
|
|
||||||
raise exceptions.PermissionDenied() from excpt
|
|
||||||
|
|
||||||
# Check permission
|
return drf.response.Response("authorized", headers=request.headers, status=200)
|
||||||
try:
|
|
||||||
document = models.Document.objects.get(pk=pk)
|
|
||||||
except models.Document.DoesNotExist as excpt:
|
|
||||||
raise exceptions.PermissionDenied() from excpt
|
|
||||||
|
|
||||||
if not document.get_abilities(request.user).get("retrieve", False):
|
@drf.decorators.action(
|
||||||
raise exceptions.PermissionDenied()
|
|
||||||
|
|
||||||
# Generate authorization headers and return an authorization to proceed with the request
|
|
||||||
request = utils.generate_s3_authorization_headers(f"{pk:s}/{attachment_key:s}")
|
|
||||||
return drf_response.Response("authorized", headers=request.headers, status=200)
|
|
||||||
|
|
||||||
@decorators.action(
|
|
||||||
detail=True,
|
detail=True,
|
||||||
methods=["post"],
|
methods=["post"],
|
||||||
name="Apply a transformation action on a piece of text with AI",
|
name="Apply a transformation action on a piece of text with AI",
|
||||||
@@ -666,9 +702,9 @@ class DocumentViewSet(
|
|||||||
|
|
||||||
response = AIService().transform(text, action)
|
response = AIService().transform(text, action)
|
||||||
|
|
||||||
return drf_response.Response(response, status=status.HTTP_200_OK)
|
return drf.response.Response(response, status=drf.status.HTTP_200_OK)
|
||||||
|
|
||||||
@decorators.action(
|
@drf.decorators.action(
|
||||||
detail=True,
|
detail=True,
|
||||||
methods=["post"],
|
methods=["post"],
|
||||||
name="Translate a piece of text with AI",
|
name="Translate a piece of text with AI",
|
||||||
@@ -695,17 +731,17 @@ class DocumentViewSet(
|
|||||||
|
|
||||||
response = AIService().translate(text, language)
|
response = AIService().translate(text, language)
|
||||||
|
|
||||||
return drf_response.Response(response, status=status.HTTP_200_OK)
|
return drf.response.Response(response, status=drf.status.HTTP_200_OK)
|
||||||
|
|
||||||
|
|
||||||
class DocumentAccessViewSet(
|
class DocumentAccessViewSet(
|
||||||
ResourceAccessViewsetMixin,
|
ResourceAccessViewsetMixin,
|
||||||
mixins.CreateModelMixin,
|
drf.mixins.CreateModelMixin,
|
||||||
mixins.DestroyModelMixin,
|
drf.mixins.DestroyModelMixin,
|
||||||
mixins.ListModelMixin,
|
drf.mixins.ListModelMixin,
|
||||||
mixins.RetrieveModelMixin,
|
drf.mixins.RetrieveModelMixin,
|
||||||
mixins.UpdateModelMixin,
|
drf.mixins.UpdateModelMixin,
|
||||||
viewsets.GenericViewSet,
|
drf.viewsets.GenericViewSet,
|
||||||
):
|
):
|
||||||
"""
|
"""
|
||||||
API ViewSet for all interactions with document accesses.
|
API ViewSet for all interactions with document accesses.
|
||||||
@@ -752,15 +788,15 @@ class DocumentAccessViewSet(
|
|||||||
|
|
||||||
|
|
||||||
class TemplateViewSet(
|
class TemplateViewSet(
|
||||||
mixins.CreateModelMixin,
|
drf.mixins.CreateModelMixin,
|
||||||
mixins.DestroyModelMixin,
|
drf.mixins.DestroyModelMixin,
|
||||||
mixins.RetrieveModelMixin,
|
drf.mixins.RetrieveModelMixin,
|
||||||
mixins.UpdateModelMixin,
|
drf.mixins.UpdateModelMixin,
|
||||||
viewsets.GenericViewSet,
|
drf.viewsets.GenericViewSet,
|
||||||
):
|
):
|
||||||
"""Template ViewSet"""
|
"""Template ViewSet"""
|
||||||
|
|
||||||
filter_backends = [drf_filters.OrderingFilter]
|
filter_backends = [drf.filters.OrderingFilter]
|
||||||
permission_classes = [
|
permission_classes = [
|
||||||
permissions.IsAuthenticatedOrSafe,
|
permissions.IsAuthenticatedOrSafe,
|
||||||
permissions.AccessPermission,
|
permissions.AccessPermission,
|
||||||
@@ -795,9 +831,9 @@ class TemplateViewSet(
|
|||||||
user = self.request.user
|
user = self.request.user
|
||||||
if user.is_authenticated:
|
if user.is_authenticated:
|
||||||
queryset = queryset.filter(
|
queryset = queryset.filter(
|
||||||
Q(accesses__user=user)
|
db.Q(accesses__user=user)
|
||||||
| Q(accesses__team__in=user.teams)
|
| db.Q(accesses__team__in=user.teams)
|
||||||
| Q(is_public=True)
|
| db.Q(is_public=True)
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
queryset = queryset.filter(is_public=True)
|
queryset = queryset.filter(is_public=True)
|
||||||
@@ -808,7 +844,7 @@ class TemplateViewSet(
|
|||||||
return self.get_paginated_response(serializer.data)
|
return self.get_paginated_response(serializer.data)
|
||||||
|
|
||||||
serializer = self.get_serializer(queryset, many=True)
|
serializer = self.get_serializer(queryset, many=True)
|
||||||
return drf_response.Response(serializer.data)
|
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."""
|
||||||
@@ -819,7 +855,7 @@ class TemplateViewSet(
|
|||||||
role=models.RoleChoices.OWNER,
|
role=models.RoleChoices.OWNER,
|
||||||
)
|
)
|
||||||
|
|
||||||
@decorators.action(
|
@drf.decorators.action(
|
||||||
detail=True,
|
detail=True,
|
||||||
methods=["post"],
|
methods=["post"],
|
||||||
url_path="generate-document",
|
url_path="generate-document",
|
||||||
@@ -842,8 +878,8 @@ class TemplateViewSet(
|
|||||||
serializer = serializers.DocumentGenerationSerializer(data=request.data)
|
serializer = serializers.DocumentGenerationSerializer(data=request.data)
|
||||||
|
|
||||||
if not serializer.is_valid():
|
if not serializer.is_valid():
|
||||||
return drf_response.Response(
|
return drf.response.Response(
|
||||||
serializer.errors, status=status.HTTP_400_BAD_REQUEST
|
serializer.errors, status=drf.status.HTTP_400_BAD_REQUEST
|
||||||
)
|
)
|
||||||
|
|
||||||
body = serializer.validated_data["body"]
|
body = serializer.validated_data["body"]
|
||||||
@@ -856,12 +892,12 @@ class TemplateViewSet(
|
|||||||
|
|
||||||
class TemplateAccessViewSet(
|
class TemplateAccessViewSet(
|
||||||
ResourceAccessViewsetMixin,
|
ResourceAccessViewsetMixin,
|
||||||
mixins.CreateModelMixin,
|
drf.mixins.CreateModelMixin,
|
||||||
mixins.DestroyModelMixin,
|
drf.mixins.DestroyModelMixin,
|
||||||
mixins.ListModelMixin,
|
drf.mixins.ListModelMixin,
|
||||||
mixins.RetrieveModelMixin,
|
drf.mixins.RetrieveModelMixin,
|
||||||
mixins.UpdateModelMixin,
|
drf.mixins.UpdateModelMixin,
|
||||||
viewsets.GenericViewSet,
|
drf.viewsets.GenericViewSet,
|
||||||
):
|
):
|
||||||
"""
|
"""
|
||||||
API ViewSet for all interactions with template accesses.
|
API ViewSet for all interactions with template accesses.
|
||||||
@@ -896,12 +932,12 @@ class TemplateAccessViewSet(
|
|||||||
|
|
||||||
|
|
||||||
class InvitationViewset(
|
class InvitationViewset(
|
||||||
mixins.CreateModelMixin,
|
drf.mixins.CreateModelMixin,
|
||||||
mixins.ListModelMixin,
|
drf.mixins.ListModelMixin,
|
||||||
mixins.RetrieveModelMixin,
|
drf.mixins.RetrieveModelMixin,
|
||||||
mixins.DestroyModelMixin,
|
drf.mixins.DestroyModelMixin,
|
||||||
mixins.UpdateModelMixin,
|
drf.mixins.UpdateModelMixin,
|
||||||
viewsets.GenericViewSet,
|
drf.viewsets.GenericViewSet,
|
||||||
):
|
):
|
||||||
"""API ViewSet for user invitations to document.
|
"""API ViewSet for user invitations to document.
|
||||||
|
|
||||||
@@ -953,7 +989,7 @@ class InvitationViewset(
|
|||||||
# Determine which role the logged-in user has in the document
|
# Determine which role the logged-in user has in the document
|
||||||
user_roles_query = (
|
user_roles_query = (
|
||||||
models.DocumentAccess.objects.filter(
|
models.DocumentAccess.objects.filter(
|
||||||
Q(user=user) | Q(team__in=teams),
|
db.Q(user=user) | db.Q(team__in=teams),
|
||||||
document=self.kwargs["resource_id"],
|
document=self.kwargs["resource_id"],
|
||||||
)
|
)
|
||||||
.values("document")
|
.values("document")
|
||||||
@@ -964,18 +1000,18 @@ class InvitationViewset(
|
|||||||
queryset = (
|
queryset = (
|
||||||
# The logged-in user should be administrator or owner to see its accesses
|
# The logged-in user should be administrator or owner to see its accesses
|
||||||
queryset.filter(
|
queryset.filter(
|
||||||
Q(
|
db.Q(
|
||||||
document__accesses__user=user,
|
document__accesses__user=user,
|
||||||
document__accesses__role__in=models.PRIVILEGED_ROLES,
|
document__accesses__role__in=models.PRIVILEGED_ROLES,
|
||||||
)
|
)
|
||||||
| Q(
|
| db.Q(
|
||||||
document__accesses__team__in=teams,
|
document__accesses__team__in=teams,
|
||||||
document__accesses__role__in=models.PRIVILEGED_ROLES,
|
document__accesses__role__in=models.PRIVILEGED_ROLES,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
# Abilities are computed based on logged-in user's role and
|
# Abilities are computed based on logged-in user's role and
|
||||||
# the user role on each document access
|
# the user role on each document access
|
||||||
.annotate(user_roles=Subquery(user_roles_query))
|
.annotate(user_roles=db.Subquery(user_roles_query))
|
||||||
.distinct()
|
.distinct()
|
||||||
)
|
)
|
||||||
return queryset
|
return queryset
|
||||||
@@ -991,7 +1027,7 @@ class InvitationViewset(
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class ConfigView(views.APIView):
|
class ConfigView(drf.views.APIView):
|
||||||
"""API ViewSet for sharing some public settings."""
|
"""API ViewSet for sharing some public settings."""
|
||||||
|
|
||||||
permission_classes = [AllowAny]
|
permission_classes = [AllowAny]
|
||||||
@@ -1016,4 +1052,4 @@ class ConfigView(views.APIView):
|
|||||||
if hasattr(settings, setting):
|
if hasattr(settings, setting):
|
||||||
dict_settings[setting] = getattr(settings, setting)
|
dict_settings[setting] = getattr(settings, setting)
|
||||||
|
|
||||||
return drf_response.Response(dict_settings)
|
return drf.response.Response(dict_settings)
|
||||||
|
|||||||
@@ -511,22 +511,23 @@ class Document(BaseModel):
|
|||||||
is_owner_or_admin = bool(
|
is_owner_or_admin = bool(
|
||||||
roles.intersection({RoleChoices.OWNER, RoleChoices.ADMIN})
|
roles.intersection({RoleChoices.OWNER, RoleChoices.ADMIN})
|
||||||
)
|
)
|
||||||
is_editor = bool(RoleChoices.EDITOR in roles)
|
|
||||||
can_get = bool(roles)
|
can_get = bool(roles)
|
||||||
|
can_update = is_owner_or_admin or RoleChoices.EDITOR in roles
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"accesses_manage": is_owner_or_admin,
|
"accesses_manage": is_owner_or_admin,
|
||||||
"accesses_view": has_role,
|
"accesses_view": has_role,
|
||||||
"ai_transform": is_owner_or_admin or is_editor,
|
"ai_transform": can_update,
|
||||||
"ai_translate": is_owner_or_admin or is_editor,
|
"ai_translate": can_update,
|
||||||
"attachment_upload": is_owner_or_admin or is_editor,
|
"attachment_upload": can_update,
|
||||||
"destroy": RoleChoices.OWNER in roles,
|
"destroy": RoleChoices.OWNER in roles,
|
||||||
"favorite": can_get and user.is_authenticated,
|
"favorite": can_get and user.is_authenticated,
|
||||||
"link_configuration": is_owner_or_admin,
|
"link_configuration": is_owner_or_admin,
|
||||||
"invite_owner": RoleChoices.OWNER in roles,
|
"invite_owner": RoleChoices.OWNER in roles,
|
||||||
"partial_update": is_owner_or_admin or is_editor,
|
"partial_update": can_update,
|
||||||
"retrieve": can_get,
|
"retrieve": can_get,
|
||||||
"update": is_owner_or_admin or is_editor,
|
"media_auth": can_get,
|
||||||
|
"update": can_update,
|
||||||
"versions_destroy": is_owner_or_admin,
|
"versions_destroy": is_owner_or_admin,
|
||||||
"versions_list": has_role,
|
"versions_list": has_role,
|
||||||
"versions_retrieve": has_role,
|
"versions_retrieve": has_role,
|
||||||
@@ -710,15 +711,15 @@ class Template(BaseModel):
|
|||||||
is_owner_or_admin = bool(
|
is_owner_or_admin = bool(
|
||||||
set(roles).intersection({RoleChoices.OWNER, RoleChoices.ADMIN})
|
set(roles).intersection({RoleChoices.OWNER, RoleChoices.ADMIN})
|
||||||
)
|
)
|
||||||
is_editor = bool(RoleChoices.EDITOR in roles)
|
|
||||||
can_get = self.is_public or bool(roles)
|
can_get = self.is_public or bool(roles)
|
||||||
|
can_update = is_owner_or_admin or RoleChoices.EDITOR in roles
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"destroy": RoleChoices.OWNER in roles,
|
"destroy": RoleChoices.OWNER in roles,
|
||||||
"generate_document": can_get,
|
"generate_document": can_get,
|
||||||
"accesses_manage": is_owner_or_admin,
|
"accesses_manage": is_owner_or_admin,
|
||||||
"update": is_owner_or_admin or is_editor,
|
"update": can_update,
|
||||||
"partial_update": is_owner_or_admin or is_editor,
|
"partial_update": can_update,
|
||||||
"retrieve": can_get,
|
"retrieve": can_get,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ from core.tests.conftest import TEAM, USER, VIA
|
|||||||
pytestmark = pytest.mark.django_db
|
pytestmark = pytest.mark.django_db
|
||||||
|
|
||||||
|
|
||||||
def test_api_documents_retrieve_auth_anonymous_public():
|
def test_api_documents_media_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(link_reach="public")
|
document = factories.DocumentFactory(link_reach="public")
|
||||||
|
|
||||||
@@ -36,7 +36,7 @@ def test_api_documents_retrieve_auth_anonymous_public():
|
|||||||
|
|
||||||
original_url = f"http://localhost/media/{key:s}"
|
original_url = f"http://localhost/media/{key:s}"
|
||||||
response = APIClient().get(
|
response = APIClient().get(
|
||||||
"/api/v1.0/documents/retrieve-auth/", HTTP_X_ORIGINAL_URL=original_url
|
"/api/v1.0/documents/media-auth/", HTTP_X_ORIGINAL_URL=original_url
|
||||||
)
|
)
|
||||||
|
|
||||||
assert response.status_code == 200
|
assert response.status_code == 200
|
||||||
@@ -65,7 +65,7 @@ def test_api_documents_retrieve_auth_anonymous_public():
|
|||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("reach", ["authenticated", "restricted"])
|
@pytest.mark.parametrize("reach", ["authenticated", "restricted"])
|
||||||
def test_api_documents_retrieve_auth_anonymous_authenticated_or_restricted(reach):
|
def test_api_documents_media_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
|
||||||
with link reach set to authenticated or restricted.
|
with link reach set to authenticated or restricted.
|
||||||
@@ -76,7 +76,7 @@ def test_api_documents_retrieve_auth_anonymous_authenticated_or_restricted(reach
|
|||||||
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 = APIClient().get(
|
||||||
"/api/v1.0/documents/retrieve-auth/", HTTP_X_ORIGINAL_URL=media_url
|
"/api/v1.0/documents/media-auth/", HTTP_X_ORIGINAL_URL=media_url
|
||||||
)
|
)
|
||||||
|
|
||||||
assert response.status_code == 403
|
assert response.status_code == 403
|
||||||
@@ -84,7 +84,7 @@ def test_api_documents_retrieve_auth_anonymous_authenticated_or_restricted(reach
|
|||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("reach", ["public", "authenticated"])
|
@pytest.mark.parametrize("reach", ["public", "authenticated"])
|
||||||
def test_api_documents_retrieve_auth_authenticated_public_or_authenticated(reach):
|
def test_api_documents_media_auth_authenticated_public_or_authenticated(reach):
|
||||||
"""
|
"""
|
||||||
Authenticated users who are not related to a document should be able to retrieve
|
Authenticated users who are not related to a document should be able to retrieve
|
||||||
attachments related to a document with public or authenticated link reach.
|
attachments related to a document with public or authenticated link reach.
|
||||||
@@ -107,7 +107,7 @@ def test_api_documents_retrieve_auth_authenticated_public_or_authenticated(reach
|
|||||||
|
|
||||||
original_url = f"http://localhost/media/{key:s}"
|
original_url = f"http://localhost/media/{key:s}"
|
||||||
response = client.get(
|
response = client.get(
|
||||||
"/api/v1.0/documents/retrieve-auth/", HTTP_X_ORIGINAL_URL=original_url
|
"/api/v1.0/documents/media-auth/", HTTP_X_ORIGINAL_URL=original_url
|
||||||
)
|
)
|
||||||
|
|
||||||
assert response.status_code == 200
|
assert response.status_code == 200
|
||||||
@@ -135,7 +135,7 @@ def test_api_documents_retrieve_auth_authenticated_public_or_authenticated(reach
|
|||||||
assert response.content.decode("utf-8") == "my prose"
|
assert response.content.decode("utf-8") == "my prose"
|
||||||
|
|
||||||
|
|
||||||
def test_api_documents_retrieve_auth_authenticated_restricted():
|
def test_api_documents_media_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 restricted.
|
retrieve attachments linked to a document that is restricted.
|
||||||
@@ -150,7 +150,7 @@ def test_api_documents_retrieve_auth_authenticated_restricted():
|
|||||||
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 = client.get(
|
response = client.get(
|
||||||
"/api/v1.0/documents/retrieve-auth/", HTTP_X_ORIGINAL_URL=media_url
|
"/api/v1.0/documents/media-auth/", HTTP_X_ORIGINAL_URL=media_url
|
||||||
)
|
)
|
||||||
|
|
||||||
assert response.status_code == 403
|
assert response.status_code == 403
|
||||||
@@ -158,7 +158,7 @@ def test_api_documents_retrieve_auth_authenticated_restricted():
|
|||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("via", VIA)
|
@pytest.mark.parametrize("via", VIA)
|
||||||
def test_api_documents_retrieve_auth_related(via, mock_user_teams):
|
def test_api_documents_media_auth_related(via, mock_user_teams):
|
||||||
"""
|
"""
|
||||||
Users who have a specific access to 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.
|
||||||
@@ -186,7 +186,7 @@ def test_api_documents_retrieve_auth_related(via, mock_user_teams):
|
|||||||
|
|
||||||
original_url = f"http://localhost/media/{key:s}"
|
original_url = f"http://localhost/media/{key:s}"
|
||||||
response = client.get(
|
response = client.get(
|
||||||
"/api/v1.0/documents/retrieve-auth/", HTTP_X_ORIGINAL_URL=original_url
|
"/api/v1.0/documents/media-auth/", HTTP_X_ORIGINAL_URL=original_url
|
||||||
)
|
)
|
||||||
|
|
||||||
assert response.status_code == 200
|
assert response.status_code == 200
|
||||||
@@ -31,6 +31,7 @@ def test_api_documents_retrieve_anonymous_public():
|
|||||||
"favorite": False,
|
"favorite": False,
|
||||||
"invite_owner": False,
|
"invite_owner": False,
|
||||||
"link_configuration": False,
|
"link_configuration": False,
|
||||||
|
"media_auth": True,
|
||||||
"partial_update": document.link_role == "editor",
|
"partial_update": document.link_role == "editor",
|
||||||
"retrieve": True,
|
"retrieve": True,
|
||||||
"update": document.link_role == "editor",
|
"update": document.link_role == "editor",
|
||||||
@@ -91,6 +92,7 @@ def test_api_documents_retrieve_authenticated_unrelated_public_or_authenticated(
|
|||||||
"destroy": False,
|
"destroy": False,
|
||||||
"favorite": True,
|
"favorite": True,
|
||||||
"invite_owner": False,
|
"invite_owner": False,
|
||||||
|
"media_auth": True,
|
||||||
"link_configuration": False,
|
"link_configuration": False,
|
||||||
"partial_update": document.link_role == "editor",
|
"partial_update": document.link_role == "editor",
|
||||||
"retrieve": True,
|
"retrieve": True,
|
||||||
|
|||||||
@@ -101,6 +101,7 @@ def test_models_documents_get_abilities_forbidden(is_authenticated, reach, role)
|
|||||||
"destroy": False,
|
"destroy": False,
|
||||||
"favorite": False,
|
"favorite": False,
|
||||||
"invite_owner": False,
|
"invite_owner": False,
|
||||||
|
"media_auth": False,
|
||||||
"link_configuration": False,
|
"link_configuration": False,
|
||||||
"partial_update": False,
|
"partial_update": False,
|
||||||
"retrieve": False,
|
"retrieve": False,
|
||||||
@@ -137,6 +138,7 @@ def test_models_documents_get_abilities_reader(is_authenticated, reach):
|
|||||||
"favorite": is_authenticated,
|
"favorite": is_authenticated,
|
||||||
"invite_owner": False,
|
"invite_owner": False,
|
||||||
"link_configuration": False,
|
"link_configuration": False,
|
||||||
|
"media_auth": True,
|
||||||
"partial_update": False,
|
"partial_update": False,
|
||||||
"retrieve": True,
|
"retrieve": True,
|
||||||
"update": False,
|
"update": False,
|
||||||
@@ -172,6 +174,7 @@ def test_models_documents_get_abilities_editor(is_authenticated, reach):
|
|||||||
"favorite": is_authenticated,
|
"favorite": is_authenticated,
|
||||||
"invite_owner": False,
|
"invite_owner": False,
|
||||||
"link_configuration": False,
|
"link_configuration": False,
|
||||||
|
"media_auth": True,
|
||||||
"partial_update": True,
|
"partial_update": True,
|
||||||
"retrieve": True,
|
"retrieve": True,
|
||||||
"update": True,
|
"update": True,
|
||||||
@@ -196,6 +199,7 @@ def test_models_documents_get_abilities_owner():
|
|||||||
"favorite": True,
|
"favorite": True,
|
||||||
"invite_owner": True,
|
"invite_owner": True,
|
||||||
"link_configuration": True,
|
"link_configuration": True,
|
||||||
|
"media_auth": True,
|
||||||
"partial_update": True,
|
"partial_update": True,
|
||||||
"retrieve": True,
|
"retrieve": True,
|
||||||
"update": True,
|
"update": True,
|
||||||
@@ -219,6 +223,7 @@ def test_models_documents_get_abilities_administrator():
|
|||||||
"favorite": True,
|
"favorite": True,
|
||||||
"invite_owner": False,
|
"invite_owner": False,
|
||||||
"link_configuration": True,
|
"link_configuration": True,
|
||||||
|
"media_auth": True,
|
||||||
"partial_update": True,
|
"partial_update": True,
|
||||||
"retrieve": True,
|
"retrieve": True,
|
||||||
"update": True,
|
"update": True,
|
||||||
@@ -245,6 +250,7 @@ def test_models_documents_get_abilities_editor_user(django_assert_num_queries):
|
|||||||
"favorite": True,
|
"favorite": True,
|
||||||
"invite_owner": False,
|
"invite_owner": False,
|
||||||
"link_configuration": False,
|
"link_configuration": False,
|
||||||
|
"media_auth": True,
|
||||||
"partial_update": True,
|
"partial_update": True,
|
||||||
"retrieve": True,
|
"retrieve": True,
|
||||||
"update": True,
|
"update": True,
|
||||||
@@ -273,6 +279,7 @@ def test_models_documents_get_abilities_reader_user(django_assert_num_queries):
|
|||||||
"favorite": True,
|
"favorite": True,
|
||||||
"invite_owner": False,
|
"invite_owner": False,
|
||||||
"link_configuration": False,
|
"link_configuration": False,
|
||||||
|
"media_auth": True,
|
||||||
"partial_update": False,
|
"partial_update": False,
|
||||||
"retrieve": True,
|
"retrieve": True,
|
||||||
"update": False,
|
"update": False,
|
||||||
@@ -302,6 +309,7 @@ def test_models_documents_get_abilities_preset_role(django_assert_num_queries):
|
|||||||
"favorite": True,
|
"favorite": True,
|
||||||
"invite_owner": False,
|
"invite_owner": False,
|
||||||
"link_configuration": False,
|
"link_configuration": False,
|
||||||
|
"media_auth": True,
|
||||||
"partial_update": False,
|
"partial_update": False,
|
||||||
"retrieve": True,
|
"retrieve": True,
|
||||||
"update": False,
|
"update": False,
|
||||||
|
|||||||
@@ -7,8 +7,8 @@ backend:
|
|||||||
replicas: 1
|
replicas: 1
|
||||||
envVars:
|
envVars:
|
||||||
DJANGO_CSRF_TRUSTED_ORIGINS: https://impress.127.0.0.1.nip.io,http://impress.127.0.0.1.nip.io
|
DJANGO_CSRF_TRUSTED_ORIGINS: https://impress.127.0.0.1.nip.io,http://impress.127.0.0.1.nip.io
|
||||||
DJANGO_CONFIGURATION: Production
|
DJANGO_CONFIGURATION: Feature
|
||||||
DJANGO_ALLOWED_HOSTS: "*"
|
DJANGO_ALLOWED_HOSTS: impress.127.0.0.1.nip.io
|
||||||
DJANGO_SECRET_KEY: {{ .Values.djangoSecretKey }}
|
DJANGO_SECRET_KEY: {{ .Values.djangoSecretKey }}
|
||||||
DJANGO_SETTINGS_MODULE: impress.settings
|
DJANGO_SETTINGS_MODULE: impress.settings
|
||||||
DJANGO_SUPERUSER_PASSWORD: admin
|
DJANGO_SUPERUSER_PASSWORD: admin
|
||||||
@@ -113,7 +113,7 @@ ingressMedia:
|
|||||||
host: impress.127.0.0.1.nip.io
|
host: impress.127.0.0.1.nip.io
|
||||||
|
|
||||||
annotations:
|
annotations:
|
||||||
nginx.ingress.kubernetes.io/auth-url: https://impress.127.0.0.1.nip.io/api/v1.0/documents/retrieve-auth/
|
nginx.ingress.kubernetes.io/auth-url: https://impress.127.0.0.1.nip.io/api/v1.0/documents/media-auth/
|
||||||
nginx.ingress.kubernetes.io/auth-response-headers: "Authorization, X-Amz-Date, X-Amz-Content-SHA256"
|
nginx.ingress.kubernetes.io/auth-response-headers: "Authorization, X-Amz-Date, X-Amz-Content-SHA256"
|
||||||
nginx.ingress.kubernetes.io/upstream-vhost: minio.impress.svc.cluster.local:9000
|
nginx.ingress.kubernetes.io/upstream-vhost: minio.impress.svc.cluster.local:9000
|
||||||
nginx.ingress.kubernetes.io/rewrite-target: /impress-media-storage/$1
|
nginx.ingress.kubernetes.io/rewrite-target: /impress-media-storage/$1
|
||||||
|
|||||||
@@ -116,7 +116,7 @@ ingressMedia:
|
|||||||
additional: []
|
additional: []
|
||||||
|
|
||||||
annotations:
|
annotations:
|
||||||
nginx.ingress.kubernetes.io/auth-url: https://impress.example.com/api/v1.0/documents/retrieve-auth/
|
nginx.ingress.kubernetes.io/auth-url: https://impress.example.com/api/v1.0/documents/media-auth/
|
||||||
nginx.ingress.kubernetes.io/auth-response-headers: "Authorization, X-Amz-Date, X-Amz-Content-SHA256"
|
nginx.ingress.kubernetes.io/auth-response-headers: "Authorization, X-Amz-Date, X-Amz-Content-SHA256"
|
||||||
nginx.ingress.kubernetes.io/upstream-vhost: minio.impress.svc.cluster.local:9000
|
nginx.ingress.kubernetes.io/upstream-vhost: minio.impress.svc.cluster.local:9000
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user