invite: Add new setting for "Who can create multiuse invite links".

This commit does the backend changes required for adding a realm
setting based on groups permission model and does the API changes
required for the new setting `Who can create multiuse invite link`.
This commit is contained in:
Ujjawal Modi 2023-08-09 18:36:56 +05:30 committed by Tim Abbott
parent 54c7cbaf1c
commit f67cef8885
19 changed files with 586 additions and 23 deletions

View File

@ -20,6 +20,19 @@ format used by the Zulip server that they are interacting with.
## Changes in Zulip 8.0 ## Changes in Zulip 8.0
**Feature level 209**
* `PATCH /realm`, [`POST /register`](/api/register-queue),
[`GET /events`](/api/get-events): Added `create_multiuse_invite_group`
realm setting, which is the ID of the user group whose members can
create [reusable invitation links](/help/invite-new-users#create-a-reusable-invitation-link)
to an organization. Previously, only admin users could create these
links.
* `POST /invites/multiuse`: Non-admin users can now use this endpoint
to create reusable invitation links. Previously, this endpoint was
restricted to admin users only.
**Feature level 208** **Feature level 208**
* [`POST /users/me/subscriptions`](/api/subscribe), * [`POST /users/me/subscriptions`](/api/subscribe),

View File

@ -16,7 +16,10 @@ from zerver.actions.realm_settings import (
from zerver.lib.bulk_create import create_users from zerver.lib.bulk_create import create_users
from zerver.lib.server_initialization import create_internal_realm, server_initialized from zerver.lib.server_initialization import create_internal_realm, server_initialized
from zerver.lib.streams import ensure_stream, get_signups_stream from zerver.lib.streams import ensure_stream, get_signups_stream
from zerver.lib.user_groups import create_system_user_groups_for_realm from zerver.lib.user_groups import (
create_system_user_groups_for_realm,
get_role_based_system_groups_dict,
)
from zerver.models import ( from zerver.models import (
DefaultStream, DefaultStream,
PreregistrationRealm, PreregistrationRealm,
@ -115,6 +118,17 @@ def set_realm_permissions_based_on_org_type(realm: Realm) -> None:
realm.move_messages_between_streams_policy = Realm.POLICY_MODERATORS_ONLY realm.move_messages_between_streams_policy = Realm.POLICY_MODERATORS_ONLY
@transaction.atomic(savepoint=False)
def set_default_for_realm_permission_group_settings(realm: Realm) -> None:
system_groups_dict = get_role_based_system_groups_dict(realm)
for setting_name, permissions_configuration in Realm.REALM_PERMISSION_GROUP_SETTINGS.items():
group_name = permissions_configuration.default_group_name
setattr(realm, setting_name, system_groups_dict[group_name])
realm.save(update_fields=list(Realm.REALM_PERMISSION_GROUP_SETTINGS.keys()))
def setup_realm_internal_bots(realm: Realm) -> None: def setup_realm_internal_bots(realm: Realm) -> None:
"""Create this realm's internal bots. """Create this realm's internal bots.
@ -204,6 +218,12 @@ def do_create_realm(
) )
set_realm_permissions_based_on_org_type(realm) set_realm_permissions_based_on_org_type(realm)
# For now a dummy value of -1 is given to groups fields which
# is changed later before the transaction is committed.
for permissions_configuration in Realm.REALM_PERMISSION_GROUP_SETTINGS.values():
setattr(realm, permissions_configuration.id_field_name, -1)
realm.save() realm.save()
RealmAuditLog.objects.create( RealmAuditLog.objects.create(
@ -230,6 +250,7 @@ def do_create_realm(
) )
create_system_user_groups_for_realm(realm) create_system_user_groups_for_realm(realm)
set_default_for_realm_permission_group_settings(realm)
# We create realms with all authentications methods enabled by default. # We create realms with all authentications methods enabled by default.
RealmAuthenticationMethod.objects.bulk_create( RealmAuthenticationMethod.objects.bulk_create(

View File

@ -31,6 +31,7 @@ from zerver.models import (
ScheduledEmail, ScheduledEmail,
Stream, Stream,
Subscription, Subscription,
UserGroup,
UserProfile, UserProfile,
active_user_ids, active_user_ids,
get_realm, get_realm,
@ -101,6 +102,42 @@ def do_set_realm_property(
update_users_in_full_members_system_group(realm, acting_user=acting_user) update_users_in_full_members_system_group(realm, acting_user=acting_user)
@transaction.atomic(durable=True)
def do_change_realm_permission_group_setting(
realm: Realm, setting_name: str, user_group: UserGroup, *, acting_user: Optional[UserProfile]
) -> None:
"""Takes in a realm object, the name of an attribute to update, the
user_group to update and and the user who initiated the update.
"""
assert setting_name in Realm.REALM_PERMISSION_GROUP_SETTINGS
old_user_group_id = getattr(realm, setting_name).id
setattr(realm, setting_name, user_group)
realm.save(update_fields=[setting_name])
event = dict(
type="realm",
op="update_dict",
property="default",
data={setting_name: user_group.id},
)
send_event_on_commit(realm, event, active_user_ids(realm.id))
event_time = timezone_now()
RealmAuditLog.objects.create(
realm=realm,
event_type=RealmAuditLog.REALM_PROPERTY_CHANGED,
event_time=event_time,
acting_user=acting_user,
extra_data={
RealmAuditLog.OLD_VALUE: old_user_group_id,
RealmAuditLog.NEW_VALUE: user_group.id,
"property": setting_name,
},
)
def parse_and_set_setting_value_if_required( def parse_and_set_setting_value_if_required(
realm: Realm, setting_name: str, value: Union[int, str], *, acting_user: Optional[UserProfile] realm: Realm, setting_name: str, value: Union[int, str], *, acting_user: Optional[UserProfile]
) -> Tuple[Optional[int], bool]: ) -> Tuple[Optional[int], bool]:

View File

@ -955,6 +955,10 @@ night_logo_data = DictType(
] ]
) )
group_setting_update_data_type = DictType(
required_keys=[], optional_keys=[("create_multiuse_invite_group", int)]
)
update_dict_data = UnionType( update_dict_data = UnionType(
[ [
allow_message_editing_data, allow_message_editing_data,
@ -964,6 +968,7 @@ update_dict_data = UnionType(
logo_data, logo_data,
message_content_edit_limit_seconds_data, message_content_edit_limit_seconds_data,
night_logo_data, night_logo_data,
group_setting_update_data_type,
] ]
) )
@ -996,6 +1001,10 @@ def check_realm_update_dict(
sub_type = edit_topic_policy_data sub_type = edit_topic_policy_data
elif "authentication_methods" in event["data"]: elif "authentication_methods" in event["data"]:
sub_type = authentication_data sub_type = authentication_data
elif any(
setting_name in event["data"] for setting_name in Realm.REALM_PERMISSION_GROUP_SETTINGS
):
sub_type = group_setting_update_data_type
else: else:
raise AssertionError("unhandled fields in data") raise AssertionError("unhandled fields in data")

View File

@ -259,6 +259,12 @@ def fetch_initial_state_data(
for property_name in Realm.property_types: for property_name in Realm.property_types:
state["realm_" + property_name] = getattr(realm, property_name) state["realm_" + property_name] = getattr(realm, property_name)
for (
setting_name,
permissions_configuration,
) in Realm.REALM_PERMISSION_GROUP_SETTINGS.items():
state["realm_" + setting_name] = getattr(realm, permissions_configuration.id_field_name)
# Most state is handled via the property_types framework; # Most state is handled via the property_types framework;
# these manual entries are for those realm settings that don't # these manual entries are for those realm settings that don't
# fit into that framework. # fit into that framework.

View File

@ -18,6 +18,7 @@ from psycopg2.extras import execute_values
from psycopg2.sql import SQL, Identifier from psycopg2.sql import SQL, Identifier
from analytics.models import RealmCount, StreamCount, UserCount from analytics.models import RealmCount, StreamCount, UserCount
from zerver.actions.create_realm import set_default_for_realm_permission_group_settings
from zerver.actions.realm_settings import do_change_realm_plan_type from zerver.actions.realm_settings import do_change_realm_plan_type
from zerver.actions.user_settings import do_change_avatar_fields from zerver.actions.user_settings import do_change_avatar_fields
from zerver.lib.avatar_hash import user_avatar_path_from_ids from zerver.lib.avatar_hash import user_avatar_path_from_ids
@ -906,10 +907,10 @@ def import_uploads(
# have to import the dependencies first.) # have to import the dependencies first.)
# #
# * Client [no deps] # * Client [no deps]
# * Realm [-notifications_stream] # * Realm [-notifications_stream,-group_permissions]
# * UserGroup # * UserGroup
# * Stream [only depends on realm] # * Stream [only depends on realm]
# * Realm's notifications_stream # * Realm's notifications_stream and group_permissions
# * UserProfile, in order by ID to avoid bot loop issues # * UserProfile, in order by ID to avoid bot loop issues
# * Now can do all realm_tables # * Now can do all realm_tables
# * Huddle # * Huddle
@ -948,12 +949,17 @@ def do_import_realm(import_dir: Path, subdomain: str, processes: int = 1) -> Rea
bulk_import_client(data, Client, "zerver_client") bulk_import_client(data, Client, "zerver_client")
# We don't import the Stream model yet, since it depends on Realm, # We don't import the Stream and UserGroup models yet, since
# which isn't imported yet. But we need the Stream model IDs for # they depend on Realm, which isn't imported yet.
# notifications_stream. # But we need the Stream and UserGroup model IDs for
# notifications_stream and group permissions, respectively
update_model_ids(Stream, data, "stream") update_model_ids(Stream, data, "stream")
re_map_foreign_keys(data, "zerver_realm", "notifications_stream", related_table="stream") re_map_foreign_keys(data, "zerver_realm", "notifications_stream", related_table="stream")
re_map_foreign_keys(data, "zerver_realm", "signup_notifications_stream", related_table="stream") re_map_foreign_keys(data, "zerver_realm", "signup_notifications_stream", related_table="stream")
if "zerver_usergroup" in data:
update_model_ids(UserGroup, data, "usergroup")
for setting_name in Realm.REALM_PERMISSION_GROUP_SETTINGS:
re_map_foreign_keys(data, "zerver_realm", setting_name, related_table="usergroup")
fix_datetime_fields(data, "zerver_realm") fix_datetime_fields(data, "zerver_realm")
# Fix realm subdomain information # Fix realm subdomain information
@ -968,10 +974,15 @@ def do_import_realm(import_dir: Path, subdomain: str, processes: int = 1) -> Rea
with transaction.atomic(durable=True): with transaction.atomic(durable=True):
realm = Realm(**realm_properties) realm = Realm(**realm_properties)
if "zerver_usergroup" not in data:
# For now a dummy value of -1 is given to groups fields which
# is changed later before the transaction is committed.
for permissions_configuration in Realm.REALM_PERMISSION_GROUP_SETTINGS.values():
setattr(realm, permissions_configuration.id_field_name, -1)
realm.save() realm.save()
if "zerver_usergroup" in data: if "zerver_usergroup" in data:
update_model_ids(UserGroup, data, "usergroup")
re_map_foreign_keys(data, "zerver_usergroup", "realm", related_table="realm") re_map_foreign_keys(data, "zerver_usergroup", "realm", related_table="realm")
for setting_name in UserGroup.GROUP_PERMISSION_SETTINGS: for setting_name in UserGroup.GROUP_PERMISSION_SETTINGS:
re_map_foreign_keys( re_map_foreign_keys(
@ -1003,6 +1014,9 @@ def do_import_realm(import_dir: Path, subdomain: str, processes: int = 1) -> Rea
stream["rendered_description"] = render_stream_description(stream["description"], realm) stream["rendered_description"] = render_stream_description(stream["description"], realm)
bulk_import_model(data, Stream) bulk_import_model(data, Stream)
if "zerver_usergroup" not in data:
set_default_for_realm_permission_group_settings(realm)
# Remap the user IDs for notification_bot and friends to their # Remap the user IDs for notification_bot and friends to their
# appropriate IDs on this server # appropriate IDs on this server
internal_realm = get_realm(settings.SYSTEM_BOT_REALM) internal_realm = get_realm(settings.SYSTEM_BOT_REALM)

View File

@ -23,14 +23,23 @@ def server_initialized() -> bool:
@transaction.atomic(durable=True) @transaction.atomic(durable=True)
def create_internal_realm() -> None: def create_internal_realm() -> None:
from zerver.actions.create_realm import set_default_for_realm_permission_group_settings
from zerver.actions.users import do_change_can_forge_sender from zerver.actions.users import do_change_can_forge_sender
realm = Realm.objects.create(string_id=settings.SYSTEM_BOT_REALM, name="System bot realm") realm = Realm(string_id=settings.SYSTEM_BOT_REALM, name="System bot realm")
# For now a dummy value of -1 is given to groups fields which
# is changed later before the transaction is committed.
for permissions_configuration in Realm.REALM_PERMISSION_GROUP_SETTINGS.values():
setattr(realm, permissions_configuration.id_field_name, -1)
realm.save()
RealmAuditLog.objects.create( RealmAuditLog.objects.create(
realm=realm, event_type=RealmAuditLog.REALM_CREATED, event_time=realm.date_created realm=realm, event_type=RealmAuditLog.REALM_CREATED, event_time=realm.date_created
) )
RealmUserDefault.objects.create(realm=realm) RealmUserDefault.objects.create(realm=realm)
create_system_user_groups_for_realm(realm) create_system_user_groups_for_realm(realm)
set_default_for_realm_permission_group_settings(realm)
# We create realms with all authentications methods enabled by default. # We create realms with all authentications methods enabled by default.
RealmAuthenticationMethod.objects.bulk_create( RealmAuthenticationMethod.objects.bulk_create(

View File

@ -0,0 +1,23 @@
# Generated by Django 4.2.1 on 2023-05-31 07:28
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("zerver", "0468_rename_followup_day_email_templates"),
]
operations = [
migrations.AddField(
model_name="realm",
name="create_multiuse_invite_group",
field=models.ForeignKey(
null=True,
on_delete=django.db.models.deletion.RESTRICT,
related_name="+",
to="zerver.usergroup",
),
),
]

View File

@ -0,0 +1,46 @@
# Generated by Django 4.2.1 on 2023-06-03 10:53
from django.db import migrations
from django.db.backends.base.schema import BaseDatabaseSchemaEditor
from django.db.migrations.state import StateApps
def set_default_value_for_create_multiuse_invite_group(
apps: StateApps, schema_editor: BaseDatabaseSchemaEditor
) -> None:
Realm = apps.get_model("zerver", "Realm")
UserGroup = apps.get_model("zerver", "UserGroup")
UserGroup.ADMINISTRATORS_GROUP_NAME = "role:administrators"
for realm in Realm.objects.all():
if realm.create_multiuse_invite_group is not None:
continue
# Prior to the new create_multiuse_invite_group field being
# created, multi-use invitation links could only be created
# and managed by administrators, regardless of
# invite_to_realm_policy. We replicate that policy for the
# initial value of the new setting.
admins_group = UserGroup.objects.get(
name=UserGroup.ADMINISTRATORS_GROUP_NAME, realm=realm, is_system_group=True
)
realm.create_multiuse_invite_group = admins_group
realm.save(update_fields=["create_multiuse_invite_group"])
class Migration(migrations.Migration):
atomic = False
dependencies = [
("zerver", "0469_realm_create_multiuse_invite_group"),
]
operations = [
migrations.RunPython(
set_default_value_for_create_multiuse_invite_group,
elidable=True,
reverse_code=migrations.RunPython.noop,
),
]

View File

@ -0,0 +1,22 @@
# Generated by Django 4.2.1 on 2023-06-06 08:26
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("zerver", "0470_set_default_value_for_create_multiuse_invite_group"),
]
operations = [
migrations.AlterField(
model_name="realm",
name="create_multiuse_invite_group",
field=models.ForeignKey(
on_delete=django.db.models.deletion.RESTRICT,
related_name="+",
to="zerver.usergroup",
),
),
]

View File

@ -409,6 +409,11 @@ class Realm(models.Model): # type: ignore[django-manager-missing] # django-stub
# Who in the organization is allowed to invite other users to organization. # Who in the organization is allowed to invite other users to organization.
invite_to_realm_policy = models.PositiveSmallIntegerField(default=POLICY_MEMBERS_ONLY) invite_to_realm_policy = models.PositiveSmallIntegerField(default=POLICY_MEMBERS_ONLY)
# UserGroup whose members are allowed to create invite link.
create_multiuse_invite_group = models.ForeignKey(
"UserGroup", on_delete=models.RESTRICT, related_name="+"
)
# Who in the organization is allowed to invite other users to streams. # Who in the organization is allowed to invite other users to streams.
invite_to_stream_policy = models.PositiveSmallIntegerField(default=POLICY_MEMBERS_ONLY) invite_to_stream_policy = models.PositiveSmallIntegerField(default=POLICY_MEMBERS_ONLY)
@ -704,6 +709,9 @@ class Realm(models.Model): # type: ignore[django-manager-missing] # django-stub
# they will not be available regardless of users' personal settings. # they will not be available regardless of users' personal settings.
enable_read_receipts = models.BooleanField(default=False) enable_read_receipts = models.BooleanField(default=False)
# Duplicates of names for system group; TODO: Clean this up.
ADMINISTRATORS_GROUP_NAME = "role:administrators"
# Define the types of the various automatically managed properties # Define the types of the various automatically managed properties
property_types: Dict[str, Union[type, Tuple[type, ...]]] = dict( property_types: Dict[str, Union[type, Tuple[type, ...]]] = dict(
add_custom_emoji_policy=int, add_custom_emoji_policy=int,
@ -751,6 +759,17 @@ class Realm(models.Model): # type: ignore[django-manager-missing] # django-stub
wildcard_mention_policy=int, wildcard_mention_policy=int,
) )
REALM_PERMISSION_GROUP_SETTINGS: Dict[str, GroupPermissionSetting] = dict(
create_multiuse_invite_group=GroupPermissionSetting(
require_system_group=True,
allow_internet_group=False,
allow_owners_group=False,
allow_nobody_group=True,
default_group_name=ADMINISTRATORS_GROUP_NAME,
id_field_name="create_multiuse_invite_group_id",
),
)
DIGEST_WEEKDAY_VALUES = [0, 1, 2, 3, 4, 5, 6] DIGEST_WEEKDAY_VALUES = [0, 1, 2, 3, 4, 5, 6]
# Icon is the square mobile icon. # Icon is the square mobile icon.
@ -2089,8 +2108,11 @@ class UserProfile(AbstractBaseUser, PermissionsMixin, UserBaseSettings): # type
return False return False
def has_permission(self, policy_name: str) -> bool: def has_permission(self, policy_name: str) -> bool:
from zerver.lib.user_groups import is_user_in_group
if policy_name not in [ if policy_name not in [
"add_custom_emoji_policy", "add_custom_emoji_policy",
"create_multiuse_invite_group",
"create_private_stream_policy", "create_private_stream_policy",
"create_public_stream_policy", "create_public_stream_policy",
"create_web_public_stream_policy", "create_web_public_stream_policy",
@ -2103,6 +2125,10 @@ class UserProfile(AbstractBaseUser, PermissionsMixin, UserBaseSettings): # type
]: ]:
raise AssertionError("Invalid policy") raise AssertionError("Invalid policy")
if policy_name in Realm.REALM_PERMISSION_GROUP_SETTINGS:
allowed_user_group = getattr(self.realm, policy_name)
return is_user_in_group(allowed_user_group, self)
policy_value = getattr(self.realm, policy_name) policy_value = getattr(self.realm, policy_name)
if policy_value == Realm.POLICY_NOBODY: if policy_value == Realm.POLICY_NOBODY:
return False return False
@ -2154,6 +2180,9 @@ class UserProfile(AbstractBaseUser, PermissionsMixin, UserBaseSettings): # type
def can_invite_others_to_realm(self) -> bool: def can_invite_others_to_realm(self) -> bool:
return self.has_permission("invite_to_realm_policy") return self.has_permission("invite_to_realm_policy")
def can_create_multiuse_invite_to_realm(self) -> bool:
return self.has_permission("create_multiuse_invite_group")
def can_move_messages_between_streams(self) -> bool: def can_move_messages_between_streams(self) -> bool:
return self.has_permission("move_messages_between_streams_policy") return self.has_permission("move_messages_between_streams_policy")

View File

@ -3961,6 +3961,19 @@ paths:
description: | description: |
The [policy](/api/roles-and-permissions#permission-levels) The [policy](/api/roles-and-permissions#permission-levels)
for which users can create bot users in this organization. for which users can create bot users in this organization.
create_multiuse_invite_group:
type: integer
description: |
The ID of the [user group](/api/get-user-groups) whose members are
allowed to create [reusable invitation
links](/help/invite-new-users#create-a-reusable-invitation-link)
to the organization.
This setting can currently only be set to user groups that are
system groups, except for the system groups named
`"role:internet"` and `"role:owners"`.
**Changes**: New in Zulip 8.0 (feature level 209).
create_public_stream_policy: create_public_stream_policy:
type: integer type: integer
description: | description: |
@ -13258,6 +13271,19 @@ paths:
[permission-level]: /api/roles-and-permissions#permission-levels [permission-level]: /api/roles-and-permissions#permission-levels
[calc-full-member]: /api/roles-and-permissions#determining-if-a-user-is-a-full-member [calc-full-member]: /api/roles-and-permissions#determining-if-a-user-is-a-full-member
realm_create_multiuse_invite_group:
type: integer
description: |
The ID of the [user group](/api/get-user-groups) whose members are
allowed to create [reusable invitation
links](/help/invite-new-users#create-a-reusable-invitation-link)
to the organization.
This setting can currently only be set to user groups that are
system groups, except for the system groups named
`"role:internet"` and `"role:owners"`.
**Changes**: New in Zulip 8.0 (feature level 209).
realm_move_messages_between_streams_policy: realm_move_messages_between_streams_policy:
type: integer type: integer
description: | description: |

View File

@ -71,6 +71,7 @@ from zerver.actions.realm_logo import do_change_logo_source
from zerver.actions.realm_playgrounds import check_add_realm_playground, do_remove_realm_playground from zerver.actions.realm_playgrounds import check_add_realm_playground, do_remove_realm_playground
from zerver.actions.realm_settings import ( from zerver.actions.realm_settings import (
do_change_realm_org_type, do_change_realm_org_type,
do_change_realm_permission_group_setting,
do_change_realm_plan_type, do_change_realm_plan_type,
do_deactivate_realm, do_deactivate_realm,
do_set_realm_authentication_methods, do_set_realm_authentication_methods,
@ -2968,11 +2969,83 @@ class RealmPropertyActionTest(BaseAction):
else: else:
check_realm_update("events[0]", events[0], name) check_realm_update("events[0]", events[0], name)
def do_set_realm_permission_group_setting_test(self, setting_name: str) -> None:
all_system_user_groups = UserGroup.objects.filter(
realm=self.user_profile.realm,
is_system_group=True,
)
setting_permission_configuration = Realm.REALM_PERMISSION_GROUP_SETTINGS[setting_name]
default_group_name = setting_permission_configuration.default_group_name
default_group = all_system_user_groups.get(name=default_group_name)
old_group_id = default_group.id
now = timezone_now()
do_change_realm_permission_group_setting(
self.user_profile.realm,
setting_name,
default_group,
acting_user=self.user_profile,
)
self.assertEqual(
RealmAuditLog.objects.filter(
realm=self.user_profile.realm,
event_type=RealmAuditLog.REALM_PROPERTY_CHANGED,
event_time__gte=now,
acting_user=self.user_profile,
).count(),
1,
)
for user_group in all_system_user_groups:
if user_group.name == default_group_name:
continue
now = timezone_now()
state_change_expected = True
num_events = 1
new_group_id = user_group.id
events = self.verify_action(
lambda: do_change_realm_permission_group_setting(
self.user_profile.realm,
setting_name,
user_group,
acting_user=self.user_profile,
),
state_change_expected=state_change_expected,
num_events=num_events,
)
self.assertEqual(
RealmAuditLog.objects.filter(
realm=self.user_profile.realm,
event_type=RealmAuditLog.REALM_PROPERTY_CHANGED,
event_time__gte=now,
acting_user=self.user_profile,
extra_data={
RealmAuditLog.OLD_VALUE: old_group_id,
RealmAuditLog.NEW_VALUE: new_group_id,
"property": setting_name,
},
).count(),
1,
)
check_realm_update_dict("events[0]", events[0])
old_group_id = new_group_id
def test_change_realm_property(self) -> None: def test_change_realm_property(self) -> None:
for prop in Realm.property_types: for prop in Realm.property_types:
with self.settings(SEND_DIGEST_EMAILS=True): with self.settings(SEND_DIGEST_EMAILS=True):
self.do_set_realm_property_test(prop) self.do_set_realm_property_test(prop)
for prop in Realm.REALM_PERMISSION_GROUP_SETTINGS:
with self.settings(SEND_DIGEST_EMAILS=True):
self.do_set_realm_permission_group_setting_test(prop)
def do_set_realm_user_default_setting_test(self, name: str) -> None: def do_set_realm_user_default_setting_test(self, name: str) -> None:
bool_tests: List[bool] = [True, False, True] bool_tests: List[bool] = [True, False, True]
test_values: Dict[str, Any] = dict( test_values: Dict[str, Any] = dict(

View File

@ -115,6 +115,7 @@ class HomeTest(ZulipTestCase):
"realm_bot_creation_policy", "realm_bot_creation_policy",
"realm_bot_domain", "realm_bot_domain",
"realm_bots", "realm_bots",
"realm_create_multiuse_invite_group",
"realm_create_private_stream_policy", "realm_create_private_stream_policy",
"realm_create_public_stream_policy", "realm_create_public_stream_policy",
"realm_create_web_public_stream_policy", "realm_create_web_public_stream_policy",

View File

@ -1045,6 +1045,13 @@ class RealmImportExportTest(ExportFile):
def get_active_stream_names(r: Realm) -> Set[str]: def get_active_stream_names(r: Realm) -> Set[str]:
return {stream.name for stream in get_active_streams(r)} return {stream.name for stream in get_active_streams(r)}
@getter
def get_group_names_for_group_settings(r: Realm) -> Set[str]:
return {
getattr(r, permmission_name).name
for permmission_name in Realm.REALM_PERMISSION_GROUP_SETTINGS
}
# test recipients # test recipients
def get_recipient_stream(r: Realm) -> Recipient: def get_recipient_stream(r: Realm) -> Recipient:
recipient = Stream.objects.get(name="Verona", realm=r).recipient recipient = Stream.objects.get(name="Verona", realm=r).recipient
@ -1559,6 +1566,12 @@ class RealmImportExportTest(ExportFile):
data = read_json("realm.json") data = read_json("realm.json")
data.pop("zerver_usergroup") data.pop("zerver_usergroup")
data.pop("zerver_realmauditlog") data.pop("zerver_realmauditlog")
# User groups data is missing. So, all the realm group based settings
# should be None.
for setting_name in Realm.REALM_PERMISSION_GROUP_SETTINGS:
data["zerver_realm"][0][setting_name] = None
with open(export_fn("realm.json"), "wb") as f: with open(export_fn("realm.json"), "wb") as f:
f.write(orjson.dumps(data)) f.write(orjson.dumps(data))

View File

@ -37,7 +37,11 @@ from zerver.actions.invites import (
do_revoke_multi_use_invite, do_revoke_multi_use_invite,
too_many_recent_realm_invites, too_many_recent_realm_invites,
) )
from zerver.actions.realm_settings import do_change_realm_plan_type, do_set_realm_property from zerver.actions.realm_settings import (
do_change_realm_permission_group_setting,
do_change_realm_plan_type,
do_set_realm_property,
)
from zerver.actions.user_settings import do_change_full_name from zerver.actions.user_settings import do_change_full_name
from zerver.actions.users import change_user_is_active from zerver.actions.users import change_user_is_active
from zerver.context_processors import common_context from zerver.context_processors import common_context
@ -55,6 +59,7 @@ from zerver.models import (
Realm, Realm,
ScheduledEmail, ScheduledEmail,
Stream, Stream,
UserGroup,
UserMessage, UserMessage,
UserProfile, UserProfile,
get_realm, get_realm,
@ -2444,22 +2449,62 @@ class MultiuseInviteTest(ZulipTestCase):
self.assert_length(get_default_streams_for_realm_as_dicts(self.realm.id), 1) self.assert_length(get_default_streams_for_realm_as_dicts(self.realm.id), 1)
self.check_user_subscribed_only_to_streams("alice", []) self.check_user_subscribed_only_to_streams("alice", [])
def test_only_admin_can_create_multiuse_link_api_call(self) -> None: def test_create_multiuse_invite_group_setting(self) -> None:
self.login("iago") realm = get_realm("zulip")
# Only admins should be able to create multiuse invites even if full_members_system_group = UserGroup.objects.get(
# invite_to_realm_policy is set to Realm.POLICY_MEMBERS_ONLY. name=UserGroup.FULL_MEMBERS_GROUP_NAME, realm=realm, is_system_group=True
self.realm.invite_to_realm_policy = Realm.POLICY_MEMBERS_ONLY
self.realm.save()
result = self.client_post(
"/json/invites/multiuse", {"invite_expires_in_minutes": 2 * 24 * 60}
) )
nobody_system_group = UserGroup.objects.get(
name=UserGroup.NOBODY_GROUP_NAME, realm=realm, is_system_group=True
)
# Default value of create_multiuse_invite_group is administrators
self.login("shiva")
result = self.client_post("/json/invites/multiuse")
self.assert_json_error(result, "Insufficient permission")
self.login("iago")
result = self.client_post("/json/invites/multiuse")
invite_link = self.assert_json_success(result)["invite_link"] invite_link = self.assert_json_success(result)["invite_link"]
self.check_user_able_to_register(self.nonreg_email("test"), invite_link) self.check_user_able_to_register(self.nonreg_email("test"), invite_link)
do_change_realm_permission_group_setting(
realm, "create_multiuse_invite_group", full_members_system_group, acting_user=None
)
self.login("hamlet") self.login("hamlet")
result = self.client_post("/json/invites/multiuse") result = self.client_post("/json/invites/multiuse")
self.assert_json_error(result, "Must be an organization administrator") invite_link = self.assert_json_success(result)["invite_link"]
self.check_user_able_to_register(self.nonreg_email("test1"), invite_link)
self.login("desdemona")
do_change_realm_permission_group_setting(
realm, "create_multiuse_invite_group", nobody_system_group, acting_user=None
)
result = self.client_post("/json/invites/multiuse")
self.assert_json_error(result, "Insufficient permission")
def test_only_owner_can_change_create_multiuse_invite_group(self) -> None:
realm = get_realm("zulip")
full_members_system_group = UserGroup.objects.get(
name=UserGroup.FULL_MEMBERS_GROUP_NAME, realm=realm, is_system_group=True
)
self.login("iago")
result = self.client_patch(
"/json/realm",
{"create_multiuse_invite_group": orjson.dumps(full_members_system_group.id).decode()},
)
self.assert_json_error(result, "Must be an organization owner")
self.login("desdemona")
result = self.client_patch(
"/json/realm",
{"create_multiuse_invite_group": orjson.dumps(full_members_system_group.id).decode()},
)
self.assert_json_success(result)
realm = get_realm("zulip")
self.assertEqual(realm.create_multiuse_invite_group_id, full_members_system_group.id)
def test_multiuse_link_for_inviting_as_owner(self) -> None: def test_multiuse_link_for_inviting_as_owner(self) -> None:
self.login("iago") self.login("iago")
@ -2483,6 +2528,78 @@ class MultiuseInviteTest(ZulipTestCase):
invite_link = self.assert_json_success(result)["invite_link"] invite_link = self.assert_json_success(result)["invite_link"]
self.check_user_able_to_register(self.nonreg_email("test"), invite_link) self.check_user_able_to_register(self.nonreg_email("test"), invite_link)
def test_multiuse_link_for_inviting_as_admin(self) -> None:
realm = get_realm("zulip")
full_members_system_group = UserGroup.objects.get(
name=UserGroup.FULL_MEMBERS_GROUP_NAME, realm=realm, is_system_group=True
)
do_change_realm_permission_group_setting(
realm, "create_multiuse_invite_group", full_members_system_group, acting_user=None
)
self.login("hamlet")
result = self.client_post(
"/json/invites/multiuse",
{
"invite_as": orjson.dumps(PreregistrationUser.INVITE_AS["REALM_ADMIN"]).decode(),
"invite_expires_in_minutes": 2 * 24 * 60,
},
)
self.assert_json_error(result, "Must be an organization administrator")
self.login("iago")
result = self.client_post(
"/json/invites/multiuse",
{
"invite_as": orjson.dumps(PreregistrationUser.INVITE_AS["REALM_ADMIN"]).decode(),
"invite_expires_in_minutes": 2 * 24 * 60,
},
)
invite_link = self.assert_json_success(result)["invite_link"]
self.check_user_able_to_register(self.nonreg_email("test"), invite_link)
def test_multiuse_link_for_inviting_as_moderator(self) -> None:
realm = get_realm("zulip")
full_members_system_group = UserGroup.objects.get(
name=UserGroup.FULL_MEMBERS_GROUP_NAME, realm=realm, is_system_group=True
)
do_change_realm_permission_group_setting(
realm, "create_multiuse_invite_group", full_members_system_group, acting_user=None
)
self.login("hamlet")
result = self.client_post(
"/json/invites/multiuse",
{
"invite_as": orjson.dumps(PreregistrationUser.INVITE_AS["MODERATOR"]).decode(),
"invite_expires_in_minutes": 2 * 24 * 60,
},
)
self.assert_json_error(result, "Must be an organization administrator")
self.login("shiva")
result = self.client_post(
"/json/invites/multiuse",
{
"invite_as": orjson.dumps(PreregistrationUser.INVITE_AS["MODERATOR"]).decode(),
"invite_expires_in_minutes": 2 * 24 * 60,
},
)
self.assert_json_error(result, "Must be an organization administrator")
self.login("iago")
result = self.client_post(
"/json/invites/multiuse",
{
"invite_as": orjson.dumps(PreregistrationUser.INVITE_AS["REALM_ADMIN"]).decode(),
"invite_expires_in_minutes": 2 * 24 * 60,
},
)
invite_link = self.assert_json_success(result)["invite_link"]
self.check_user_able_to_register(self.nonreg_email("test"), invite_link)
def test_create_multiuse_link_invalid_stream_api_call(self) -> None: def test_create_multiuse_link_invalid_stream_api_call(self) -> None:
self.login("iago") self.login("iago")
result = self.client_post( result = self.client_post(

View File

@ -892,6 +892,14 @@ class RealmTest(ZulipTestCase):
self.assertEqual(realm.plan_type, Realm.PLAN_TYPE_LIMITED) self.assertEqual(realm.plan_type, Realm.PLAN_TYPE_LIMITED)
for (
setting_name,
permissions_configuration,
) in Realm.REALM_PERMISSION_GROUP_SETTINGS.items():
self.assertEqual(
getattr(realm, setting_name).name, permissions_configuration.default_group_name
)
def test_do_create_realm_with_keyword_arguments(self) -> None: def test_do_create_realm_with_keyword_arguments(self) -> None:
date_created = timezone_now() - datetime.timedelta(days=100) date_created = timezone_now() - datetime.timedelta(days=100)
realm = do_create_realm( realm = do_create_realm(
@ -1207,11 +1215,56 @@ class RealmAPITest(ZulipTestCase):
realm = self.update_with_api(name, vals[0]) realm = self.update_with_api(name, vals[0])
self.assertEqual(getattr(realm, name), vals[0]) self.assertEqual(getattr(realm, name), vals[0])
def do_test_realm_permission_group_setting_update_api(self, setting_name: str) -> None:
realm = get_realm("zulip")
all_system_user_groups = UserGroup.objects.filter(
realm=realm,
is_system_group=True,
)
setting_permission_configuration = Realm.REALM_PERMISSION_GROUP_SETTINGS[setting_name]
default_group_name = setting_permission_configuration.default_group_name
default_group = all_system_user_groups.get(name=default_group_name)
self.set_up_db(setting_name, default_group)
for user_group in all_system_user_groups:
if (
(
user_group.name == UserGroup.EVERYONE_ON_INTERNET_GROUP_NAME
and not setting_permission_configuration.allow_internet_group
)
or (
user_group.name == UserGroup.NOBODY_GROUP_NAME
and not setting_permission_configuration.allow_nobody_group
)
or (
user_group.name == UserGroup.OWNERS_GROUP_NAME
and not setting_permission_configuration.allow_owners_group
)
):
value = orjson.dumps(user_group.id).decode()
result = self.client_patch("/json/realm", {setting_name: value})
self.assert_json_error(
result, f"'{setting_name}' setting cannot be set to '{user_group.name}' group."
)
continue
realm = self.update_with_api(setting_name, user_group.id)
self.assertEqual(getattr(realm, setting_name), user_group)
def test_update_realm_properties(self) -> None: def test_update_realm_properties(self) -> None:
for prop in Realm.property_types: for prop in Realm.property_types:
with self.subTest(property=prop): with self.subTest(property=prop):
self.do_test_realm_update_api(prop) self.do_test_realm_update_api(prop)
for prop in Realm.REALM_PERMISSION_GROUP_SETTINGS:
with self.subTest(property=prop):
self.do_test_realm_permission_group_setting_update_api(prop)
# Not in Realm.property_types because org_type has # Not in Realm.property_types because org_type has
# a unique RealmAuditLog event_type. # a unique RealmAuditLog event_type.
def test_update_realm_org_type(self) -> None: def test_update_realm_org_type(self) -> None:

View File

@ -184,7 +184,7 @@ def resend_user_invite_email(
return json_success(request, data={"timestamp": timestamp}) return json_success(request, data={"timestamp": timestamp})
@require_realm_admin @require_member_or_admin
@has_request_variables @has_request_variables
def generate_multiuse_invite_backend( def generate_multiuse_invite_backend(
request: HttpRequest, request: HttpRequest,
@ -200,7 +200,19 @@ def generate_multiuse_invite_backend(
), ),
stream_ids: Sequence[int] = REQ(json_validator=check_list(check_int), default=[]), stream_ids: Sequence[int] = REQ(json_validator=check_list(check_int), default=[]),
) -> HttpResponse: ) -> HttpResponse:
check_role_based_permissions(invite_as, user_profile, require_admin=True) if not user_profile.can_create_multiuse_invite_to_realm():
# Guest users case will not be handled here as it will
# be handled by the decorator above.
raise JsonableError(_("Insufficient permission"))
require_admin = invite_as in [
# Owners can only be invited by owners, checked by separate
# logic in check_role_based_permissions.
PreregistrationUser.INVITE_AS["REALM_OWNER"],
PreregistrationUser.INVITE_AS["REALM_ADMIN"],
PreregistrationUser.INVITE_AS["MODERATOR"],
]
check_role_based_permissions(invite_as, user_profile, require_admin=require_admin)
streams = [] streams = []
for stream_id in stream_ids: for stream_id in stream_ids:

View File

@ -10,6 +10,7 @@ from confirmation.models import Confirmation, ConfirmationKeyError, get_object_f
from zerver.actions.create_realm import do_change_realm_subdomain from zerver.actions.create_realm import do_change_realm_subdomain
from zerver.actions.realm_settings import ( from zerver.actions.realm_settings import (
do_change_realm_org_type, do_change_realm_org_type,
do_change_realm_permission_group_setting,
do_deactivate_realm, do_deactivate_realm,
do_reactivate_realm, do_reactivate_realm,
do_set_realm_authentication_methods, do_set_realm_authentication_methods,
@ -27,6 +28,7 @@ from zerver.lib.request import REQ, has_request_variables
from zerver.lib.response import json_success from zerver.lib.response import json_success
from zerver.lib.retention import parse_message_retention_days from zerver.lib.retention import parse_message_retention_days
from zerver.lib.streams import access_stream_by_id from zerver.lib.streams import access_stream_by_id
from zerver.lib.user_groups import access_user_group_for_setting
from zerver.lib.validator import ( from zerver.lib.validator import (
check_bool, check_bool,
check_capped_string, check_capped_string,
@ -60,6 +62,9 @@ def update_realm(
invite_to_realm_policy: Optional[int] = REQ( invite_to_realm_policy: Optional[int] = REQ(
json_validator=check_int_in(Realm.INVITE_TO_REALM_POLICY_TYPES), default=None json_validator=check_int_in(Realm.INVITE_TO_REALM_POLICY_TYPES), default=None
), ),
create_multiuse_invite_group_id: Optional[int] = REQ(
"create_multiuse_invite_group", json_validator=check_int, default=None
),
name_changes_disabled: Optional[bool] = REQ(json_validator=check_bool, default=None), name_changes_disabled: Optional[bool] = REQ(json_validator=check_bool, default=None),
email_changes_disabled: Optional[bool] = REQ(json_validator=check_bool, default=None), email_changes_disabled: Optional[bool] = REQ(json_validator=check_bool, default=None),
avatar_changes_disabled: Optional[bool] = REQ(json_validator=check_bool, default=None), avatar_changes_disabled: Optional[bool] = REQ(json_validator=check_bool, default=None),
@ -188,7 +193,9 @@ def update_realm(
) )
if ( if (
invite_to_realm_policy is not None or invite_required is not None invite_to_realm_policy is not None
or invite_required is not None
or create_multiuse_invite_group_id is not None
) and not user_profile.is_realm_owner: ) and not user_profile.is_realm_owner:
raise OrganizationOwnerRequiredError raise OrganizationOwnerRequiredError
@ -275,7 +282,16 @@ def update_realm(
# TODO: It should be possible to deduplicate this function up # TODO: It should be possible to deduplicate this function up
# further by some more advanced usage of the # further by some more advanced usage of the
# `REQ/has_request_variables` extraction. # `REQ/has_request_variables` extraction.
req_vars = {k: v for k, v in list(locals().items()) if k in realm.property_types} req_vars = {}
req_group_setting_vars = {}
for k, v in list(locals().items()):
if k in realm.property_types:
req_vars[k] = v
for permissions_configuration in Realm.REALM_PERMISSION_GROUP_SETTINGS.values():
if k == permissions_configuration.id_field_name:
req_group_setting_vars[k] = v
for k, v in list(req_vars.items()): for k, v in list(req_vars.items()):
if v is not None and getattr(realm, k) != v: if v is not None and getattr(realm, k) != v:
@ -285,6 +301,29 @@ def update_realm(
else: else:
data[k] = v data[k] = v
for setting_name, permissions_configuration in Realm.REALM_PERMISSION_GROUP_SETTINGS.items():
setting_group_id_name = permissions_configuration.id_field_name
assert setting_group_id_name in req_group_setting_vars
if req_group_setting_vars[setting_group_id_name] is not None and req_group_setting_vars[
setting_group_id_name
] != getattr(realm, setting_group_id_name):
user_group_id = req_group_setting_vars[setting_group_id_name]
user_group = access_user_group_for_setting(
user_group_id,
user_profile,
setting_name=setting_name,
require_system_group=permissions_configuration.require_system_group,
allow_internet_group=permissions_configuration.allow_internet_group,
allow_owners_group=permissions_configuration.allow_owners_group,
allow_nobody_group=permissions_configuration.allow_nobody_group,
)
do_change_realm_permission_group_setting(
realm, setting_name, user_group, acting_user=user_profile
)
data[setting_name] = user_group_id
# The following realm properties do not fit the pattern above # The following realm properties do not fit the pattern above
# authentication_methods is not supported by the do_set_realm_property # authentication_methods is not supported by the do_set_realm_property
# framework because it's tracked through the RealmAuthenticationMethod table. # framework because it's tracked through the RealmAuthenticationMethod table.