realm: Add maximum file size upload restriction.

This commit adds a restriction to the maximum file size
that can be uploaded to a realm based on its plan_type.
This commit is contained in:
Prakhar Pratyush 2024-10-03 18:18:43 +05:30 committed by Tim Abbott
parent 808acc9e47
commit 3314c89288
12 changed files with 52 additions and 8 deletions

View File

@ -28,6 +28,11 @@ format used by the Zulip server that they are interacting with.
a standard `realm/update_dict` event to notify clients about changes a standard `realm/update_dict` event to notify clients about changes
in `plan_type` and other fields that atomically change with a given in `plan_type` and other fields that atomically change with a given
change in plan. change in plan.
* [`GET /events`](/api/get-events): Added `max_file_upload_size_mib`
field to the `data` object in `realm/update_dict` event format;
previously, this was a constant. Note that the field does not have a
`realm_` prefix in the [`POST /register`](/api/register-queue)
response.
**Feature level 305** **Feature level 305**

View File

@ -34,7 +34,7 @@ DESKTOP_WARNING_VERSION = "5.9.3"
# new level means in api_docs/changelog.md, as well as "**Changes**" # new level means in api_docs/changelog.md, as well as "**Changes**"
# entries in the endpoint's documentation in `zulip.yaml`. # entries in the endpoint's documentation in `zulip.yaml`.
API_FEATURE_LEVEL = 305 # Last bumped for adding can_add_members_group to usergroup. API_FEATURE_LEVEL = 306 # Last bumped for adding `max_file_upload_size_mib`.
# Bump the minor PROVISION_VERSION to indicate that folks should provision # Bump the minor PROVISION_VERSION to indicate that folks should provision
# only when going from an old version of the code to a newer version. Bump # only when going from an old version of the code to a newer version. Bump

View File

