diff --git a/config/locales/en.yml b/config/locales/en.yml index d6932626b99e..3db1c0b3dfe5 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -3626,6 +3626,7 @@ en: label_ical_access_key_generation_hint: "Automatically generated when subscribing to a calendar." label_ical_access_key_latest: "latest" label_ical_access_key_revoke: "Revoke" + label_integrations: "Integrations" label_add_column: "Add column" label_applied_status: "Applied status" label_archive_project: "Archive project" diff --git a/modules/github_integration/app/components/github_integration/admin/page_header_component.html.erb b/modules/github_integration/app/components/github_integration/admin/page_header_component.html.erb new file mode 100644 index 000000000000..5551a143b625 --- /dev/null +++ b/modules/github_integration/app/components/github_integration/admin/page_header_component.html.erb @@ -0,0 +1,36 @@ +<%#-- copyright +OpenProject is an open source project management software. +Copyright (C) the OpenProject GmbH + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License version 3. + +OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: +Copyright (C) 2006-2013 Jean-Philippe Lang +Copyright (C) 2010-2013 the ChiliProject Team + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +as published by the Free Software Foundation; either version 2 +of the License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +See COPYRIGHT and LICENSE files for more details. + +++#%> + +<%= + render(Primer::OpenProject::PageHeader.new) do |header| + header.with_title { t(:label_github_integration) } + header.with_breadcrumbs(breadcrumb_items) + helpers.render_tab_header_nav(header, tabs) + end +%> diff --git a/modules/github_integration/app/components/github_integration/admin/page_header_component.rb b/modules/github_integration/app/components/github_integration/admin/page_header_component.rb new file mode 100644 index 000000000000..d51f404da514 --- /dev/null +++ b/modules/github_integration/app/components/github_integration/admin/page_header_component.rb @@ -0,0 +1,62 @@ +# frozen_string_literal: true + +#-- copyright +# OpenProject is an open source project management software. +# Copyright (C) the OpenProject GmbH +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License version 3. +# +# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: +# Copyright (C) 2006-2013 Jean-Philippe Lang +# Copyright (C) 2010-2013 the ChiliProject Team +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# See COPYRIGHT and LICENSE files for more details. +#++ + +module GithubIntegration + module Admin + class PageHeaderComponent < ApplicationComponent + def tabs + result = [ + { + name: "settings", + path: github_integration_admin_settings_path, + label: t(:label_setting_plural) + } + ] + + if OpenProject::FeatureDecisions.deploy_targets_active? + result << { + name: "deploy_targets", + path: deploy_targets_path, + label: t(:label_deploy_target_plural) + } + end + + result + end + + def breadcrumb_items + [ + { href: admin_index_path, text: t("label_administration") }, + t(:label_github_integration) + ] + end + end + end +end diff --git a/modules/github_integration/app/controllers/github_integration/admin/settings_controller.rb b/modules/github_integration/app/controllers/github_integration/admin/settings_controller.rb new file mode 100644 index 000000000000..35483043c6ce --- /dev/null +++ b/modules/github_integration/app/controllers/github_integration/admin/settings_controller.rb @@ -0,0 +1,65 @@ +# frozen_string_literal: true + +#-- copyright +# OpenProject is an open source project management software. +# Copyright (C) the OpenProject GmbH +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License version 3. +# +# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: +# Copyright (C) 2006-2013 Jean-Philippe Lang +# Copyright (C) 2010-2013 the ChiliProject Team +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# See COPYRIGHT and LICENSE files for more details. +#++ + +module GithubIntegration + module Admin + class SettingsController < ApplicationController + layout "admin" + + menu_item :admin_github_integration + + before_action :require_admin + + def show + settings = plugin_settings + user_id = settings[:github_user_id].presence + @github_comment_user = user_id ? User.find_by(id: user_id) : nil + @webhook_secret = settings[:webhook_secret] + end + + def update + merged = plugin_settings.merge(permitted_params) + Setting.plugin_openproject_github_integration = merged + flash[:notice] = I18n.t(:notice_successful_update) + redirect_to github_integration_admin_settings_path + end + + private + + def permitted_params + params.permit(:github_user_id, :webhook_secret).to_h + end + + def plugin_settings + Hash(Setting.plugin_openproject_github_integration).with_indifferent_access + end + end + end +end diff --git a/modules/github_integration/app/forms/github_integration/admin/settings_form.rb b/modules/github_integration/app/forms/github_integration/admin/settings_form.rb new file mode 100644 index 000000000000..54d6db00ff96 --- /dev/null +++ b/modules/github_integration/app/forms/github_integration/admin/settings_form.rb @@ -0,0 +1,69 @@ +# frozen_string_literal: true + +#-- copyright +# OpenProject is an open source project management software. +# Copyright (C) the OpenProject GmbH +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License version 3. +# +# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: +# Copyright (C) 2006-2013 Jean-Philippe Lang +# Copyright (C) 2010-2013 the ChiliProject Team +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# See COPYRIGHT and LICENSE files for more details. +#++ + +module GithubIntegration + module Admin + class SettingsForm < ApplicationForm + form do |f| + f.autocompleter( + name: :github_user_id, + label: I18n.t(:label_github_comment_user), + caption: I18n.t(:text_github_comment_user_info), + autocomplete_options: { + component: "opce-user-autocompleter", + allowEmpty: true, + defaultData: false, + model: @comment_user_model + } + ) + + f.text_field( + name: :webhook_secret, + label: I18n.t(:label_github_webhook_secret), + caption: I18n.t(:text_github_webhook_secret_info), + value: @webhook_secret, + input_width: :xxlarge + ) + + f.submit( + name: :submit, + label: I18n.t(:button_save), + scheme: :primary + ) + end + + def initialize(comment_user: nil, webhook_secret: nil) + super() + @comment_user_model = comment_user.present? ? { id: comment_user.id, name: comment_user.name } : nil + @webhook_secret = webhook_secret + end + end + end +end diff --git a/modules/github_integration/app/views/deploy_targets/index.html.erb b/modules/github_integration/app/views/deploy_targets/index.html.erb index 3cfc21201ab0..2d80729cb6f2 100644 --- a/modules/github_integration/app/views/deploy_targets/index.html.erb +++ b/modules/github_integration/app/views/deploy_targets/index.html.erb @@ -29,15 +29,7 @@ See COPYRIGHT and LICENSE files for more details. <% html_title t(:label_administration), t(:label_github_integration), t(:label_deploy_target_plural) %> -<%= - render(Primer::OpenProject::PageHeader.new) do |header| - header.with_title { t(:label_github_integration) } - header.with_breadcrumbs( - [{ href: admin_index_path, text: t("label_administration") }, - t(:label_github_integration)] - ) - end -%> +<%= render(GithubIntegration::Admin::PageHeaderComponent.new) %> <%= render(Primer::OpenProject::SubHeader.new) do |subheader| diff --git a/modules/github_integration/app/views/github_integration/admin/settings/show.html.erb b/modules/github_integration/app/views/github_integration/admin/settings/show.html.erb new file mode 100644 index 000000000000..3e464344336c --- /dev/null +++ b/modules/github_integration/app/views/github_integration/admin/settings/show.html.erb @@ -0,0 +1,42 @@ +<%#-- copyright +OpenProject is an open source project management software. +Copyright (C) the OpenProject GmbH + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License version 3. + +OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: +Copyright (C) 2006-2013 Jean-Philippe Lang +Copyright (C) 2010-2013 the ChiliProject Team + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +as published by the Free Software Foundation; either version 2 +of the License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +See COPYRIGHT and LICENSE files for more details. + +++#%> +<% html_title t(:label_administration), t(:label_github_integration), t(:label_setting_plural) %> + +<%= render(GithubIntegration::Admin::PageHeaderComponent.new) %> + +<%= settings_primer_form_with(url: github_integration_admin_settings_path, method: :patch) do |f| %> + <% if @webhook_secret.blank? %> + <%= render(Primer::Alpha::Banner.new(scheme: :warning, icon: :alert, mb: 4)) do %> + <%= t(:text_github_webhook_secret_missing_warning) %> + <% end %> + <% end %> + <%= render GithubIntegration::Admin::SettingsForm.new(f, + comment_user: @github_comment_user, + webhook_secret: @webhook_secret) %> +<% end %> diff --git a/modules/github_integration/config/locales/en.yml b/modules/github_integration/config/locales/en.yml index fe0e76a8c0ec..55eddc66a59c 100644 --- a/modules/github_integration/config/locales/en.yml +++ b/modules/github_integration/config/locales/en.yml @@ -49,6 +49,21 @@ en: label_github_integration: GitHub Integration notice_deploy_target_created: Deploy target created notice_deploy_target_destroyed: Deploy target deleted + label_github_comment_user: "GitHub actor" + label_github_webhook_secret: "Webhook secret" + text_github_comment_user_info: > + The OpenProject user whose API key must be used to authenticate incoming webhook requests. + When set, requests authenticated with any other user's credentials are rejected. + This user also posts automated deploy-status comments on work packages. Defaults to the system user when not set. + text_github_webhook_secret_missing_warning: > + No webhook secret is configured. Any request to the GitHub webhook endpoint will be accepted + without verification, which may allow unauthorized actors to forge events. It is strongly + recommended to set a secret. + text_github_webhook_secret_info: > + A secret token shared with GitHub when configuring the webhook. + When set, OpenProject verifies the X-Hub-Signature-256 header on every incoming request, + rejecting payloads that do not match. Leave blank to skip verification (not recommended). + plugin_openproject_github_integration: name: "OpenProject GitHub Integration" description: "Integrates OpenProject and GitHub for a better workflow" diff --git a/modules/github_integration/config/routes.rb b/modules/github_integration/config/routes.rb index 2bff27a8e463..836be4f8bdfc 100644 --- a/modules/github_integration/config/routes.rb +++ b/modules/github_integration/config/routes.rb @@ -27,5 +27,11 @@ #++ Rails.application.routes.draw do + namespace "github_integration" do + namespace "admin" do + resource :settings, only: %i[show update] + end + end + resources :deploy_targets, only: %i[index new create destroy] end diff --git a/modules/github_integration/lib/open_project/github_integration/engine.rb b/modules/github_integration/lib/open_project/github_integration/engine.rb index 448d566f45c9..c05d86659e89 100644 --- a/modules/github_integration/lib/open_project/github_integration/engine.rb +++ b/modules/github_integration/lib/open_project/github_integration/engine.rb @@ -38,7 +38,8 @@ class Engine < ::Rails::Engine def self.settings { default: { - "github_user_id" => nil + "github_user_id" => nil, + "webhook_secret" => nil } } end @@ -54,10 +55,16 @@ def self.settings settings: ) do ::Redmine::MenuManager.map(:admin_menu) do |menu| + menu.push :admin_integrations, + { controller: "/github_integration/admin/settings", action: "show" }, + if: ->(_) { User.current.admin? }, + icon: :"git-compare", + caption: :label_integrations menu.push :admin_github_integration, - { controller: "/deploy_targets", action: "index" }, - if: ->(_) { OpenProject::FeatureDecisions.deploy_targets_active? && User.current.admin? }, - caption: :label_github_integration, + { controller: "/github_integration/admin/settings", action: "show" }, + parent: :admin_integrations, + if: ->(_) { User.current.admin? }, + caption: "GitHub", icon: "mark-github" end diff --git a/modules/github_integration/lib/open_project/github_integration/hook_handler.rb b/modules/github_integration/lib/open_project/github_integration/hook_handler.rb index ffc7ed7911d7..9fe200e6d349 100644 --- a/modules/github_integration/lib/open_project/github_integration/hook_handler.rb +++ b/modules/github_integration/lib/open_project/github_integration/hook_handler.rb @@ -45,9 +45,26 @@ def process(_hook, request, params, user) Rails.logger.debug { "Received github webhook #{event_type} (#{event_delivery})" } - return 404 unless KNOWN_EVENTS.include?(event_type) && event_delivery - return 403 if user.blank? + return 404 unless valid_event?(event_type, event_delivery) + return 403 unless authorized?(request, user) + notify(params, user, event_type, event_delivery) + 200 + end + + private + + def valid_event?(event_type, event_delivery) + KNOWN_EVENTS.include?(event_type) && event_delivery + end + + def authorized?(request, user) + valid_signature?(request) && + user.present? && + (configured_user_id.blank? || user.id == configured_user_id) + end + + def notify(params, user, event_type, event_delivery) payload = params[:payload] .permit! .to_h @@ -55,10 +72,26 @@ def process(_hook, request, params, user) "github_event" => event_type, "github_delivery" => event_delivery) - event_name = "github.#{event_type}" - OpenProject::Notifications.send(event_name, payload) + OpenProject::Notifications.send("github.#{event_type}", payload) + end - 200 + def valid_signature?(request) + secret = plugin_settings[:webhook_secret].presence + return true if secret.blank? + + signature_header = request.env["HTTP_X_HUB_SIGNATURE_256"] + return false if signature_header.blank? + + expected = "sha256=#{OpenSSL::HMAC.hexdigest('SHA256', secret, request.raw_post)}" + ActiveSupport::SecurityUtils.secure_compare(expected, signature_header) + end + + def configured_user_id + plugin_settings[:github_user_id].presence&.to_i + end + + def plugin_settings + Hash(Setting.plugin_openproject_github_integration).with_indifferent_access end end end diff --git a/modules/github_integration/spec/features/admin_github_integration_spec.rb b/modules/github_integration/spec/features/admin_github_integration_spec.rb index 96132392daba..14f27a05abf6 100644 --- a/modules/github_integration/spec/features/admin_github_integration_spec.rb +++ b/modules/github_integration/spec/features/admin_github_integration_spec.rb @@ -29,13 +29,74 @@ #++ require "spec_helper" +require "support/components/autocompleter/ng_select_autocomplete_helpers" + +RSpec.describe "Admin GitHub Integration settings", :js do + include Components::Autocompleter::NgSelectAutocompleteHelpers -RSpec.describe "Admin GitHub Integration page" do current_user { create(:admin) } - it "renders the page" do - visit deploy_targets_path + describe "deploy targets page" do + it "renders the page" do + visit deploy_targets_path + + expect(page).to have_content("GitHub Integration") + end + end + + describe "settings page" do + shared_let(:github_actor) { create(:user, firstname: "GitHub", lastname: "Actor") } + + before do + Setting.plugin_openproject_github_integration = {} + visit github_integration_admin_settings_path + end + + it "shows a warning banner when no webhook secret is configured" do + expect(page).to have_text I18n.t(:text_github_webhook_secret_missing_warning) + end + + it "saves the webhook secret" do + fill_in I18n.t(:label_github_webhook_secret), with: "my-super-secret" + click_button I18n.t(:button_save) + + expect(page).to have_content(I18n.t(:notice_successful_update)) + + Setting.clear_cache + expect(Setting.plugin_openproject_github_integration["webhook_secret"]).to eq("my-super-secret") + end + + it "does not show the warning banner after a webhook secret is saved" do + expect(page).to have_text I18n.t(:text_github_webhook_secret_missing_warning) + fill_in I18n.t(:label_github_webhook_secret), with: "my-super-secret" + click_button I18n.t(:button_save) + + expect(page).to have_no_text I18n.t(:text_github_webhook_secret_missing_warning) + end + + it "saves the GitHub actor user" do + github_actor # ensure user exists + + select_autocomplete find("opce-user-autocompleter"), + query: github_actor.name, + results_selector: "body" + + click_button I18n.t(:button_save) + + expect(page).to have_content(I18n.t(:notice_successful_update)) + + Setting.clear_cache + expect(Setting.plugin_openproject_github_integration["github_user_id"]).to eq(github_actor.id.to_s) + end + + it "displays the currently saved actor after reload" do + Setting.plugin_openproject_github_integration = { "github_user_id" => github_actor.id.to_s, + "webhook_secret" => "existing-secret" } + + visit github_integration_admin_settings_path - expect(page).to have_content("GitHub Integration") + expect(page).to have_css("opce-user-autocompleter .ng-value", text: github_actor.name) + expect(page).to have_field(I18n.t(:label_github_webhook_secret), with: "existing-secret") + end end end diff --git a/modules/github_integration/spec/lib/open_project/github_integration/hook_handler_spec.rb b/modules/github_integration/spec/lib/open_project/github_integration/hook_handler_spec.rb index 8889254f6da8..7a2cd031c1b8 100644 --- a/modules/github_integration/spec/lib/open_project/github_integration/hook_handler_spec.rb +++ b/modules/github_integration/spec/lib/open_project/github_integration/hook_handler_spec.rb @@ -32,12 +32,13 @@ describe "#process" do let(:handler) { described_class.new } let(:hook) { "fake hook" } + let(:raw_body) { '{"fake":"value"}' } let(:params) { ActionController::Parameters.new({ payload: { "fake" => "value" } }) } let(:environment) do { "HTTP_X_GITHUB_EVENT" => "pull_request", "HTTP_X_GITHUB_DELIVERY" => "veryuniqueid" } end - let(:request) { OpenStruct.new(env: environment) } + let(:request) { Struct.new(:env, :raw_post).new(environment, raw_body) } let(:user) do user = instance_double(User) allow(user).to receive(:id).and_return(12) @@ -65,6 +66,63 @@ end end + context "with webhook secret verification" do + let(:secret) { "super_secret" } + let(:correct_signature) { "sha256=#{OpenSSL::HMAC.hexdigest('SHA256', secret, raw_body)}" } + + before { allow(OpenProject::Notifications).to receive(:send) } + + context "when a secret is configured and the signature matches", + with_settings: { plugin_openproject_github_integration: { webhook_secret: "super_secret" } } do + let(:environment) do + { "HTTP_X_GITHUB_EVENT" => "pull_request", + "HTTP_X_GITHUB_DELIVERY" => "veryuniqueid", + "HTTP_X_HUB_SIGNATURE_256" => correct_signature } + end + + it "returns 200" do + expect(handler.process(hook, request, params, user)).to eq(200) + end + end + + context "when a secret is configured and the signature is wrong", + with_settings: { plugin_openproject_github_integration: { webhook_secret: "super_secret" } } do + let(:environment) do + { "HTTP_X_GITHUB_EVENT" => "pull_request", + "HTTP_X_GITHUB_DELIVERY" => "veryuniqueid", + "HTTP_X_HUB_SIGNATURE_256" => "sha256=invalidsignature" } + end + + it "returns 403" do + expect(handler.process(hook, request, params, user)).to eq(403) + end + + it "does not send a notification" do + handler.process(hook, request, params, user) + expect(OpenProject::Notifications).not_to have_received(:send) + end + end + + context "when a secret is configured and the signature header is missing", + with_settings: { plugin_openproject_github_integration: { webhook_secret: "super_secret" } } do + it "returns 403" do + expect(handler.process(hook, request, params, user)).to eq(403) + end + + it "does not send a notification" do + handler.process(hook, request, params, user) + expect(OpenProject::Notifications).not_to have_received(:send) + end + end + + context "when no secret is configured", + with_settings: { plugin_openproject_github_integration: {} } do + it "returns 200 without requiring a signature" do + expect(handler.process(hook, request, params, user)).to eq(200) + end + end + end + context "with a supported event and a user" do let(:expected_params) do { @@ -88,6 +146,39 @@ result = handler.process(hook, request, params, user) expect(result).to eq(200) end + + context "when a github_user_id is configured" do + context "and the request user matches the configured user", + with_settings: { plugin_openproject_github_integration: { github_user_id: 12 } } do + it "returns 200" do + expect(handler.process(hook, request, params, user)).to eq(200) + end + + it "sends the notification" do + handler.process(hook, request, params, user) + expect(OpenProject::Notifications).to have_received(:send).with("github.pull_request", expected_params) + end + end + + context "and the request user does not match the configured user", + with_settings: { plugin_openproject_github_integration: { github_user_id: 99 } } do + it "returns 403" do + expect(handler.process(hook, request, params, user)).to eq(403) + end + + it "does not send a notification" do + handler.process(hook, request, params, user) + expect(OpenProject::Notifications).not_to have_received(:send) + end + end + end + + context "when no github_user_id is configured", + with_settings: { plugin_openproject_github_integration: {} } do + it "returns 200 regardless of which user authenticates" do + expect(handler.process(hook, request, params, user)).to eq(200) + end + end end end end diff --git a/modules/gitlab_integration/app/components/gitlab_integration/admin/page_header_component.html.erb b/modules/gitlab_integration/app/components/gitlab_integration/admin/page_header_component.html.erb new file mode 100644 index 000000000000..07a53cb07905 --- /dev/null +++ b/modules/gitlab_integration/app/components/gitlab_integration/admin/page_header_component.html.erb @@ -0,0 +1,35 @@ +<%#-- copyright +OpenProject is an open source project management software. +Copyright (C) the OpenProject GmbH + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License version 3. + +OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: +Copyright (C) 2006-2013 Jean-Philippe Lang +Copyright (C) 2010-2013 the ChiliProject Team + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +as published by the Free Software Foundation; either version 2 +of the License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +See COPYRIGHT and LICENSE files for more details. + +++#%> + +<%= + render(Primer::OpenProject::PageHeader.new) do |header| + header.with_title { t(:label_gitlab_integration) } + header.with_breadcrumbs(breadcrumb_items) + end +%> diff --git a/modules/gitlab_integration/app/components/gitlab_integration/admin/page_header_component.rb b/modules/gitlab_integration/app/components/gitlab_integration/admin/page_header_component.rb new file mode 100644 index 000000000000..c355105d9c62 --- /dev/null +++ b/modules/gitlab_integration/app/components/gitlab_integration/admin/page_header_component.rb @@ -0,0 +1,42 @@ +# frozen_string_literal: true + +#-- copyright +# OpenProject is an open source project management software. +# Copyright (C) the OpenProject GmbH +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License version 3. +# +# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: +# Copyright (C) 2006-2013 Jean-Philippe Lang +# Copyright (C) 2010-2013 the ChiliProject Team +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# See COPYRIGHT and LICENSE files for more details. +#++ + +module GitlabIntegration + module Admin + class PageHeaderComponent < ApplicationComponent + def breadcrumb_items + [ + { href: admin_index_path, text: t("label_administration") }, + t(:label_gitlab_integration) + ] + end + end + end +end diff --git a/modules/gitlab_integration/app/controllers/gitlab_integration/admin/settings_controller.rb b/modules/gitlab_integration/app/controllers/gitlab_integration/admin/settings_controller.rb new file mode 100644 index 000000000000..f4fd24ba2bbf --- /dev/null +++ b/modules/gitlab_integration/app/controllers/gitlab_integration/admin/settings_controller.rb @@ -0,0 +1,65 @@ +# frozen_string_literal: true + +#-- copyright +# OpenProject is an open source project management software. +# Copyright (C) the OpenProject GmbH +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License version 3. +# +# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: +# Copyright (C) 2006-2013 Jean-Philippe Lang +# Copyright (C) 2010-2013 the ChiliProject Team +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# See COPYRIGHT and LICENSE files for more details. +#++ + +module GitlabIntegration + module Admin + class SettingsController < ApplicationController + layout "admin" + + menu_item :admin_gitlab_integration + + before_action :require_admin + + def show + settings = plugin_settings + user_id = settings[:gitlab_user_id].presence + @gitlab_comment_user = user_id ? User.find_by(id: user_id) : nil + @webhook_secret = settings[:webhook_secret] + end + + def update + merged = plugin_settings.merge(permitted_params) + Setting.plugin_openproject_gitlab_integration = merged + flash[:notice] = I18n.t(:notice_successful_update) + redirect_to gitlab_integration_admin_settings_path + end + + private + + def permitted_params + params.permit(:gitlab_user_id, :webhook_secret).to_h + end + + def plugin_settings + Hash(Setting.plugin_openproject_gitlab_integration).with_indifferent_access + end + end + end +end diff --git a/modules/gitlab_integration/app/forms/gitlab_integration/admin/settings_form.rb b/modules/gitlab_integration/app/forms/gitlab_integration/admin/settings_form.rb new file mode 100644 index 000000000000..dfee81bb494f --- /dev/null +++ b/modules/gitlab_integration/app/forms/gitlab_integration/admin/settings_form.rb @@ -0,0 +1,69 @@ +# frozen_string_literal: true + +#-- copyright +# OpenProject is an open source project management software. +# Copyright (C) the OpenProject GmbH +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License version 3. +# +# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: +# Copyright (C) 2006-2013 Jean-Philippe Lang +# Copyright (C) 2010-2013 the ChiliProject Team +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# See COPYRIGHT and LICENSE files for more details. +#++ + +module GitlabIntegration + module Admin + class SettingsForm < ApplicationForm + form do |f| + f.autocompleter( + name: :gitlab_user_id, + label: I18n.t(:label_gitlab_actor), + caption: I18n.t(:text_gitlab_actor_info), + autocomplete_options: { + component: "opce-user-autocompleter", + allowEmpty: true, + defaultData: false, + model: @comment_user_model + } + ) + + f.text_field( + name: :webhook_secret, + label: I18n.t(:label_gitlab_webhook_secret), + caption: I18n.t(:text_gitlab_webhook_secret_info), + value: @webhook_secret, + input_width: :xxlarge + ) + + f.submit( + name: :submit, + label: I18n.t(:button_save), + scheme: :primary + ) + end + + def initialize(comment_user: nil, webhook_secret: nil) + super() + @comment_user_model = comment_user.present? ? { id: comment_user.id, name: comment_user.name } : nil + @webhook_secret = webhook_secret + end + end + end +end diff --git a/modules/gitlab_integration/app/views/gitlab_integration/admin/settings/show.html.erb b/modules/gitlab_integration/app/views/gitlab_integration/admin/settings/show.html.erb new file mode 100644 index 000000000000..e353469a7b59 --- /dev/null +++ b/modules/gitlab_integration/app/views/gitlab_integration/admin/settings/show.html.erb @@ -0,0 +1,42 @@ +<%#-- copyright +OpenProject is an open source project management software. +Copyright (C) the OpenProject GmbH + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License version 3. + +OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: +Copyright (C) 2006-2013 Jean-Philippe Lang +Copyright (C) 2010-2013 the ChiliProject Team + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +as published by the Free Software Foundation; either version 2 +of the License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +See COPYRIGHT and LICENSE files for more details. + +++#%> +<% html_title t(:label_administration), t(:label_gitlab_integration), t(:label_setting_plural) %> + +<%= render(GitlabIntegration::Admin::PageHeaderComponent.new) %> + +<%= settings_primer_form_with(url: gitlab_integration_admin_settings_path, method: :patch) do |f| %> + <% if @webhook_secret.blank? %> + <%= render(Primer::Alpha::Banner.new(scheme: :warning, icon: :alert, mb: 4)) do %> + <%= t(:text_gitlab_webhook_secret_missing_warning) %> + <% end %> + <% end %> + <%= render GitlabIntegration::Admin::SettingsForm.new(f, + comment_user: @gitlab_comment_user, + webhook_secret: @webhook_secret) %> +<% end %> diff --git a/modules/gitlab_integration/config/locales/en.yml b/modules/gitlab_integration/config/locales/en.yml index 73a5b6c25612..963b96ddebde 100644 --- a/modules/gitlab_integration/config/locales/en.yml +++ b/modules/gitlab_integration/config/locales/en.yml @@ -56,6 +56,22 @@ en: labels: invalid_schema: "must be an array of hashes with keys: color, title" + label_gitlab_integration: "GitLab Integration" + label_gitlab_actor: "GitLab actor" + label_gitlab_webhook_secret: "Webhook secret" + text_gitlab_actor_info: > + The OpenProject user whose API key must be used to authenticate incoming webhook requests. + When set, requests authenticated with any other user's credentials are rejected. + This user also posts automated comments on work packages. Defaults to the system user when not set. + text_gitlab_webhook_secret_info: > + A secret token shared with GitLab when configuring the webhook. + When set, OpenProject verifies the X-Gitlab-Token header on every incoming request, + rejecting payloads that do not match. Leave blank to skip verification (not recommended). + text_gitlab_webhook_secret_missing_warning: > + No webhook secret is configured. Any request to the GitLab webhook endpoint will be accepted + without verification, which may allow unauthorized actors to forge events. It is strongly + recommended to set a secret. + project_module_gitlab: "GitLab" permission_show_gitlab_content: "Show GitLab content" diff --git a/modules/gitlab_integration/config/routes.rb b/modules/gitlab_integration/config/routes.rb new file mode 100644 index 000000000000..dc2901e396ce --- /dev/null +++ b/modules/gitlab_integration/config/routes.rb @@ -0,0 +1,37 @@ +# frozen_string_literal: true + +#-- copyright +# OpenProject is an open source project management software. +# Copyright (C) the OpenProject GmbH +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License version 3. +# +# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: +# Copyright (C) 2006-2013 Jean-Philippe Lang +# Copyright (C) 2010-2013 the ChiliProject Team +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# See COPYRIGHT and LICENSE files for more details. +#++ + +Rails.application.routes.draw do + namespace "gitlab_integration" do + namespace "admin" do + resource :settings, only: %i[show update] + end + end +end diff --git a/modules/gitlab_integration/lib/open_project/gitlab_integration/engine.rb b/modules/gitlab_integration/lib/open_project/gitlab_integration/engine.rb index 8e79caf2d51e..7882aaff3e57 100644 --- a/modules/gitlab_integration/lib/open_project/gitlab_integration/engine.rb +++ b/modules/gitlab_integration/lib/open_project/gitlab_integration/engine.rb @@ -40,9 +40,28 @@ class Engine < ::Rails::Engine include OpenProject::Plugins::ActsAsOpEngine + def self.settings + { + default: { + "gitlab_user_id" => nil, + "webhook_secret" => nil + } + } + end + register "openproject-gitlab_integration", author_url: "https://github.com/btey/openproject", - bundled: true do + bundled: true, + settings: do + ::Redmine::MenuManager.map(:admin_menu) do |menu| + menu.push :admin_gitlab_integration, + { controller: "/gitlab_integration/admin/settings", action: "show" }, + parent: :admin_integrations, + if: ->(_) { User.current.admin? }, + caption: "GitLab", + icon: :"op-logo-gitlab" + end + project_module(:gitlab, dependencies: :work_package_tracking) do permission(:show_gitlab_content, {}, diff --git a/modules/gitlab_integration/lib/open_project/gitlab_integration/hook_handler.rb b/modules/gitlab_integration/lib/open_project/gitlab_integration/hook_handler.rb index 4602bb862c8b..751e68be82dc 100644 --- a/modules/gitlab_integration/lib/open_project/gitlab_integration/hook_handler.rb +++ b/modules/gitlab_integration/lib/open_project/gitlab_integration/hook_handler.rb @@ -43,25 +43,55 @@ class HookHandler # We need to check validity of the data and send a Notification # which we process in our NotificationHandler. def process(_hook, request, params, user) - event_type = request.env["HTTP_X_GITLAB_EVENT"] - event_type.tr!(" ", "_") - event_type = event_type.to_s.downcase + event_type = normalize_event_type(request) Rails.logger.debug { "Received gitlab webhook #{event_type}" } return 404 unless KNOWN_EVENTS.include?(event_type) - return 403 if user.blank? + return 403 unless authorized?(request, user) + notify(params, user, event_type) + 200 + end + + private + + def normalize_event_type(request) + request.env["HTTP_X_GITLAB_EVENT"].tr(" ", "_").downcase + end + + def authorized?(request, user) + valid_token?(request) && + user.present? && + (configured_user_id.blank? || user.id == configured_user_id) + end + + def notify(params, user, event_type) payload = params[:payload] .permit! .to_h .merge("open_project_user_id" => user.id, "gitlab_event" => event_type) - event_name = :"gitlab.#{event_type}" - OpenProject::Notifications.send(event_name, payload) + OpenProject::Notifications.send(:"gitlab.#{event_type}", payload) + end - 200 + def valid_token?(request) + secret = plugin_settings[:webhook_secret].presence + return true if secret.blank? + + token_header = request.env["HTTP_X_GITLAB_TOKEN"] + return false if token_header.blank? + + ActiveSupport::SecurityUtils.secure_compare(secret, token_header) + end + + def configured_user_id + plugin_settings[:gitlab_user_id].presence&.to_i + end + + def plugin_settings + Hash(Setting.plugin_openproject_gitlab_integration).with_indifferent_access end end end diff --git a/modules/gitlab_integration/spec/features/admin_gitlab_integration_spec.rb b/modules/gitlab_integration/spec/features/admin_gitlab_integration_spec.rb new file mode 100644 index 000000000000..2f22f7c03a95 --- /dev/null +++ b/modules/gitlab_integration/spec/features/admin_gitlab_integration_spec.rb @@ -0,0 +1,90 @@ +# frozen_string_literal: true + +#-- copyright +# OpenProject is an open source project management software. +# Copyright (C) the OpenProject GmbH +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License version 3. +# +# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: +# Copyright (C) 2006-2013 Jean-Philippe Lang +# Copyright (C) 2010-2013 the ChiliProject Team +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# See COPYRIGHT and LICENSE files for more details. +#++ + +require "spec_helper" +require "support/components/autocompleter/ng_select_autocomplete_helpers" + +RSpec.describe "Admin GitLab Integration settings", :js do + include Components::Autocompleter::NgSelectAutocompleteHelpers + + current_user { create(:admin) } + + shared_let(:gitlab_actor) { create(:user, firstname: "GitLab", lastname: "Actor") } + + before do + Setting.plugin_openproject_gitlab_integration = {} + visit gitlab_integration_admin_settings_path + end + + it "shows a warning banner when no webhook secret is configured" do + expect(page).to have_text I18n.t(:text_gitlab_webhook_secret_missing_warning) + end + + it "saves the webhook secret" do + fill_in I18n.t(:label_gitlab_webhook_secret), with: "my-gitlab-secret" + click_button I18n.t(:button_save) + + expect(page).to have_content(I18n.t(:notice_successful_update)) + + Setting.clear_cache + expect(Setting.plugin_openproject_gitlab_integration["webhook_secret"]).to eq("my-gitlab-secret") + end + + it "does not show the warning banner after a webhook secret is saved" do + expect(page).to have_text I18n.t(:text_gitlab_webhook_secret_missing_warning) + fill_in I18n.t(:label_gitlab_webhook_secret), with: "my-gitlab-secret" + click_button I18n.t(:button_save) + + expect(page).to have_no_text I18n.t(:text_gitlab_webhook_secret_missing_warning) + end + + it "saves the GitLab actor user" do + select_autocomplete find("opce-user-autocompleter"), + query: gitlab_actor.name, + results_selector: "body" + + click_button I18n.t(:button_save) + + expect(page).to have_content(I18n.t(:notice_successful_update)) + + Setting.clear_cache + expect(Setting.plugin_openproject_gitlab_integration["gitlab_user_id"]).to eq(gitlab_actor.id.to_s) + end + + it "displays the currently saved actor after reload" do + Setting.plugin_openproject_gitlab_integration = { "gitlab_user_id" => gitlab_actor.id.to_s, + "webhook_secret" => "existing-secret" } + + visit gitlab_integration_admin_settings_path + + expect(page).to have_css("opce-user-autocompleter .ng-value", text: gitlab_actor.name) + expect(page).to have_field(I18n.t(:label_gitlab_webhook_secret), with: "existing-secret") + end +end diff --git a/modules/gitlab_integration/spec/lib/open_project/gitlab_integration/hook_handler_spec.rb b/modules/gitlab_integration/spec/lib/open_project/gitlab_integration/hook_handler_spec.rb new file mode 100644 index 000000000000..48a7197a039d --- /dev/null +++ b/modules/gitlab_integration/spec/lib/open_project/gitlab_integration/hook_handler_spec.rb @@ -0,0 +1,179 @@ +# frozen_string_literal: true + +#-- copyright +# OpenProject is an open source project management software. +# Copyright (C) the OpenProject GmbH +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License version 3. +# +# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: +# Copyright (C) 2006-2013 Jean-Philippe Lang +# Copyright (C) 2010-2013 the ChiliProject Team +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# See COPYRIGHT and LICENSE files for more details. +#++ + +require_relative "../../../spec_helper" + +RSpec.describe OpenProject::GitlabIntegration::HookHandler do + describe "#process" do + let(:handler) { described_class.new } + let(:hook) { "fake hook" } + let(:params) { ActionController::Parameters.new({ payload: { "fake" => "value" } }) } + let(:environment) do + { "HTTP_X_GITLAB_EVENT" => "Merge Request Hook" } + end + let(:request) { Struct.new(:env).new(env: environment) } + let(:user) do + user = instance_double(User) + allow(user).to receive(:id).and_return(12) + user + end + + context "with an unsupported event" do + let(:environment) do + { "HTTP_X_GITLAB_EVENT" => "Unsupported Hook" } + end + + it "returns 404" do + result = handler.process(hook, request, params, user) + expect(result).to eq(404) + end + end + + context "with a supported event and without user" do + let(:user) { nil } + + it "returns 403" do + result = handler.process(hook, request, params, user) + expect(result).to eq(403) + end + end + + context "with webhook secret verification" do + let(:secret) { "super_secret" } + + before { allow(OpenProject::Notifications).to receive(:send) } + + context "when a secret is configured and the token matches", + with_settings: { plugin_openproject_gitlab_integration: { webhook_secret: "super_secret" } } do + let(:environment) do + { "HTTP_X_GITLAB_EVENT" => "Merge Request Hook", + "HTTP_X_GITLAB_TOKEN" => secret } + end + + it "returns 200" do + expect(handler.process(hook, request, params, user)).to eq(200) + end + end + + context "when a secret is configured and the token is wrong", + with_settings: { plugin_openproject_gitlab_integration: { webhook_secret: "super_secret" } } do + let(:environment) do + { "HTTP_X_GITLAB_EVENT" => "Merge Request Hook", + "HTTP_X_GITLAB_TOKEN" => "wrong_secret" } + end + + it "returns 403" do + expect(handler.process(hook, request, params, user)).to eq(403) + end + + it "does not send a notification" do + handler.process(hook, request, params, user) + expect(OpenProject::Notifications).not_to have_received(:send) + end + end + + context "when a secret is configured and the token header is missing", + with_settings: { plugin_openproject_gitlab_integration: { webhook_secret: "super_secret" } } do + it "returns 403" do + expect(handler.process(hook, request, params, user)).to eq(403) + end + + it "does not send a notification" do + handler.process(hook, request, params, user) + expect(OpenProject::Notifications).not_to have_received(:send) + end + end + + context "when no secret is configured", + with_settings: { plugin_openproject_gitlab_integration: {} } do + it "returns 200 without requiring a token" do + expect(handler.process(hook, request, params, user)).to eq(200) + end + end + end + + context "with a supported event and a user" do + let(:expected_params) do + { + "fake" => "value", + "open_project_user_id" => 12, + "gitlab_event" => "merge_request_hook" + } + end + + before do + allow(OpenProject::Notifications).to receive(:send) + end + + it "sends a notification with the correct contents" do + handler.process(hook, request, params, user) + expect(OpenProject::Notifications).to have_received(:send).with(:"gitlab.merge_request_hook", expected_params) + end + + it "returns 200" do + result = handler.process(hook, request, params, user) + expect(result).to eq(200) + end + + context "when a gitlab_user_id is configured" do + context "and the request user matches the configured user", + with_settings: { plugin_openproject_gitlab_integration: { gitlab_user_id: 12 } } do + it "returns 200" do + expect(handler.process(hook, request, params, user)).to eq(200) + end + + it "sends the notification" do + handler.process(hook, request, params, user) + expect(OpenProject::Notifications).to have_received(:send).with(:"gitlab.merge_request_hook", expected_params) + end + end + + context "and the request user does not match the configured user", + with_settings: { plugin_openproject_gitlab_integration: { gitlab_user_id: 99 } } do + it "returns 403" do + expect(handler.process(hook, request, params, user)).to eq(403) + end + + it "does not send a notification" do + handler.process(hook, request, params, user) + expect(OpenProject::Notifications).not_to have_received(:send) + end + end + end + + context "when no gitlab_user_id is configured", + with_settings: { plugin_openproject_gitlab_integration: {} } do + it "returns 200 regardless of which user authenticates" do + expect(handler.process(hook, request, params, user)).to eq(200) + end + end + end + end +end diff --git a/spec/features/menu_items/admin_menu_item_spec.rb b/spec/features/menu_items/admin_menu_item_spec.rb index 4b3c5f73bf9e..35c51f1f53c8 100644 --- a/spec/features/menu_items/admin_menu_item_spec.rb +++ b/spec/features/menu_items/admin_menu_item_spec.rb @@ -47,8 +47,8 @@ context "without having any menu items hidden in configuration" do it "must display all menu items" do expect(page).to have_test_selector("menu-blocks--container") - expect(page).to have_test_selector("menu-block", count: 22) - expect(page).to have_test_selector("op-menu--item-action", count: 23) # All plus 'overview' + expect(page).to have_test_selector("menu-block", count: 23) + expect(page).to have_test_selector("op-menu--item-action", count: 24) # All plus 'overview' end end @@ -58,10 +58,10 @@ } do it "must not display the hidden menu items and blocks" do expect(page).to have_test_selector("menu-blocks--container") - expect(page).to have_test_selector("menu-block", count: 21) + expect(page).to have_test_selector("menu-block", count: 22) expect(page).not_to have_test_selector("menu-block", text: I18n.t(:label_color_plural)) - expect(page).to have_test_selector("op-menu--item-action", count: 22) # All plus 'overview' + expect(page).to have_test_selector("op-menu--item-action", count: 23) # All plus 'overview' expect(page).not_to have_test_selector("op-menu--item-action", text: I18n.t(:label_color_plural)) end end