From d1ff871523841eb15b1f29f67c16c8d66e4bb850 Mon Sep 17 00:00:00 2001 From: Aditya Kumar Kasaudhan Date: Fri, 20 Sep 2024 15:11:30 +0530 Subject: [PATCH] webhooks: Support filtering GitHub activity from private repositories. Currently, the GitHub webhook sends activity from both public and private repositories, which could lead to unintended disclosure of sensitive information from private repositories. This commit introduces a ignore_private_repositories parameter to the webhook URL. When set to true, the webhook ignore processing activity from private repositories, ensuring that such activities are not posted to Zulip streams. By default, if the parameter is omitted or set to false, activities from both public and private repositories are processed normally. This provides users with the flexibility to control the visibility of private repository activities without altering the default behavior. More importantly, this introduces a cleaner mechanism for individual incoming webhooks to declare support for settings not common to all webhook integrations. Fixes #31638. --- api_docs/changelog.md | 7 + version.py | 2 +- web/src/integration_url_modal.ts | 82 ++++ web/src/state_data.ts | 10 +- ..._integration_url_config_checkbox_modal.hbs | 7 + ...rate_integration_url_config_text_modal.hbs | 4 + .../generate_integration_url_modal.hbs | 3 + zerver/lib/events.py | 11 +- zerver/lib/integrations.py | 20 +- zerver/lib/users.py | 4 +- zerver/lib/webhooks/common.py | 8 + zerver/openapi/zulip.yaml | 34 +- zerver/tests/test_bots.py | 7 +- ...ll_request__merged_private_repository.json | 412 ++++++++++++++++++ .../push__1_commit_private_repository.json | 163 +++++++ .../github/fixtures/push__merge_queue.json | 2 +- .../webhooks/github/fixtures/repository.json | 2 +- .../github/fixtures/repository_private.json | 119 +++++ zerver/webhooks/github/tests.py | 31 ++ zerver/webhooks/github/view.py | 11 + 20 files changed, 927 insertions(+), 12 deletions(-) create mode 100644 web/templates/settings/generate_integration_url_config_checkbox_modal.hbs create mode 100644 web/templates/settings/generate_integration_url_config_text_modal.hbs create mode 100644 zerver/webhooks/github/fixtures/pull_request__merged_private_repository.json create mode 100644 zerver/webhooks/github/fixtures/push__1_commit_private_repository.json create mode 100644 zerver/webhooks/github/fixtures/repository_private.json diff --git a/api_docs/changelog.md b/api_docs/changelog.md index a306d3a361..d09f7c47c7 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 318** + +* [`POST /register`](/api/register-queue): Updated + `realm_incoming_webhook_bots` with a new `config_options` key, + defining which options should be offered when creating URLs for this + integration. + **Feature level 317** * [`POST /user_groups/create`](/api/create-user-group): diff --git a/version.py b/version.py index d5826b3aaf..fcc6efc6a1 100644 --- a/version.py +++ b/version.py @@ -34,7 +34,7 @@ DESKTOP_WARNING_VERSION = "5.9.3" # new level means in api_docs/changelog.md, as well as "**Changes**" # entries in the endpoint's documentation in `zulip.yaml`. -API_FEATURE_LEVEL = 317 # Last bumped for inclusion of `group_id` in user group creation response. +API_FEATURE_LEVEL = 318 # Last bumped for `WebhookConfigOption` configuration update in realm_incoming_webhook_bots. # Bump the minor PROVISION_VERSION to indicate that folks should provision # only when going from an old version of the code to a newer version. Bump diff --git a/web/src/integration_url_modal.ts b/web/src/integration_url_modal.ts index 61f285fd01..5e6c25b3a6 100644 --- a/web/src/integration_url_modal.ts +++ b/web/src/integration_url_modal.ts @@ -1,7 +1,10 @@ import ClipboardJS from "clipboard"; import $ from "jquery"; import type * as tippy from "tippy.js"; +import {z} from "zod"; +import render_generate_integration_url_config_checkbox_modal from "../templates/settings/generate_integration_url_config_checkbox_modal.hbs"; +import render_generate_integration_url_config_text_modal from "../templates/settings/generate_integration_url_config_text_modal.hbs"; import render_generate_integration_url_modal from "../templates/settings/generate_integration_url_modal.hbs"; import render_integration_events from "../templates/settings/integration_events.hbs"; @@ -14,6 +17,20 @@ import {realm} from "./state_data"; import * as stream_data from "./stream_data"; import * as util from "./util"; +type ConfigOption = { + key: string; + label: string; + validator: string; +}; + +const config_option_schema = z.object({ + key: z.string(), + label: z.string(), + validator: z.string(), +}); + +const config_options_schema = z.array(config_option_schema); + export function show_generate_integration_url_modal(api_key: string): void { const default_url_message = $t_html({defaultMessage: "Integration URL will appear here."}); const streams = stream_data.subscribed_subs(); @@ -42,6 +59,7 @@ export function show_generate_integration_url_modal(api_key: string): void { const $integration_url = $("#generate-integration-url-modal .integration-url"); const $dialog_submit_button = $("#generate-integration-url-modal .dialog_submit_button"); const $show_integration_events = $("#show-integration-events"); + const $config_container = $("#integration-url-config-options-container"); $dialog_submit_button.prop("disabled", true); $("#integration-url-stream_widget").prop("disabled", true); @@ -57,6 +75,40 @@ export function show_generate_integration_url_modal(api_key: string): void { ); }); + function render_config(config: ConfigOption[]): void { + const validated_config = config_options_schema.parse(config); + $config_container.empty(); + + for (const option of validated_config) { + let $config_element: JQuery; + + if (option.validator === "check_bool") { + const config_html = render_generate_integration_url_config_checkbox_modal({ + key: option.key, + label: option.label, + }); + $config_element = $(config_html); + $config_element + .find(`#integration-url-${option.key}-checkbox`) + .on("change", () => { + update_url(); + }); + } else if (option.validator === "check_string") { + const config_html = render_generate_integration_url_config_text_modal({ + key: option.key, + label: option.label, + }); + $config_element = $(config_html); + $config_element.find(`#integration-url-${option.key}-text`).on("change", () => { + update_url(); + }); + } else { + continue; + } + $config_container.append($config_element); + } + } + $override_topic.on("change", function () { const checked = this.checked; $topic_input.parent().toggleClass("hide", !checked); @@ -109,6 +161,7 @@ export function show_generate_integration_url_modal(api_key: string): void { (bot) => bot.name === selected_integration, ); const all_event_types = selected_integration_data?.all_event_types; + const config = selected_integration_data?.config_options; if (all_event_types !== null) { $("#integration-events-parameter").removeClass("hide"); @@ -136,8 +189,27 @@ export function show_generate_integration_url_modal(api_key: string): void { params.set("topic", topic_name); } } + const selected_events = set_events_param(params); + if (config) { + for (const option of config) { + let $input_element; + if (option.validator === "check_bool") { + $input_element = $(`#integration-url-${option.key}-checkbox`); + if ($input_element.prop("checked")) { + params.set(option.key, "true"); + } + } else if (option.validator === "check_string") { + $input_element = $(`#integration-url-${option.key}-text`); + const value = $input_element.val(); + if (value) { + params.set(option.key, value.toString()); + } + } + } + } + const realm_url = realm.realm_url; const base_url = `${realm_url}/api/v1/external/`; $integration_url.text(`${base_url}${selected_integration}?${params.toString()}`); @@ -181,6 +253,15 @@ export function show_generate_integration_url_modal(api_key: string): void { integration_input_dropdown_widget.render(); $(".integration-url-name-wrapper").trigger("input"); + const selected_integration = integration_input_dropdown_widget.value(); + const selected_integration_data = realm.realm_incoming_webhook_bots.find( + (bot) => bot.name === selected_integration, + ); + + if (selected_integration_data?.config_options) { + render_config(selected_integration_data.config_options); + } + dropdown.hide(); event.preventDefault(); event.stopPropagation(); @@ -265,6 +346,7 @@ export function show_generate_integration_url_modal(api_key: string): void { $topic_input.parent().addClass("hide"); stream_input_dropdown_widget.render(direct_messages_option.unique_id); + $config_container.empty(); } } diff --git a/web/src/state_data.ts b/web/src/state_data.ts index 6956e25bf9..8e70f9f7d7 100644 --- a/web/src/state_data.ts +++ b/web/src/state_data.ts @@ -342,7 +342,15 @@ export const realm_schema = z.object({ display_name: z.string(), name: z.string(), all_event_types: z.nullable(z.array(z.string())), - // We currently ignore the `config` field in these objects. + config_options: z + .array( + z.object({ + key: z.string(), + label: z.string(), + validator: z.string(), + }), + ) + .optional(), }), ), realm_inline_image_preview: z.boolean(), diff --git a/web/templates/settings/generate_integration_url_config_checkbox_modal.hbs b/web/templates/settings/generate_integration_url_config_checkbox_modal.hbs new file mode 100644 index 0000000000..5297008e5b --- /dev/null +++ b/web/templates/settings/generate_integration_url_config_checkbox_modal.hbs @@ -0,0 +1,7 @@ +
+ + +
diff --git a/web/templates/settings/generate_integration_url_config_text_modal.hbs b/web/templates/settings/generate_integration_url_config_text_modal.hbs new file mode 100644 index 0000000000..b7ec813241 --- /dev/null +++ b/web/templates/settings/generate_integration_url_config_text_modal.hbs @@ -0,0 +1,4 @@ +
+ + +
diff --git a/web/templates/settings/generate_integration_url_modal.hbs b/web/templates/settings/generate_integration_url_modal.hbs index 6f6d58f882..c4c12a785f 100644 --- a/web/templates/settings/generate_integration_url_modal.hbs +++ b/web/templates/settings/generate_integration_url_modal.hbs @@ -25,6 +25,9 @@ +
+ +