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

settings: Allow "resolve topic" permissions to be managed independently. #29965

Open
wants to merge 1 commit into
base: main
Choose a base branch
from

Conversation

roanster007
Copy link
Collaborator

@roanster007 roanster007 commented May 6, 2024

This commit separates the "resolve topic" permissions from the topic editing permissions, through the introduction of two settings - "allow_resolve_topic" to control whether to allow resolve topics in organization, and "resolve_topic_policy" to restrict the resolve topics permissions.

In addition, it also restricts resolve topic permissions to streams where users have posting permissions.

Fixes #21811, #21281

#api desing thread

Screenshots and screen captures:

image

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

zulipbot commented May 6, 2024

Hello @zulip/server-settings members, this pull request was labeled with the "area: settings (admin/org)" label, so you may want to check it out!

@timabbott
Copy link
Sponsor Member

So I guess I should have made sure folks were aware of this previously, but all future permissions settings need to be done using groups (see #19525), and likely building on the approach in #29937.

Maybe you can chat with @sahil839, but I think you might be able to build this on top of #29937, which should merge in the next couple days.

@roanster007 roanster007 force-pushed the iss-21811 branch 5 times, most recently from 8cb9fd4 to e7c6889 Compare May 8, 2024 16:12
@roanster007 roanster007 marked this pull request as ready for review May 8, 2024 16:13
@roanster007 roanster007 marked this pull request as draft May 8, 2024 19:22
@@ -302,7 +303,7 @@ export function dispatch_normal_event(event) {
gear_menu.rerender();
}

if (key === "edit_topic_policy") {
if (key === "edit_topic_policy" || key === "resolve_topic_group") {
message_live_update.rerender_messages_view();
}
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

so as to update the resolve topic icon in the message list.

if (setting_name === "resolve_topic_group") {
group_setting_config = {...group_setting_config, allow_nobody_group: false};
}

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

This is done to remove the "role:nobody" from dropdown options in case of resolve_topic_group since it is represented by checkbox.

if (
realm.realm_resolve_topic_group === user_groups.get_user_group_from_name("role:nobody").id
) {
update_resolve_topic_sub_settings(false);
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

When initial group of the resolve_topic_group is "role:nobody", we don't want to show it in dropdown option. Hence, we rather show the checkbox as unchecked and dropdown disabled with "role:members" (default).

Copy link
Collaborator

Choose a reason for hiding this comment

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

I am not sure if there is need to show the option selected as members when the option is disabled. We can just show the option which was selected before the setting was disabled and I guess this is what we do for other similar settings. Maybe we can have a tooltip on hovering over it that "Resolving topics is not allowed in this organization".

Copy link
Collaborator

Choose a reason for hiding this comment

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

And you can just call the code to disable the dropdown initially in build_page.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

We can just show the option which was selected before the setting was disabled and I guess this is what we do for other similar settings.

The problem here is that we are using just one database field for both allow_resolve_topic and can_resolve_topics_group settings. So technically, if you uncheck the checkbox, the value of this field is role:nobody -- which removes the possibility of storing the last option of dropdown. Hence, I thought I would just simply show the default value (members).

const is_checked = $(e.target).prop("checked");
update_resolve_topic_sub_settings(is_checked);
});

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

This is for disabling/enabling the dropdown depending on the allow_resolve_topic checkbox.

property_name,
property_value,
);
} else {
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

When the permission is set to role:nobody using checkbox, when resolve_topic_group is updated, we set the dropdown value with updated one only if the value !== role:nobody.id. Else, we just disable dropdown.

if (input_value === false) {
user_group = user_groups.get_user_group_from_name("role:nobody");
data.resolve_topic_group = user_group.id;
} else {
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

When submitting request, if allow_resolve_topic checkbox is checked, we send a request of resolve_topic_group = dropdown value. Else, we send request of resolve_topic_group = role:nobody.id.

def is_resolve_topic_request(
user_profile: UserProfile,
message_id: PathOnly[NonNegativeInt],
new_topic_name: OptionalTopic,
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

This is used for determining if the message edit request is a resolve topic request, and calling the corresponding resolve topic method in actions. Not sure if is_resolve_topic_request is a good name.

) and not new_topic_name.startswith(RESOLVED_TOPIC_PREFIX)

if topic_resolved or topic_unresolved:
return True
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I believe the conditions are enough for determining if it is a resolve topic request?

@roanster007
Copy link
Collaborator Author

roanster007 commented May 11, 2024

addressed review:

I think can_resolve_topics_group would be a name that fits the pattern we've begun to use in other places (like can_remove_subscribers_group).

Renamed resolve_topic_group to can_resolve_topics_group.

@roanster007
Copy link
Collaborator Author

Updated it to use the groups object rather than group ids.

return setting_value["direct_subgroups"][0]

return AnonymousSettingGroupDict( # nocoverage
direct_members=setting_value["direct_members"],
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 setting that returns AnonymousSettingGroupDict currently in views/realm. Hence, no coverage for now

Copy link
Collaborator

@sahil839 sahil839 left a comment

Choose a reason for hiding this comment

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

Posted some initial comments. Only reviewed backend changes for now, will review the remaining tommorrow.

@@ -101,6 +132,9 @@ def update_realm(
edit_topic_policy: Optional[int] = REQ(
json_validator=check_int_in(Realm.EDIT_TOPIC_POLICY_TYPES), default=None
),
can_resolve_topics_group_id: Optional[Union[Dict[str, List[int]], int]] = REQ(
Copy link
Collaborator

Choose a reason for hiding this comment

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

It would be better to just name this as can_resolve_topics_group now since it can also be a dict.

Copy link
Collaborator

Choose a reason for hiding this comment

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

I see the docs mention that can_resolve_topics_group can only be set to a system group, so there is no advantage of this conversion and this should just be kept as int. I guess we would support setting it to any group(s) in future, so would be good to just confirm it once whether we want to support only system groups.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Ya, I just realised that there is an assertion that is preventing me to access the group setting if permission_configuration.require_system_group is True (if the setting value is not int), which in this case would hence raise error.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Would still be good to confirm if we just want to allow any group(s) in the API and we can just design the API according to that now instead of reworking it later. We can allow only system groups in the UI for now though or just allow all groups in the UI as well but only single group.

allow_everyone_group=False,
default_group_name=SystemGroups.MEMBERS,
id_field_name="can_resolve_topics_group_id",
allowed_system_groups=[
Copy link
Collaborator

Choose a reason for hiding this comment

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

We generally use this only if we want to allow a couple of groups and we cannot do that using allow_... parameters, which I think is not the case here and you can just keep this list empty.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

ya makes sense! updated

raise JsonableError(_("Direct messages cannot be moved to channels."))

if not user_profile.can_resolve_topic():
raise JsonableError(_("You don't have permission to resolve topics."))
Copy link
Collaborator

Choose a reason for hiding this comment

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

This duplicates some code from check_update_message. Maybe we can do some refactor to still have separate functions but reduce the duplication.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Only the message.is_stream_message() is duplicated. user_profile.can_resolve_topic() is a new method. So I guess it is fine?

Copy link
Collaborator

Choose a reason for hiding this comment

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

But this also needs changes like below to chck permission to move between streams. Might be better to just have a function named check_resolve_topic which is called from check_update_message if the user is trying to resolve the topic.

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 problem with calling it from check_update_message is that there are also lots of checks for the time limit for editing, but according to this issue - There should not be an option to time-limit topic resolution..

Another problem is it only checks the stream post policy if stream_id is None. For resolving the topics, we also need to check it here.

and raises a JsonableError if otherwise.
It returns the number changed.
"""
message = access_message(user_profile, message_id, lock_message=True)
Copy link
Collaborator

Choose a reason for hiding this comment

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

This is also called in is_resolve_topic_request, I guess we should try removing one of them.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

updated!

]
),
]
)
Copy link
Collaborator

Choose a reason for hiding this comment

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

I was working on a PR for converting this to typed_endpoint. I will try to open the PR tommorrow, so that we do not need these changes.

@roanster007
Copy link
Collaborator Author

@sahil839 addressed your reviews.

Copy link
Collaborator

@sahil839 sahil839 left a comment

Choose a reason for hiding this comment

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

Posted some comments.

Much of the frontend code would have been simpler if could just avoid the checkbox and keep the dropdown with "Nobody" option, but yeah having a checkbox to disable resolving seems good from UX perspective so it is fine for now.

}
const sub = stream_data.get_sub(stream_name);
group.user_can_resolve_topic =
stream_data.can_post_messages_in_stream(sub) && settings_data.user_can_resolve_topic();
Copy link
Collaborator

Choose a reason for hiding this comment

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

Has this been discussed to allow only users who can post messages in stream to allow resolving topics?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

yup, it is related to this issue - #21281

Copy link
Collaborator

Choose a reason for hiding this comment

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

Yeah it is related, but #21811 mentions that the behavior should be discussed.

@@ -891,6 +909,9 @@ export function check_property_changed(
case "field_data":
proposed_val = get_input_element_value(elem, "field-data-setting");
break;
case "realm_allow_resolve_topic":
proposed_val = get_input_element_value(elem, "checkbox");
Copy link
Collaborator

Choose a reason for hiding this comment

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

I don't think this is required as we do not have this for other checkbox settings.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

oh ya right, thanks for catching it. Must have left it when I updated the setting. updated it.

unique_id_type: dropdown_widget.DataTypes.NUMBER,
});
settings_components.set_can_resolve_topics_group_widget(can_resolve_topics_group_widget);
can_resolve_topics_group_widget.setup();
Copy link
Collaborator

Choose a reason for hiding this comment

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

We should look to somehow deduplicate this as a follow-up, else there will be lot of these when we would convert existing settings to be group based settings.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

sure! do you want me to update in this rather?

raise JsonableError(_("Direct messages cannot be moved to channels."))

if not user_profile.can_resolve_topic():
raise JsonableError(_("You don't have permission to resolve topics."))
Copy link
Collaborator

Choose a reason for hiding this comment

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

But this also needs changes like below to chck permission to move between streams. Might be better to just have a function named check_resolve_topic which is called from check_update_message if the user is trying to resolve the topic.

if not user_profile.can_move_messages_between_streams():
raise JsonableError(_("You don't have permission to move this message"))
new_stream = access_stream_by_id(user_profile, stream_id, require_active=True)[0]
check_stream_access_based_on_stream_post_policy(user_profile, new_stream)
Copy link
Collaborator

Choose a reason for hiding this comment

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

I am not sure but should we check the stream posting permission in original stream as well as resolving topics is only allowed if you are allowed to post in the original stream.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

ya it checks the permission in the original stream in the else statement below.

const group_id = can_resolve_topics_group_widget.value();
assert(typeof group_id === "number");
data.can_resolve_topics_group = group_id;
continue;
Copy link
Collaborator

Choose a reason for hiding this comment

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

Can you just move this code to a function which will be called only if property_name === "allow_resolve_topic" || property_name === "can_resolve_topics_group" and the function should just return the value of setting by checking two values - it should return "nobody" group if the checkbox is unchecked and the group the ID from the dropdown widget if it is checked.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

ya you're right. it makes it a lot cleaner. updated!

if (
realm.realm_resolve_topic_group === user_groups.get_user_group_from_name("role:nobody").id
) {
update_resolve_topic_sub_settings(false);
Copy link
Collaborator

Choose a reason for hiding this comment

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

I am not sure if there is need to show the option selected as members when the option is disabled. We can just show the option which was selected before the setting was disabled and I guess this is what we do for other similar settings. Maybe we can have a tooltip on hovering over it that "Resolving topics is not allowed in this organization".

if (
realm.realm_resolve_topic_group === user_groups.get_user_group_from_name("role:nobody").id
) {
update_resolve_topic_sub_settings(false);
Copy link
Collaborator

Choose a reason for hiding this comment

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

And you can just call the code to disable the dropdown initially in build_page.

@@ -567,6 +585,10 @@ export function discard_property_element_changes(elem, for_realm_default_setting
case "message_retention_days":
set_message_retention_setting_dropdown(sub);
break;
case "allow_resolve_topic":
settings_components.set_input_element_value($elem, property_value);
update_resolve_topic_sub_settings(property_value);
Copy link
Collaborator

Choose a reason for hiding this comment

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

I think we do not need this. When this function will be called for "allow_resolve_topic" it will also be called for "realm_can_resolve_topics_group", so the update_resolve_topic... part will be handled. And the set_input_element_value will be called in "default" case. Can you check this once?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

ya you're right. updated!

@@ -101,6 +132,9 @@ def update_realm(
edit_topic_policy: Optional[int] = REQ(
json_validator=check_int_in(Realm.EDIT_TOPIC_POLICY_TYPES), default=None
),
can_resolve_topics_group_id: Optional[Union[Dict[str, List[int]], int]] = REQ(
Copy link
Collaborator

Choose a reason for hiding this comment

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

Would still be good to confirm if we just want to allow any group(s) in the API and we can just design the API according to that now instead of reworking it later. We can allow only system groups in the UI for now though or just allow all groups in the UI as well but only single group.

@roanster007
Copy link
Collaborator Author

addressed all the reviews. posted replies for this and this

This commit separates the "resolve topic" permissions from the
topic editing permissions, through the introduction of setting -
"can_resolve_topics_group" which is the groups id of the user
group whose members can resolve topics.

In addition, it also restricts resolve topic permissions to streams
where users have posting permissions.

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

Successfully merging this pull request may close these issues.

Allow "resolve topic" permissions to be managed independently
4 participants