streams: Add new can_subscribe_group permission setting.

Fixes part of #33417.
This commit is contained in:
Sahil Batra
2025-02-17 15:14:53 +05:30
committed by Tim Abbott
parent c1c321fd34
commit bafec11c61
12 changed files with 216 additions and 6 deletions

View File

@@ -20,6 +20,17 @@ format used by the Zulip server that they are interacting with.
## Changes in Zulip 10.0
**Feature level 357**
* [`GET /users/me/subscriptions`](/api/get-subscriptions),
[`GET /streams`](/api/get-streams), [`GET /events`](/api/get-events),
[`POST /register`](/api/register-queue): Added `can_subscribe_group`
field to Stream and Subscription objects.
* [`POST /users/me/subscriptions`](/api/subscribe),
[`PATCH /streams/{stream_id}`](/api/update-stream): Added
`can_subscribe_group` parameter to support setting and changing the
user group whose members can subscribe to the specified stream.
**Feature level 356**
* [`GET /streams`](/api/get-streams): The new parameter

View File

@@ -34,7 +34,7 @@ DESKTOP_WARNING_VERSION = "5.9.3"
# new level means in api_docs/changelog.md, as well as "**Changes**"
# entries in the endpoint's documentation in `zulip.yaml`.
API_FEATURE_LEVEL = 356 # Last bumped for adding `include_can_access_content` to get-streams.
API_FEATURE_LEVEL = 357 # Last bumped for adding 'can_subscribe_group'
# 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

View File

