✨(collaboration) show a user the history since they got access

The collaboration server's activity and changeset routes are opened to
the browser, so a document's editing history can be read from where it
actually lives now. What a user may see of it is bounded to the moment
they were given access to the document: joining a document that has been
written for a year does not hand them the year.

That rule is not new. It is the one the version endpoints have always
applied - "only those created after the user got access to the document"
- and the date is the same one: the earliest access the user holds on
the document or on any of its ancestors, so sharing a folder shares its
subtree from that moment. It was computed twice in the backend,
differently, and exposed nowhere. It is now a single annotation,
user_access_since, that the version endpoints and the collaboration
server both read, the latter through the document detail response it
already fetches to authorize a connection.

The bound is applied server-side and silently: a client asks for
whatever range it likes and receives only its own share, so there is no
bound for it to get wrong and none it can widen. It is a stored date
rather than a wall-clock-relative one, which is what keeps it stable
across a websocket re-check, and it is never zero - the one value that
would also unlock a full-history connection.

A reader who reaches a document through its link alone holds no access
and so has no date to bound a history with. They get none, which is why
the backend has always refused them their versions. rollback and prune
stay refused to everyone: restoring a version is a separate decision.

Signed-off-by: Kevin Jahns <kevin.jahns@protonmail.com>
This commit is contained in:
Kevin Jahns
2026-09-22 16:01:42 +02:00
committed by Manuel Raynaud
parent 4041d38db2
commit 4a373f69df
10 changed files with 394 additions and 68 deletions
+22 -21
View File
@@ -563,6 +563,7 @@ class DocumentViewSet(
all_serializer_class = serializers.ListDocumentSerializer
children_serializer_class = serializers.ListDocumentSerializer
descendants_serializer_class = serializers.ListDocumentSerializer
favorite_list_serializer_class = serializers.ListDocumentSerializer
list_serializer_class = serializers.ListDocumentSerializer
trashbin_serializer_class = serializers.ListDocumentSerializer
tree_serializer_class = serializers.ListDocumentSerializer
@@ -612,6 +613,9 @@ class DocumentViewSet(
queryset = queryset.annotate_is_favorite(user)
queryset = queryset.annotate_user_roles(user)
queryset = queryset.annotate_user_has_link_trace(user)
# Detail views only — `list` builds its own annotation chain below and does not need
# this one, so the list endpoint keeps its current query cost
queryset = queryset.annotate_user_access_since(user)
return queryset
@@ -1822,16 +1826,14 @@ class DocumentViewSet(
document = self.get_object()
# 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
access_queryset = models.DocumentAccess.objects.filter(
db.Q(user=user) | db.Q(team__in=user.teams),
document__path__in=document.get_self_and_ancestors_paths(),
).aggregate(min_date=db.Min("created_at"))
# Handle the case where the user has no accesses
min_datetime = access_queryset["min_date"]
if not min_datetime:
return drf.exceptions.PermissionDenied(
# document. `user_access_since` is annotated onto the queryset (see
# `DocumentQuerySet.annotate_user_access_since`) and is the one definition of that
# date — the collaboration server is handed the same value to bound the history it
# serves. It is None for a user who reaches the document through its link reach
# alone: no access, no date, and so no history.
min_datetime = document.user_access_since
if min_datetime is None:
raise drf.exceptions.PermissionDenied(
"Only users with specific access can see version history"
)
@@ -1853,22 +1855,21 @@ class DocumentViewSet(
"""Custom action to retrieve a specific version of a document"""
document = self.get_object()
# Don't let users access versions that were created before they were given access to
# the document — the same cut-off as `versions_list`, from the same annotation.
# Checked before the object is fetched: a caller who may see no version at all should
# not learn from a 404 whether this one exists.
min_datetime = document.user_access_since
if min_datetime is None:
raise drf.exceptions.PermissionDenied(
"Only users with specific access can see version history"
)
try:
response = document.get_content_response(version_id=version_id)
except (FileNotFoundError, ClientError) as err:
raise Http404 from err
# Don't let users access versions that were created before they were given access
# to the document
user = request.user
min_datetime = min(
access.created_at
for access in models.DocumentAccess.objects.filter(
db.Q(user=user) | db.Q(team__in=user.teams),
document__path__in=document.get_self_and_ancestors_paths(),
)
)
if response["LastModified"] < min_datetime:
raise Http404