mirror of
https://github.com/zulip/zulip.git
synced 2025-11-02 13:03:29 +00:00
tusd: Reject tusd terminations after we insert them into our database.
The tusd protocol allows DELETE requests ("terminations") at any
point, including after a file has successfully been uploaded. This
can allow tusd to remove a file from the bucket, out from under Zulip.
We use the new-in-2.7.0 pre-terminate hook to look up the file which
the client is requesting to terminate, and reject the termination if
it is a file that the Zulip database is already aware of.
This commit is contained in:
committed by
Tim Abbott
parent
21eff33875
commit
cf51013bb7
@@ -55,7 +55,7 @@ class Command(BaseCommand):
|
||||
"-behind-proxy",
|
||||
f"-hooks-http={hooks_http}",
|
||||
"-hooks-http-forward-headers=Cookie,Authorization",
|
||||
"--hooks-enabled-events=pre-create,pre-finish",
|
||||
"--hooks-enabled-events=pre-create,pre-finish,pre-terminate",
|
||||
"-disable-download",
|
||||
"--show-startup-logs=false",
|
||||
]
|
||||
|
||||
@@ -8,7 +8,12 @@ from typing_extensions import ParamSpec
|
||||
from zerver.lib.cache import cache_delete, get_realm_used_upload_space_cache_key
|
||||
from zerver.lib.test_classes import ZulipTestCase
|
||||
from zerver.lib.test_helpers import create_s3_buckets, find_key_by_email, use_s3_backend
|
||||
from zerver.lib.upload import sanitize_name, upload_backend, upload_message_attachment
|
||||
from zerver.lib.upload import (
|
||||
create_attachment,
|
||||
sanitize_name,
|
||||
upload_backend,
|
||||
upload_message_attachment,
|
||||
)
|
||||
from zerver.lib.upload.s3 import S3UploadBackend
|
||||
from zerver.lib.utils import assert_is_not_none
|
||||
from zerver.models import Attachment, PreregistrationRealm, Realm
|
||||
@@ -662,3 +667,76 @@ class TusdPreFinishTest(ZulipTestCase):
|
||||
|
||||
response = bucket.Object(f"{path_id}.info").get()
|
||||
self.assertEqual(response["ContentType"], "binary/octet-stream")
|
||||
|
||||
|
||||
class TusdPreTerminateTest(ZulipTestCase):
|
||||
def request(self, info: TusUpload) -> TusHook:
|
||||
return TusHook(
|
||||
type="pre-terminate",
|
||||
event=TusEvent(
|
||||
upload=info,
|
||||
http_request=TusHTTPRequest(
|
||||
method="PATCH",
|
||||
uri=f"/api/v1/tus/{info.id}",
|
||||
remote_addr="12.34.56.78",
|
||||
header={},
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
def test_tusd_pre_terminate_hook(self) -> None:
|
||||
self.login("hamlet")
|
||||
hamlet = self.example_user("hamlet")
|
||||
|
||||
# Act like tusd does -- put the file and its .info in place
|
||||
path_id = upload_backend.generate_message_upload_path(
|
||||
str(hamlet.realm.id), sanitize_name("zulip.txt")
|
||||
)
|
||||
upload_backend.upload_message_attachment(
|
||||
path_id, "zulip.txt", "text/plain", b"zulip!", hamlet
|
||||
)
|
||||
|
||||
info = TusUpload(
|
||||
id=path_id,
|
||||
size=len("zulip!"),
|
||||
offset=0,
|
||||
size_is_deferred=False,
|
||||
meta_data={
|
||||
"filename": "zulip.txt",
|
||||
"filetype": "text/plain",
|
||||
"name": "zulip.txt",
|
||||
"type": "text/plain",
|
||||
},
|
||||
is_final=False,
|
||||
is_partial=False,
|
||||
partial_uploads=None,
|
||||
storage=None,
|
||||
)
|
||||
|
||||
# Try to terminate the upload before it's in Attachments
|
||||
result = self.client_post(
|
||||
"/api/internal/tusd",
|
||||
self.request(info).model_dump(),
|
||||
content_type="application/json",
|
||||
)
|
||||
self.assertEqual(result.status_code, 200)
|
||||
self.assertEqual(result.json(), {})
|
||||
|
||||
# Make the attachment
|
||||
create_attachment(
|
||||
"zulip.txt",
|
||||
path_id,
|
||||
"text/plain",
|
||||
b"zulip!",
|
||||
hamlet,
|
||||
hamlet.realm,
|
||||
)
|
||||
|
||||
# The terminate should get rejected now
|
||||
result = self.client_post(
|
||||
"/api/internal/tusd",
|
||||
self.request(info).model_dump(),
|
||||
content_type="application/json",
|
||||
)
|
||||
self.assertEqual(result.status_code, 200)
|
||||
self.assertEqual(result.json(), {"RejectTermination": True})
|
||||
|
||||
@@ -26,7 +26,7 @@ from zerver.lib.upload import (
|
||||
sanitize_name,
|
||||
upload_backend,
|
||||
)
|
||||
from zerver.models import PreregistrationRealm, Realm, UserProfile
|
||||
from zerver.models import ArchivedAttachment, Attachment, PreregistrationRealm, Realm, UserProfile
|
||||
|
||||
|
||||
# See https://tus.github.io/tusd/advanced-topics/hooks/ for the spec
|
||||
@@ -219,6 +219,22 @@ def handle_upload_pre_finish_hook(
|
||||
)
|
||||
|
||||
|
||||
def handle_upload_pre_terminate_hook(
|
||||
request: HttpRequest, user_profile: UserProfile, data: TusUpload
|
||||
) -> HttpResponse:
|
||||
path_id = data.id.partition("+")[0]
|
||||
|
||||
if (
|
||||
Attachment.objects.filter(path_id=path_id).exists()
|
||||
or ArchivedAttachment.objects.filter(path_id=path_id).exists()
|
||||
):
|
||||
# Once we have it in our Attachments table (i.e. the
|
||||
# pre-upload-finished hook has run), it is ours to manage and
|
||||
# we no longer accept terminations.
|
||||
return tusd_json_response({"RejectTermination": True})
|
||||
return tusd_json_response({})
|
||||
|
||||
|
||||
def authenticate_user(request: HttpRequest) -> UserProfile | AnonymousUser:
|
||||
# This acts like the authenticated_rest_api_view wrapper, while
|
||||
# allowing fallback to session-based request.user
|
||||
@@ -291,6 +307,8 @@ def handle_tusd_hook(
|
||||
return handle_upload_pre_create_hook(request, maybe_user, payload.event.upload)
|
||||
elif hook_name == "pre-finish":
|
||||
return handle_upload_pre_finish_hook(request, maybe_user, payload.event.upload)
|
||||
elif hook_name == "pre-terminate":
|
||||
return handle_upload_pre_terminate_hook(request, maybe_user, payload.event.upload)
|
||||
else:
|
||||
return HttpResponseNotFound()
|
||||
|
||||
|
||||
Reference in New Issue
Block a user