@@ -425,6 +425,7 @@ def send_subscription_add_events(
can_administer_channel_group=stream_dict["can_administer_channel_group"],
can_send_message_group=stream_dict["can_send_message_group"],
can_remove_subscribers_group=stream_dict["can_remove_subscribers_group"],
can_subscribe_group=stream_dict["can_subscribe_group"],
creator_id=stream_dict["creator_id"],
date_created=stream_dict["date_created"],
description=stream_dict["description"],

View File

@@ -86,6 +86,7 @@ class StreamDict(TypedDict, total=False):
can_administer_channel_group: UserGroup | None
can_send_message_group: UserGroup | None
can_remove_subscribers_group: UserGroup | None
can_subscribe_group: UserGroup | None
def get_stream_permission_policy_name(
@@ -256,6 +257,7 @@ def create_stream_if_needed(
can_administer_channel_group: UserGroup | None = None,
can_send_message_group: UserGroup | None = None,
can_remove_subscribers_group: UserGroup | None = None,
can_subscribe_group: UserGroup | None = None,
acting_user: UserProfile | None = None,
setting_groups_dict: dict[int, int | AnonymousSettingGroupDict] | None = None,
) -> tuple[Stream, bool]:
@@ -370,6 +372,7 @@ def create_streams_if_needed(
can_administer_channel_group=stream_dict.get("can_administer_channel_group", None),
can_send_message_group=stream_dict.get("can_send_message_group", None),
can_remove_subscribers_group=stream_dict.get("can_remove_subscribers_group", None),
can_subscribe_group=stream_dict.get("can_subscribe_group", None),
acting_user=acting_user,
setting_groups_dict=setting_groups_dict,
)
@@ -1398,6 +1401,7 @@ def stream_to_dict(
can_administer_channel_group = setting_groups_dict[stream.can_administer_channel_group_id]
can_send_message_group = setting_groups_dict[stream.can_send_message_group_id]
can_remove_subscribers_group = setting_groups_dict[stream.can_remove_subscribers_group_id]
can_subscribe_group = setting_groups_dict[stream.can_subscribe_group_id]
stream_post_policy = get_stream_post_policy_value_based_on_group_setting(
stream.can_send_message_group
@@ -1409,6 +1413,7 @@ def stream_to_dict(
can_administer_channel_group=can_administer_channel_group,
can_send_message_group=can_send_message_group,
can_remove_subscribers_group=can_remove_subscribers_group,
can_subscribe_group=can_subscribe_group,
creator_id=stream.creator_id,
date_created=datetime_to_timestamp(stream.date_created),
description=stream.description,

View File

@@ -62,6 +62,7 @@ def get_web_public_subs(realm: Realm) -> SubscriptionInfo:
can_administer_channel_group = setting_groups_dict[stream.can_administer_channel_group_id]
can_send_message_group = setting_groups_dict[stream.can_send_message_group_id]
can_remove_subscribers_group = setting_groups_dict[stream.can_remove_subscribers_group_id]
can_subscribe_group = setting_groups_dict[stream.can_subscribe_group_id]
creator_id = stream.creator_id
date_created = datetime_to_timestamp(stream.date_created)
description = stream.description
@@ -101,6 +102,7 @@ def get_web_public_subs(realm: Realm) -> SubscriptionInfo:
can_administer_channel_group=can_administer_channel_group,
can_send_message_group=can_send_message_group,
can_remove_subscribers_group=can_remove_subscribers_group,
can_subscribe_group=can_subscribe_group,
color=color,
creator_id=creator_id,
date_created=date_created,
@@ -168,6 +170,7 @@ def build_stream_api_dict(
can_remove_subscribers_group = setting_groups_dict[
raw_stream_dict["can_remove_subscribers_group_id"]
]
can_subscribe_group = setting_groups_dict[raw_stream_dict["can_subscribe_group_id"]]
return APIStreamDict(
is_archived=raw_stream_dict["deactivated"],
@@ -175,6 +178,7 @@ def build_stream_api_dict(
can_administer_channel_group=can_administer_channel_group,
can_send_message_group=can_send_message_group,
can_remove_subscribers_group=can_remove_subscribers_group,
can_subscribe_group=can_subscribe_group,
creator_id=raw_stream_dict["creator_id"],
date_created=datetime_to_timestamp(raw_stream_dict["date_created"]),
description=raw_stream_dict["description"],
@@ -204,6 +208,7 @@ def build_stream_dict_for_sub(
can_administer_channel_group = stream_dict["can_administer_channel_group"]
can_send_message_group = stream_dict["can_send_message_group"]
can_remove_subscribers_group = stream_dict["can_remove_subscribers_group"]
can_subscribe_group = stream_dict["can_subscribe_group"]
creator_id = stream_dict["creator_id"]
date_created = stream_dict["date_created"]
description = stream_dict["description"]
@@ -242,6 +247,7 @@ def build_stream_dict_for_sub(
can_administer_channel_group=can_administer_channel_group,
can_send_message_group=can_send_message_group,
can_remove_subscribers_group=can_remove_subscribers_group,
can_subscribe_group=can_subscribe_group,
color=color,
creator_id=creator_id,
date_created=date_created,
@@ -305,6 +311,7 @@ def build_stream_dict_for_never_sub(
can_remove_subscribers_group_value = setting_groups_dict[
raw_stream_dict["can_remove_subscribers_group_id"]
]
can_subscribe_group_value = setting_groups_dict[raw_stream_dict["can_subscribe_group_id"]]
# Backwards-compatibility addition of removed field.
is_announcement_only = raw_stream_dict["stream_post_policy"] == Stream.STREAM_POST_POLICY_ADMINS
@@ -316,6 +323,7 @@ def build_stream_dict_for_never_sub(
can_administer_channel_group=can_administer_channel_group_value,
can_send_message_group=can_send_message_group_value,
can_remove_subscribers_group=can_remove_subscribers_group_value,
can_subscribe_group=can_subscribe_group_value,
creator_id=creator_id,
date_created=date_created,
description=description,

View File

@@ -157,6 +157,7 @@ class RawStreamDict(TypedDict):
can_administer_channel_group_id: int
can_send_message_group_id: int
can_remove_subscribers_group_id: int
can_subscribe_group_id: int
creator_id: int | None
date_created: datetime
deactivated: bool
@@ -202,6 +203,7 @@ class SubscriptionStreamDict(TypedDict):
can_administer_channel_group: int | AnonymousSettingGroupDict
can_send_message_group: int | AnonymousSettingGroupDict
can_remove_subscribers_group: int | AnonymousSettingGroupDict
can_subscribe_group: int | AnonymousSettingGroupDict
color: str
creator_id: int | None
date_created: int
@@ -235,6 +237,7 @@ class NeverSubscribedStreamDict(TypedDict):
can_administer_channel_group: int | AnonymousSettingGroupDict
can_send_message_group: int | AnonymousSettingGroupDict
can_remove_subscribers_group: int | AnonymousSettingGroupDict
can_subscribe_group: int | AnonymousSettingGroupDict
creator_id: int | None
date_created: int
description: str
@@ -264,6 +267,7 @@ class DefaultStreamDict(TypedDict):
can_administer_channel_group: int | AnonymousSettingGroupDict
can_send_message_group: int | AnonymousSettingGroupDict
can_remove_subscribers_group: int | AnonymousSettingGroupDict
can_subscribe_group: int | AnonymousSettingGroupDict
creator_id: int | None
date_created: int
description: str

View File

@@ -0,0 +1,23 @@
# Generated by Django 5.0.10 on 2025-02-17 09:00
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("zerver", "0672_fix_attachment_realm"),
]
operations = [
migrations.AddField(
model_name="stream",
name="can_subscribe_group",
field=models.ForeignKey(
null=True,
on_delete=django.db.models.deletion.RESTRICT,
related_name="+",
to="zerver.usergroup",
),
),
]

View File

@@ -0,0 +1,55 @@
# Generated by Django 5.0.10 on 2025-02-17 09:18
from django.db import migrations, transaction
from django.db.backends.base.schema import BaseDatabaseSchemaEditor
from django.db.migrations.state import StateApps
from django.db.models import Max, Min, OuterRef
def set_default_value_for_can_subscribe_group(
apps: StateApps, schema_editor: BaseDatabaseSchemaEditor
) -> None:
Stream = apps.get_model("zerver", "Stream")
NamedUserGroup = apps.get_model("zerver", "NamedUserGroup")
BATCH_SIZE = 1000
max_id = Stream.objects.filter(can_subscribe_group=None).aggregate(Max("id"))["id__max"]
if max_id is None:
# Do nothing if there are no channels on the server.
return
lower_bound = Stream.objects.filter(can_subscribe_group=None).aggregate(Min("id"))["id__min"]
while lower_bound <= max_id + BATCH_SIZE / 2:
upper_bound = lower_bound + BATCH_SIZE - 1
print(f"Processing batch {lower_bound} to {upper_bound} for Stream")
with transaction.atomic():
Stream.objects.filter(
id__range=(lower_bound, upper_bound),
can_subscribe_group=None,
).update(
can_subscribe_group=NamedUserGroup.objects.filter(
name="role:nobody",
realm_for_sharding=OuterRef("realm_id"),
is_system_group=True,
).values("pk")
)
lower_bound += BATCH_SIZE
class Migration(migrations.Migration):
atomic = False
dependencies = [
("zerver", "0673_stream_can_subscribe_group"),
]
operations = [
migrations.RunPython(
set_default_value_for_can_subscribe_group,
elidable=True,
reverse_code=migrations.RunPython.noop,
),
]

View File

@@ -0,0 +1,22 @@
# Generated by Django 5.0.10 on 2025-02-17 09:32
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("zerver", "0674_set_default_for_stream_can_subscribe_group"),
]
operations = [
migrations.AlterField(
model_name="stream",
name="can_subscribe_group",
field=models.ForeignKey(
on_delete=django.db.models.deletion.RESTRICT,
related_name="+",
to="zerver.usergroup",
),
),
]

View File

@@ -135,6 +135,7 @@ class Stream(models.Model):
can_send_message_group = models.ForeignKey(
UserGroup, on_delete=models.RESTRICT, related_name="+"
)
can_subscribe_group = models.ForeignKey(UserGroup, on_delete=models.RESTRICT, related_name="+")
# The very first message ID in the stream. Used to help clients
# determine whether they might need to display "show all topics" for a
@@ -175,6 +176,13 @@ class Stream(models.Model):
allow_everyone_group=True,
default_group_name=SystemGroups.EVERYONE,
),
"can_subscribe_group": GroupPermissionSetting(
require_system_group=False,
allow_internet_group=False,
allow_nobody_group=True,
allow_everyone_group=False,
default_group_name=SystemGroups.NOBODY,
),
}
stream_permission_group_settings_requiring_content_access = [
@@ -234,6 +242,7 @@ class Stream(models.Model):
"can_administer_channel_group_id",
"can_send_message_group_id",
"can_remove_subscribers_group_id",
"can_subscribe_group_id",
"is_recently_active",
]

View File

@@ -718,6 +718,7 @@ paths:
"stream_weekly_traffic": null,
"can_add_subscribers_group": 2,
"can_remove_subscribers_group": 2,
"can_subscribe_group": 2,
"subscribers": [10],
},
],
@@ -1355,6 +1356,7 @@ paths:
"is_announcement_only": false,
"can_add_subscribers_group": 2,
"can_remove_subscribers_group": 2,
"can_subscribe_group": 2,
"stream_weekly_traffic": null,
},
],
@@ -10642,12 +10644,16 @@ paths:
created. The initial [channel settings](/api/update-stream) will be determined
by the optional parameters, like `invite_only`, detailed below.
Note that the ability to subscribe oneself and/or other users to a specified
channel depends on the [channel's type: private, public, or
web-public](/help/channel-permissions).
Note that the ability to subscribe oneself and/or other users
to a specified channel depends on the [channel's permissions
settings](/help/channel-permissions).
**Changes**: Before Zulip 10.0 (feature level 349), a user cannot
subscribe other users to a private channel without being subscribed
**Changes**: Before Zulip 10.0 (feature level 357), the
`can_subscribe_group` permission, which allows members of the
group to subscribe themselves to the channel, did not exist.
Before Zulip 10.0 (feature level 349), a user cannot subscribe
other users to a private channel without being subscribed
to that channel themselves. Now, If a user is part of
`can_add_subscribers_group`, they can subscribe themselves or other
users to a private channel without being subscribed to that channel.
@@ -10798,6 +10804,8 @@ paths:
$ref: "#/components/schemas/CanAdministerChannelGroup"
can_send_message_group:
$ref: "#/components/schemas/CanSendMessageGroup"
can_subscribe_group:
$ref: "#/components/schemas/CanSubscribeGroup"
required:
- subscriptions
encoding:
@@ -10825,6 +10833,8 @@ paths:
contentType: application/json
can_send_message_group:
contentType: application/json
can_subscribe_group:
contentType: application/json
responses:
"200":
description: Success.
@@ -15374,6 +15384,7 @@ paths:
can_remove_subscribers_group: {}
can_administer_channel_group: {}
can_send_message_group: {}
can_subscribe_group: {}
stream_weekly_traffic:
type: integer
nullable: true
@@ -20440,6 +20451,7 @@ paths:
can_remove_subscribers_group: {}
can_administer_channel_group: {}
can_send_message_group: {}
can_subscribe_group: {}
stream_weekly_traffic:
type: integer
nullable: true
@@ -20481,6 +20493,7 @@ paths:
- first_message_id
- is_announcement_only
- can_remove_subscribers_group
- can_subscribe_group
- stream_weekly_traffic
- is_recently_active
example:
@@ -20492,6 +20505,7 @@ paths:
{
"can_add_subscribers_group": 10,
"can_remove_subscribers_group": 10,
"can_subscribe_group": 10,
"creator_id": null,
"date_created": 1691057093,
"description": "A private channel",
@@ -20513,6 +20527,7 @@ paths:
{
"can_add_subscribers_group": 9,
"can_remove_subscribers_group": 9,
"can_subscribe_group": 10,
"creator_id": 12,
"date_created": 1691057093,
"description": "A default public channel",
@@ -20599,6 +20614,7 @@ paths:
"stream_post_policy": 1,
"can_add_subscribers_group": 2,
"can_remove_subscribers_group": 2,
"can_subscribe_group": 2,
"stream_weekly_traffic": null,
},
}
@@ -20861,6 +20877,32 @@ paths:
}
- $ref: "#/components/schemas/GroupSettingValueUpdate"
can_subscribe_group:
allOf:
- description: |
The set of users who have permission to subscribe themselves to this channel
expressed as an [update to a group-setting value][update-group-setting].
[update-group-setting]: /api/group-setting-values#updating-group-setting-values
Everyone, excluding guests, can subscribe to any public channel
irrespective of this setting.
Users in this group can subscribe to a private channel as well.
Note that a user must [have content access](/help/channel-permissions)
to a channel and permission to administer the channel in order to
modify this setting.
**Changes**: New in Zulip 10.0 (feature level 357).
example:
{
"new":
{"direct_members": [10], "direct_subgroups": [11]},
"old": 15,
}
- $ref: "#/components/schemas/GroupSettingValueUpdate"
encoding:
is_private:
contentType: application/json
@@ -20878,6 +20920,8 @@ paths:
contentType: application/json
can_send_message_group:
contentType: application/json
can_subscribe_group:
contentType: application/json
responses:
"200":
$ref: "#/components/responses/SimpleSuccess"
@@ -22622,6 +22666,7 @@ components:
can_remove_subscribers_group: {}
can_administer_channel_group: {}
can_send_message_group: {}
can_subscribe_group: {}
stream_weekly_traffic:
type: integer
nullable: true
@@ -22655,6 +22700,7 @@ components:
- is_announcement_only
- can_remove_subscribers_group
- stream_weekly_traffic
- can_subscribe_group
BasicChannelBase:
type: object
description: |
@@ -22809,6 +22855,8 @@ components:
$ref: "#/components/schemas/CanAdministerChannelGroup"
can_send_message_group:
$ref: "#/components/schemas/CanSendMessageGroup"
can_subscribe_group:
$ref: "#/components/schemas/CanSubscribeGroup"
BasicBot:
allOf:
- $ref: "#/components/schemas/BasicBotBase"
@@ -23873,6 +23921,8 @@ components:
$ref: "#/components/schemas/CanAdministerChannelGroup"
can_send_message_group:
$ref: "#/components/schemas/CanSendMessageGroup"
can_subscribe_group:
$ref: "#/components/schemas/CanSubscribeGroup"
is_archived:
type: boolean
description: |
@@ -25607,6 +25657,25 @@ components:
`stream_post_policy` field used to control the permission to
post in the channel.
[setting-values]: /api/group-setting-values
CanSubscribeGroup:
allOf:
- $ref: "#/components/schemas/GroupSettingValue"
- description: |
A [group-setting value][setting-values] defining the set of users
who have permission to subscribe themselves to this channel.
Everyone, excluding guests, can subscribe to any public channel
irrespective of this setting.
Users in this group can subscribe to a private channel as well.
Note that a user must [have content access](/help/channel-permissions)
to a channel and permission to administer the channel in order to
modify this setting.
**Changes**: New in Zulip 10.0 (feature level 357).
[setting-values]: /api/group-setting-values
LinkifierPattern:
description: |

View File

@@ -271,6 +271,7 @@ def update_stream_backend(
can_administer_channel_group: Json[GroupSettingChangeRequest] | None = None,
can_send_message_group: Json[GroupSettingChangeRequest] | None = None,
can_remove_subscribers_group: Json[GroupSettingChangeRequest] | None = None,
can_subscribe_group: Json[GroupSettingChangeRequest] | None = None,
) -> HttpResponse:
# Most settings updates only require metadata access, not content
# access. We will check for content access further when and where
@@ -602,6 +603,7 @@ def add_subscriptions_backend(
can_administer_channel_group: Json[int | AnonymousSettingGroupDict] | None = None,
can_send_message_group: Json[int | AnonymousSettingGroupDict] | None = None,
can_remove_subscribers_group: Json[int | AnonymousSettingGroupDict] | None = None,
can_subscribe_group: Json[int | AnonymousSettingGroupDict] | None = None,
announce: Json[bool] = False,
principals: Json[list[str] | list[int]] | None = None,
authorization_errors_fatal: Json[bool] = True,
@@ -681,6 +683,7 @@ def add_subscriptions_backend(
stream_dict_copy["can_remove_subscribers_group"] = group_settings_map[
"can_remove_subscribers_group"
]
stream_dict_copy["can_subscribe_group"] = group_settings_map["can_subscribe_group"]
stream_dicts.append(stream_dict_copy)