diff --git a/api_docs/changelog.md b/api_docs/changelog.md index a5bb6eff0b..97912e8bc1 100644 --- a/api_docs/changelog.md +++ b/api_docs/changelog.md @@ -20,6 +20,13 @@ format used by the Zulip server that they are interacting with. ## Changes in Zulip 10.0 +**Feature level 280** + +* `PATCH /realm`, [`POST /register`](/api/register-queue), + [`GET /events`](/api/get-events): Added `can_create_web_public_channel_group` + realm setting, which is a [group-setting value](/api/group-setting-values) + describing the set of users with permission to create web-public channels. + Feature levels 278-279 are reserved for future use in 9.x maintenance releases. diff --git a/zerver/lib/event_schema.py b/zerver/lib/event_schema.py index 9f5d4c8077..eaeb749c3f 100644 --- a/zerver/lib/event_schema.py +++ b/zerver/lib/event_schema.py @@ -1049,6 +1049,7 @@ group_setting_update_data_type = DictType( ("can_access_all_users_group", int), ("can_create_public_channel_group", group_setting_type), ("can_create_private_channel_group", group_setting_type), + ("can_create_web_public_channel_group", group_setting_type), ("direct_message_initiator_group", group_setting_type), ("direct_message_permission_group", group_setting_type), ], diff --git a/zerver/lib/user_groups.py b/zerver/lib/user_groups.py index fbf4613362..65512e63fb 100644 --- a/zerver/lib/user_groups.py +++ b/zerver/lib/user_groups.py @@ -300,9 +300,8 @@ def access_user_group_for_setting( ) return named_user_group.usergroup_ptr - # The API would not allow passing the setting parameter as a Dict - # if require_system_group is true for a setting. - assert permission_configuration.require_system_group is False + if permission_configuration.require_system_group: + raise SystemGroupRequiredError(setting_name) user_group = update_or_create_user_group_for_setting( user_profile.realm, diff --git a/zerver/migrations/0559_realm_can_create_web_public_channel_group.py b/zerver/migrations/0559_realm_can_create_web_public_channel_group.py new file mode 100644 index 0000000000..0e39e4aca3 --- /dev/null +++ b/zerver/migrations/0559_realm_can_create_web_public_channel_group.py @@ -0,0 +1,23 @@ +# Generated by Django 5.0.6 on 2024-07-01 11:48 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("zerver", "0558_realmuserdefault_web_animate_image_previews_and_more"), + ] + + operations = [ + migrations.AddField( + model_name="realm", + name="can_create_web_public_channel_group", + field=models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.RESTRICT, + related_name="+", + to="zerver.usergroup", + ), + ), + ] diff --git a/zerver/migrations/0560_set_can_create_web_public_channel_group.py b/zerver/migrations/0560_set_can_create_web_public_channel_group.py new file mode 100644 index 0000000000..be6e48b75d --- /dev/null +++ b/zerver/migrations/0560_set_can_create_web_public_channel_group.py @@ -0,0 +1,61 @@ +# Generated by Django 5.0.6 on 2024-07-01 11:53 + +from django.db import migrations +from django.db.backends.base.schema import BaseDatabaseSchemaEditor +from django.db.migrations.state import StateApps +from django.db.models import OuterRef + + +def set_can_create_web_public_channel_group_for_existing_realms( + apps: StateApps, schema_editor: BaseDatabaseSchemaEditor +) -> None: + Realm = apps.get_model("zerver", "Realm") + NamedUserGroup = apps.get_model("zerver", "NamedUserGroup") + + ADMINS_ONLY = 2 + MODERATORS_ONLY = 4 + NOBODY = 6 + OWNERS_ONLY = 7 + + Realm.objects.filter( + can_create_web_public_channel_group=None, create_web_public_stream_policy=ADMINS_ONLY + ).update( + can_create_web_public_channel_group=NamedUserGroup.objects.filter( + name="role:administrators", realm=OuterRef("id"), is_system_group=True + ).values("pk") + ) + Realm.objects.filter( + can_create_web_public_channel_group=None, create_web_public_stream_policy=MODERATORS_ONLY + ).update( + can_create_web_public_channel_group=NamedUserGroup.objects.filter( + name="role:moderators", realm=OuterRef("id"), is_system_group=True + ).values("pk") + ) + Realm.objects.filter( + can_create_web_public_channel_group=None, create_web_public_stream_policy=NOBODY + ).update( + can_create_web_public_channel_group=NamedUserGroup.objects.filter( + name="role:nobody", realm=OuterRef("id"), is_system_group=True + ).values("pk") + ) + Realm.objects.filter( + can_create_web_public_channel_group=None, create_web_public_stream_policy=OWNERS_ONLY + ).update( + can_create_web_public_channel_group=NamedUserGroup.objects.filter( + name="role:owners", realm=OuterRef("id"), is_system_group=True + ).values("pk") + ) + + +class Migration(migrations.Migration): + dependencies = [ + ("zerver", "0559_realm_can_create_web_public_channel_group"), + ] + + operations = [ + migrations.RunPython( + set_can_create_web_public_channel_group_for_existing_realms, + elidable=True, + reverse_code=migrations.RunPython.noop, + ), + ] diff --git a/zerver/migrations/0561_alter_realm_can_create_web_public_channel_group.py b/zerver/migrations/0561_alter_realm_can_create_web_public_channel_group.py new file mode 100644 index 0000000000..533551d69e --- /dev/null +++ b/zerver/migrations/0561_alter_realm_can_create_web_public_channel_group.py @@ -0,0 +1,22 @@ +# Generated by Django 5.0.6 on 2024-07-01 12:00 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("zerver", "0560_set_can_create_web_public_channel_group"), + ] + + operations = [ + migrations.AlterField( + model_name="realm", + name="can_create_web_public_channel_group", + field=models.ForeignKey( + on_delete=django.db.models.deletion.RESTRICT, + related_name="+", + to="zerver.usergroup", + ), + ), + ] diff --git a/zerver/models/realms.py b/zerver/models/realms.py index 4ed18c7362..e06a869af6 100644 --- a/zerver/models/realms.py +++ b/zerver/models/realms.py @@ -308,6 +308,9 @@ class Realm(models.Model): # type: ignore[django-manager-missing] # django-stub can_create_private_channel_group = models.ForeignKey( "UserGroup", on_delete=models.RESTRICT, related_name="+" ) + can_create_web_public_channel_group = models.ForeignKey( + "UserGroup", on_delete=models.RESTRICT, related_name="+" + ) # Who in the organization is allowed to delete messages they themselves sent. delete_own_message_policy = models.PositiveSmallIntegerField( @@ -743,11 +746,27 @@ class Realm(models.Model): # type: ignore[django-manager-missing] # django-stub default_group_name=SystemGroups.EVERYONE, id_field_name="direct_message_permission_group_id", ), + can_create_web_public_channel_group=GroupPermissionSetting( + require_system_group=True, + allow_internet_group=False, + allow_owners_group=True, + allow_nobody_group=True, + allow_everyone_group=False, + default_group_name=SystemGroups.OWNERS, + id_field_name="can_create_web_public_channel_group_id", + allowed_system_groups=[ + SystemGroups.MODERATORS, + SystemGroups.ADMINISTRATORS, + SystemGroups.OWNERS, + SystemGroups.NOBODY, + ], + ), ) REALM_PERMISSION_GROUP_SETTINGS_WITH_NEW_API_FORMAT = [ "can_create_private_channel_group", "can_create_public_channel_group", + "can_create_web_public_channel_group", "direct_message_initiator_group", "direct_message_permission_group", ] @@ -1115,6 +1134,8 @@ def get_realm_with_settings(realm_id: int) -> Realm: "can_create_public_channel_group__named_user_group", "can_create_private_channel_group", "can_create_private_channel_group__named_user_group", + "can_create_web_public_channel_group", + "can_create_web_public_channel_group__named_user_group", "direct_message_initiator_group", "direct_message_initiator_group__named_user_group", "direct_message_permission_group", diff --git a/zerver/openapi/zulip.yaml b/zerver/openapi/zulip.yaml index 412f38cf3a..1a3ffb2081 100644 --- a/zerver/openapi/zulip.yaml +++ b/zerver/openapi/zulip.yaml @@ -4283,6 +4283,21 @@ paths: **Changes**: New in Zulip 9.0 (feature level 266). Previously `realm_create_private_stream_policy` field used to control the permission to create private channels. + can_create_web_public_channel_group: + allOf: + - $ref: "#/components/schemas/GroupSettingValue" + - description: | + A [group-setting value](/api/group-setting-values) defining + the set of users who have permission to create web-public + channels in this organization. + + This setting can only be set to `"role:moderators"`, + `"role:administrators"`, `"role:owners"` and + `"role:nobody"` system groups. + + **Changes**: New in Zulip 10.0 (feature level 280). Previously + `realm_create_web_public_stream_policy` field used to control + the permission to create web-public channels. create_web_public_stream_policy: type: integer description: | @@ -15440,6 +15455,26 @@ paths: **Changes**: New in Zulip 9.0 (feature level 266). Previously `realm_create_private_stream_policy` field used to control the permission to create private channels. + realm_can_create_web_public_channel_group: + allOf: + - $ref: "#/components/schemas/GroupSettingValue" + - description: | + A [group-setting value](/api/group-setting-values) defining + the set of users who have permission to create web-public + channels in this organization. + + This setting can only be set to `"role:moderators"`, + `"role:administrators"`, `"role:owners"` and + `"role:nobody"` system groups. + + Has no effect and should not be displayed in settings UI + unless the Zulip server has the `WEB_PUBLIC_STREAMS_ENABLED` + server-level setting enabled and the organization has enabled + the `enable_spectator_access` realm setting. + + **Changes**: New in Zulip 10.0 (feature level 280). Previously + `realm_create_web_public_stream_policy` field used to control + the permission to create web-public channels. realm_create_public_stream_policy: type: integer deprecated: true diff --git a/zerver/tests/test_events.py b/zerver/tests/test_events.py index 5fc0661fc9..9c47afc1d9 100644 --- a/zerver/tests/test_events.py +++ b/zerver/tests/test_events.py @@ -3772,6 +3772,8 @@ class RealmPropertyActionTest(BaseAction): self.do_set_realm_permission_group_setting_test(prop) for prop in Realm.REALM_PERMISSION_GROUP_SETTINGS_WITH_NEW_API_FORMAT: + if prop == "can_create_web_public_channel_group": + continue with self.settings(SEND_DIGEST_EMAILs=True): self.do_set_realm_permission_group_setting_to_anonymous_groups_test(prop) diff --git a/zerver/tests/test_home.py b/zerver/tests/test_home.py index 05d8ba64cc..1ac2b8df65 100644 --- a/zerver/tests/test_home.py +++ b/zerver/tests/test_home.py @@ -132,6 +132,7 @@ class HomeTest(ZulipTestCase): "realm_can_access_all_users_group", "realm_can_create_private_channel_group", "realm_can_create_public_channel_group", + "realm_can_create_web_public_channel_group", "realm_create_multiuse_invite_group", "realm_create_private_stream_policy", "realm_create_public_stream_policy", diff --git a/zerver/tests/test_realm.py b/zerver/tests/test_realm.py index cb3b96cd3e..b2a38f6680 100644 --- a/zerver/tests/test_realm.py +++ b/zerver/tests/test_realm.py @@ -1664,6 +1664,60 @@ class RealmAPITest(ZulipTestCase): realm = self.update_with_api(setting_name, value) self.assertEqual(getattr(realm, setting_name), user_group.usergroup_ptr) + if setting_permission_configuration.require_system_group: + leadership_group = NamedUserGroup.objects.get(name="leadership", realm=realm) + + value = orjson.dumps(leadership_group.id).decode() + if setting_name in Realm.REALM_PERMISSION_GROUP_SETTINGS_WITH_NEW_API_FORMAT: + value = orjson.dumps( + { + "new": leadership_group.id, + } + ).decode() + + result = self.client_patch("/json/realm", {setting_name: value}) + self.assert_json_error(result, f"'{setting_name}' must be a system user group.") + + if setting_name in Realm.REALM_PERMISSION_GROUP_SETTINGS_WITH_NEW_API_FORMAT: + admins_group = NamedUserGroup.objects.get( + name=SystemGroups.ADMINISTRATORS, realm=realm + ) + moderators_group = NamedUserGroup.objects.get( + name=SystemGroups.MODERATORS, realm=realm + ) + value = orjson.dumps( + { + "new": { + "direct_members": [], + "direct_subgroups": [admins_group.id, leadership_group.id], + } + } + ).decode() + result = self.client_patch("/json/realm", {setting_name: value}) + self.assert_json_error(result, f"'{setting_name}' must be a system user group.") + + value = orjson.dumps( + { + "new": { + "direct_members": [], + "direct_subgroups": [admins_group.id, moderators_group.id], + } + } + ).decode() + result = self.client_patch("/json/realm", {setting_name: value}) + self.assert_json_error(result, f"'{setting_name}' must be a system user group.") + + value = orjson.dumps( + { + "new": { + "direct_members": [], + "direct_subgroups": [admins_group.id], + } + } + ).decode() + realm = self.update_with_api(setting_name, value) + self.assertEqual(getattr(realm, setting_name), admins_group.usergroup_ptr) + def do_test_realm_permission_group_setting_update_api_with_anonymous_groups( self, setting_name: str ) -> None: @@ -1897,14 +1951,18 @@ class RealmAPITest(ZulipTestCase): with self.subTest(property=prop): self.do_test_realm_update_api(prop) + check_add_user_group( + get_realm("zulip"), "leadership", [self.example_user("hamlet")], acting_user=None + ) for prop in Realm.REALM_PERMISSION_GROUP_SETTINGS: with self.subTest(property=prop): self.do_test_realm_permission_group_setting_update_api(prop) - check_add_user_group( - get_realm("zulip"), "leadership", [self.example_user("hamlet")], acting_user=None - ) for prop in Realm.REALM_PERMISSION_GROUP_SETTINGS_WITH_NEW_API_FORMAT: + if prop == "can_create_web_public_channel_group": + # This setting supports the new API format but + # allows only system groups. + continue with self.subTest(property=prop): self.do_test_realm_permission_group_setting_update_api_with_anonymous_groups(prop) diff --git a/zerver/views/realm.py b/zerver/views/realm.py index f3158360dc..3a0e0423e6 100644 --- a/zerver/views/realm.py +++ b/zerver/views/realm.py @@ -144,6 +144,7 @@ def update_realm( bot_creation_policy: Json[BotCreationPolicyEnum] | None = None, can_create_public_channel_group: Json[GroupSettingChangeRequest] | None = None, can_create_private_channel_group: Json[GroupSettingChangeRequest] | None = None, + can_create_web_public_channel_group: Json[GroupSettingChangeRequest] | None = None, direct_message_initiator_group: Json[GroupSettingChangeRequest] | None = None, direct_message_permission_group: Json[GroupSettingChangeRequest] | None = None, create_web_public_stream_policy: Json[CreateWebPublicStreamPolicyEnum] | None = None,