@ -284,7 +284,12 @@ export function dispatch_normal_event(event) {
switch (event.property) { switch (event.property) {
case "default": case "default":
for (const [key, value] of Object.entries(event.data)) { for (const [key, value] of Object.entries(event.data)) {
realm["realm_" + key] = value; if (key === "max_file_upload_size_mib") {
realm[key] = value;
} else {
realm["realm_" + key] = value;
}
if (Object.hasOwn(realm_settings, key)) { if (Object.hasOwn(realm_settings, key)) {
settings_org.sync_realm_settings(key); settings_org.sync_realm_settings(key);
} }

View File

@ -586,6 +586,7 @@ run_test("realm settings", ({override}) => {
override(realm, "realm_direct_message_permission_group", 1); override(realm, "realm_direct_message_permission_group", 1);
override(realm, "realm_plan_type", 2); override(realm, "realm_plan_type", 2);
override(realm, "realm_upload_quota_mib", 5000); override(realm, "realm_upload_quota_mib", 5000);
override(realm, "max_file_upload_size_mib", 10);
override(settings_org, "populate_auth_methods", noop); override(settings_org, "populate_auth_methods", noop);
dispatch(event); dispatch(event);
assert_same(realm.realm_create_multiuse_invite_group, 3); assert_same(realm.realm_create_multiuse_invite_group, 3);
@ -599,6 +600,7 @@ run_test("realm settings", ({override}) => {
assert_same(realm.realm_direct_message_permission_group, 3); assert_same(realm.realm_direct_message_permission_group, 3);
assert_same(realm.realm_plan_type, 3); assert_same(realm.realm_plan_type, 3);
assert_same(realm.realm_upload_quota_mib, 50000); assert_same(realm.realm_upload_quota_mib, 50000);
assert_same(realm.max_file_upload_size_mib, 1024);
assert_same(update_stream_privacy_choices_called, true); assert_same(update_stream_privacy_choices_called, true);
event = event_fixtures.realm__update_dict__icon; event = event_fixtures.realm__update_dict__icon;

View File

@ -372,6 +372,7 @@ exports.fixtures = {
direct_message_permission_group: 3, direct_message_permission_group: 3,
plan_type: 3, plan_type: 3,
upload_quota_mib: 50000, upload_quota_mib: 50000,
max_file_upload_size_mib: 1024,
}, },
}, },

View File

@ -794,6 +794,7 @@ def do_change_realm_plan_type(
data={ data={
"plan_type": plan_type, "plan_type": plan_type,
"upload_quota_mib": optional_bytes_to_mib(realm.upload_quota_bytes()), "upload_quota_mib": optional_bytes_to_mib(realm.upload_quota_bytes()),
"max_file_upload_size_mib": realm.get_max_file_upload_size_mebibytes(),
}, },
) )
send_event_on_commit(realm, event, active_user_ids(realm.id)) send_event_on_commit(realm, event, active_user_ids(realm.id))

View File

@ -1075,6 +1075,7 @@ plan_type_data = DictType(
required_keys=[ required_keys=[
("plan_type", int), ("plan_type", int),
("upload_quota_mib", OptionalType(int)), ("upload_quota_mib", OptionalType(int)),
("max_file_upload_size_mib", int),
], ],
) )

View File

@ -350,7 +350,7 @@ def fetch_initial_state_data(
# Important: Encode units in the client-facing API name. # Important: Encode units in the client-facing API name.
state["max_avatar_file_size_mib"] = settings.MAX_AVATAR_FILE_SIZE_MIB state["max_avatar_file_size_mib"] = settings.MAX_AVATAR_FILE_SIZE_MIB
state["max_file_upload_size_mib"] = settings.MAX_FILE_UPLOAD_SIZE state["max_file_upload_size_mib"] = realm.get_max_file_upload_size_mebibytes()
state["max_icon_file_size_mib"] = settings.MAX_ICON_FILE_SIZE_MIB state["max_icon_file_size_mib"] = settings.MAX_ICON_FILE_SIZE_MIB
upload_quota_bytes = realm.upload_quota_bytes() upload_quota_bytes = realm.upload_quota_bytes()
state["realm_upload_quota_mib"] = optional_bytes_to_mib(upload_quota_bytes) state["realm_upload_quota_mib"] = optional_bytes_to_mib(upload_quota_bytes)
@ -1308,6 +1308,10 @@ def apply_event(
) )
elif event["op"] == "update_dict": elif event["op"] == "update_dict":
for key, value in event["data"].items(): for key, value in event["data"].items():
if key == "max_file_upload_size_mib":
state["max_file_upload_size_mib"] = value
continue
state["realm_" + key] = value state["realm_" + key] = value
# It's a bit messy, but this is where we need to # It's a bit messy, but this is where we need to
# update the state for whether password authentication # update the state for whether password authentication

View File

@ -1011,6 +1011,21 @@ class Realm(models.Model): # type: ignore[django-manager-missing] # django-stub
# it as gibibytes (GiB) to be a bit more generous in case of confusion. # it as gibibytes (GiB) to be a bit more generous in case of confusion.
return self.upload_quota_gb << 30 return self.upload_quota_gb << 30
def get_max_file_upload_size_mebibytes(self) -> int:
plan_type = self.plan_type
if plan_type == Realm.PLAN_TYPE_SELF_HOSTED:
return settings.MAX_FILE_UPLOAD_SIZE
elif plan_type == Realm.PLAN_TYPE_LIMITED:
return 10
elif plan_type in [
Realm.PLAN_TYPE_STANDARD,
Realm.PLAN_TYPE_STANDARD_FREE,
Realm.PLAN_TYPE_PLUS,
]:
return 1024
else:
raise AssertionError("Invalid plan type")
# `realm` instead of `self` here to make sure the parameters of the cache key # `realm` instead of `self` here to make sure the parameters of the cache key
# function matches the original method. # function matches the original method.
@cache_with_key( @cache_with_key(

View File

@ -4679,6 +4679,14 @@ paths:
type: boolean type: boolean
description: | description: |
Whether [topics are required](/help/require-topics) for messages in this organization. Whether [topics are required](/help/require-topics) for messages in this organization.
max_file_upload_size_mib:
type: integer
description: |
The new maximum file size that can be uploaded to this Zulip organization.
**Changes**: New in Zulip 10.0 (feature level 306). Previously, this field of
the core state did not support being updated via the events system, as it was
typically hardcoded for a given Zulip installation.
message_content_allowed_in_email_notifications: message_content_allowed_in_email_notifications:
type: boolean type: boolean
description: | description: |
@ -17320,7 +17328,7 @@ paths:
description: | description: |
Present if `realm` is present in `fetch_event_types`. Present if `realm` is present in `fetch_event_types`.
The maximum file size that can be uploaded to this Zulip server. The maximum file size that can be uploaded to this Zulip organization.
max_avatar_file_size_mib: max_avatar_file_size_mib:
type: integer type: integer
description: | description: |

View File

@ -99,10 +99,11 @@ def handle_upload_pre_create_hook(
if data.size_is_deferred or data.size is None: if data.size_is_deferred or data.size is None:
return reject_upload("SizeIsDeferred is not supported", 411) return reject_upload("SizeIsDeferred is not supported", 411)
if data.size > settings.MAX_FILE_UPLOAD_SIZE * 1024 * 1024: max_file_upload_size_mebibytes = user_profile.realm.get_max_file_upload_size_mebibytes()
if data.size > max_file_upload_size_mebibytes * 1024 * 1024:
return reject_upload( return reject_upload(
_("Uploaded file is larger than the allowed limit of {max_file_size} MiB").format( _("Uploaded file is larger than the allowed limit of {max_file_size} MiB").format(
max_file_size=settings.MAX_FILE_UPLOAD_SIZE max_file_size=max_file_upload_size_mebibytes
), ),
413, 413,
) )

View File

@ -448,10 +448,11 @@ def upload_file_backend(request: HttpRequest, user_profile: UserProfile) -> Http
assert isinstance(user_file, UploadedFile) assert isinstance(user_file, UploadedFile)
file_size = user_file.size file_size = user_file.size
assert file_size is not None assert file_size is not None
if file_size > settings.MAX_FILE_UPLOAD_SIZE * 1024 * 1024: max_file_upload_size_mebibytes = user_profile.realm.get_max_file_upload_size_mebibytes()
if file_size > max_file_upload_size_mebibytes * 1024 * 1024:
raise JsonableError( raise JsonableError(
_("Uploaded file is larger than the allowed limit of {max_size} MiB").format( _("Uploaded file is larger than the allowed limit of {max_size} MiB").format(
max_size=settings.MAX_FILE_UPLOAD_SIZE, max_size=max_file_upload_size_mebibytes,
) )
) )
check_upload_within_quota(user_profile.realm, file_size) check_upload_within_quota(user_profile.realm, file_size)