Skip to content
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

Open
wants to merge 16 commits into
base: main
Choose a base branch
from

Conversation

sanchi-t
Copy link
Collaborator

@sanchi-t sanchi-t commented Mar 30, 2024

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
  • Self-reviewed the changes for clarity and maintainability
    (variable names, code reuse, readability, etc.).

Communicate decisions, questions, and potential concerns.

  • Explains differences from previous plans (e.g., issue description).
  • Highlights technical choices and bugs encountered.
  • Calls out remaining decisions and concerns.
  • Automated tests verify logic where appropriate.

Individual commits are ready for review (see commit discipline).

  • Each commit is a coherent idea.
  • Commit message(s) explain reasoning and motivation for changes.

Completed manual review and testing of the following:

  • Visual appearance of the changes.
  • Responsiveness and internationalization.
  • Strings and tooltips.
  • End-to-end functionality of buttons, interactions and flows.
  • Corner cases, error conditions, and easily imagined bugs.

@zulipbot
Copy link
Member

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 git commit --amend in your command line client to amend your commit message description with Fixes #19434..

An example of a correctly-formatted commit:

commit fabd5e450374c8dde65ec35f02140383940fe146
Author: zulipbot
Date:   Sat Mar 18 13:42:40 2017 -0700

    pull requests: Check PR commits reference when issue is referenced.

    Fixes #51.

To learn how to write a great commit message, please refer to our guide.

@@ -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();
Copy link
Sponsor Member

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?

Copy link
Collaborator Author

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),
Copy link
Sponsor Member

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.


Whether

**Changes**: New in Zulip 5.0 (feature level 71).
Copy link
Sponsor Member

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

Copy link
Collaborator Author

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.

"Must be invited by a subscriber; Topics created by others will be hidden from non-administrator users",
}),
},

Copy link
Sponsor Member

@timabbott timabbott Apr 20, 2024

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.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Got it.

"enable_offline_push_notifications",
"is_bot",
"bot_type",
"long_term_idle",
)
Copy link
Sponsor Member

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.

)

if stream and stream.is_support_stream and not message_sender.is_bot:
realm = get_realm_by_id(realm_id)
Copy link
Sponsor Member

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.


return first_message_in_topic.sender_id

def check_access_based_on_stream_topic_view_policy(sender: UserProfile, stream: Stream) -> bool:
Copy link
Sponsor Member

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.

authorized_user_ids
+ ([topic_creator_user_id] if topic_creator_user_id is not None else [])
+ [sender_id]
)
Copy link
Sponsor Member

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.

Copy link
Collaborator Author

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
Copy link
Sponsor Member

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.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah fixed that.

@timabbott
Copy link
Sponsor Member

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.

@timabbott
Copy link
Sponsor Member

I talked to @alya a bit and I think we're closer to having the right product design for this than I'd thought, so feel free to work on this more -- the main reworking required is just using groups to avoid having to redo everything as part of #19525.

@sanchi-t sanchi-t changed the title Create "support" stream type with topic-specific permissions Create "support" stream type with topic-specific permissions #19434 May 14, 2024
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.
@sanchi-t sanchi-t marked this pull request as ready for review May 14, 2024 20:42
model_name="stream",
name="stream_topic_access_group",
field=models.ForeignKey(
null=True, on_delete=django.db.models.deletion.RESTRICT, to="zerver.namedusergroup"
Copy link
Collaborator Author

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
)

Comment on lines +190 to +192
def is_support_stream(self) -> bool:
return self.stream_topic_access_group.name != SystemGroups.EVERYONE

Copy link
Collaborator Author

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.
Comment on lines 466 to +472
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()
Copy link
Collaborator Author

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Create "support" stream type with topic-specific permissions
3 participants