New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Create "support" stream type with topic-specific permissions #19434 #29520
base: main
Are you sure you want to change the base?
Conversation
Hello @sanchi-t, it seems like you have referenced #19434 in your pull request description, but you have not referenced them in your commit message description(s). Referencing an issue in a commit message automatically closes the corresponding issue when the commit is merged, which makes the issue tracker easier to manage. Please run An example of a correctly-formatted commit:
To learn how to write a great commit message, please refer to our guide. |
web/src/stream_popover.js
Outdated
@@ -173,6 +173,7 @@ function build_stream_popover(opts) { | |||
$(this).closest(".popover").fadeOut(500).delay(500).remove(); | |||
|
|||
const sub = stream_popover_sub(e); | |||
hide_stream_popover(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is this an existing bug we should fix via a preparatory PR?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No no I accidentally added an unrelated commit while rebasing.
migrations.AddField( | ||
model_name="stream", | ||
name="stream_topic_view_policy", | ||
field=models.PositiveSmallIntegerField(default=STREAM_POST_POLICY_EVERYONE), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This should probably be a UserGroup
foreign key.
zerver/openapi/zulip.yaml
Outdated
|
||
Whether | ||
|
||
**Changes**: New in Zulip 5.0 (feature level 71). |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
How'd you get that version number? Be careful when copy-pasting :P
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, I was just creating a template for myself to work on later. This PR wasn't ready for review back then.
web/src/settings_config.ts
Outdated
"Must be invited by a subscriber; Topics created by others will be hidden from non-administrator users", | ||
}), | ||
}, | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This should likely be an independent setting, rather than a private type, in the UI.
I also generally recommend putting "add UI for creating a thing" commits at the end of a branch, after all the commits that make the feature work as designed... since it's generally much easier to merge commits that implement a stream type without actually having any exist, than it is to have theme exist but have incomplete/unfinished behavior.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Got it.
zerver/actions/message_send.py
Outdated
"enable_offline_push_notifications", | ||
"is_bot", | ||
"bot_type", | ||
"long_term_idle", | ||
) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Removing this optimization is likely not a good idea.
zerver/actions/message_send.py
Outdated
) | ||
|
||
if stream and stream.is_support_stream and not message_sender.is_bot: | ||
realm = get_realm_by_id(realm_id) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is available as stream.realm
; no need to fetch it separately.
zerver/lib/topic.py
Outdated
|
||
return first_message_in_topic.sender_id | ||
|
||
def check_access_based_on_stream_topic_view_policy(sender: UserProfile, stream: Stream) -> bool: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This should become a group membership check with the new groups-based permissions systems, which should be much cheaper and also a lot less semi-duplicated code to write.
zerver/actions/message_send.py
Outdated
authorized_user_ids | ||
+ ([topic_creator_user_id] if topic_creator_user_id is not None else []) | ||
+ [sender_id] | ||
) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This might be cleaner with just a set.add(sender_id)
type line after initializing the starting set.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done.
if stream.is_support_stream and not check_access_based_on_stream_topic_view_policy(sender,stream): | ||
topic_creator_user_id = get_topic_creator_user_id(realm.id,stream.recipient_id,topic_name) | ||
if topic_creator_user_id and not (sender.id == topic_creator_user_id): | ||
raise UnauthorizedTopicAccessError |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So as written, you're calling topic_creator_user_id
twice when sending a single message; you'll want to avoid that by just calling it once here, and passing it forward if get_recipient_info
also needs the value.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah fixed that.
I posted an initial review on this, but I think it probably makes sense for you to prioritize on other projects -- I'm not entirely sure after reading this that we have the right product design for this feature. |
The `stream_topic_access_group` field have been added to the stream model and across various types. The new fields causes the query length to be increased by 34 in `test_subs`
`namedusergroup` is also included in the `ID_MAP`.
Using `stream_topic_access_group.name` in `is_support_stream` function is causing the increase in database query count.
model_name="stream", | ||
name="stream_topic_access_group", | ||
field=models.ForeignKey( | ||
null=True, on_delete=django.db.models.deletion.RESTRICT, to="zerver.namedusergroup" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The only reason I am writing NamedUserGroup
as foreign key instead of UserGroup
is that I want to use to is_support_stream
function defined here.
With UserGroup
there will be no name
property on stream_topic_access_group
and I will have to do something like this which I think will be a heavier database operation:
NamedUserGroup.objects.get(
id=stream_topic_access_group_id, realm=realm, is_system_group=True
)
def is_support_stream(self) -> bool: | ||
return self.stream_topic_access_group.name != SystemGroups.EVERYONE | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Accessing stream_topic_access_group.name
is leading to an inflated number of database queries. Unfortunately, I can't find a way around.
The `check_access_based_on_stream_topic_view_policy` function, similar to `check_stream_access_based_on_stream_post_policy`, determines restricted users based on stream_topic_view_policy. In `do_send_messages`, `user_ids` are derived from `um_eligible_user_ids` rather than `active_user_ids` for support streams. This ensures that server events for new messages aren't sent to users who lack access to those messages.
We need to check that the user is not restricted in order to allow them to view messages in support stream.
`is_support_stream` and `stream_topic_view_policy` have been added to various types to align with the stream model.
We need to check if a restricted user can access the topic before echoing their message, which happens on the server side.
def is_user_in_group( | ||
user_group: UserGroup, user: UserProfile, *, direct_member_only: bool = False | ||
user_group: UserGroup, user_id: int, *, direct_member_only: bool = False | ||
) -> bool: | ||
if direct_member_only: | ||
return get_user_group_direct_members(user_group=user_group).filter(id=user.id).exists() | ||
return get_user_group_direct_members(user_group=user_group).filter(id=user_id).exists() | ||
|
||
return get_recursive_group_members(user_group=user_group).filter(id=user.id).exists() | ||
return get_recursive_group_members(user_group=user_group).filter(id=user_id).exists() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The changes were made to maintain the optimization outlined in function get_recipient_info
in message_send.py
, as shown below:
query: ValuesQuerySet[UserProfile, ActiveUserDict] = UserProfile.objects.filter(
is_active=True
).values(
"id",
"enable_online_push_notifications",
"enable_offline_email_notifications",
"enable_offline_push_notifications",
"is_bot",
"bot_type",
"long_term_idle",
)
As later in the function get_recipient_info
we need to call check_access_based_on_stream_topic_access_group
which would then call is_user_in_group
.
Fixes: #19434
This PR introduces support for creating private support-type streams, where topics are accessible only to specific user groups. Members of these designated groups can initiate new topics and participate in discussions within topics they create. Conversely, users outside these restricted groups can view all messages and topics within the stream.
To implement this functionality, a new stream setting named "who can access all topics?" is introduced. This setting is defined by a new stream property called
is_stream_access_group
. By default, the is_stream_access_group property will be assigned the value "role:everyone," indicating that all users will have access to all topics and messages within the stream.Screenshots and screen captures:
Self-review checklist
(variable names, code reuse, readability, etc.).
Communicate decisions, questions, and potential concerns.
Individual commits are ready for review (see commit discipline).
Completed manual review and testing of the following: